pax_global_header00006660000000000000000000000064146346350320014520gustar00rootroot0000000000000052 comment=098d43b96ba3e0e467c9b3bd6e6941659707fa80 fronzbot-blinkpy-098d43b/000077500000000000000000000000001463463503200153705ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/.coveragerc000066400000000000000000000000731463463503200175110ustar00rootroot00000000000000[run] parallel = true omit = tests/* setup.py fronzbot-blinkpy-098d43b/.github/000077500000000000000000000000001463463503200167305ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/.github/ISSUE_TEMPLATE/000077500000000000000000000000001463463503200211135ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012101463463503200235770ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. 2. 3. **Expected behavior** A clear and concise description of what you expected to happen. **Home Assistant version (if applicable):** **`blinkpy` version (not needed if filling out Home Assistant version):** **Log Output/Additional Information** If using home-assistant, please paste the output of the log showing your error below. If not, please include any additional useful information. ``` PASTE LOG OUTPUT HERE ``` fronzbot-blinkpy-098d43b/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006711463463503200246440ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context about the feature request (such as API endpoint responses, etc). fronzbot-blinkpy-098d43b/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000004771463463503200225410ustar00rootroot00000000000000## Description: **Related issue (if applicable):** fixes # ## Checklist: - [ ] Local tests with `tox` run successfully **PR cannot be meged unless tests pass** - [ ] Changes tested locally to ensure platform still works as intended - [ ] Tests added to verify new code works fronzbot-blinkpy-098d43b/.github/dependabot.yml000066400000000000000000000002271463463503200215610ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 reviewers: - fronzbot fronzbot-blinkpy-098d43b/.github/workflows/000077500000000000000000000000001463463503200207655ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/.github/workflows/build.yml000066400000000000000000000013641463463503200226130ustar00rootroot00000000000000name: build on: push: branches: [ master, dev ] pull_request: branches: [ master, dev ] jobs: build: runs-on: ${{ matrix.platform }} strategy: matrix: platform: - ubuntu-latest python-version: ['3.11'] steps: - name: Check out code from GitHub uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_test.txt pip install tox - name: Build Wheel run: | tox -r -e build fronzbot-blinkpy-098d43b/.github/workflows/coverage.yml000066400000000000000000000030031463463503200232770ustar00rootroot00000000000000name: coverage on: push: branches: [ master, dev ] pull_request: branches: [ master, dev ] jobs: coverage: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.11'] steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_test.txt pip install tox - name: Run Coverage run: | tox -r -e cov - name: Upload coverage uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }} path: coverage.xml overwrite: true upload-coverage: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.11'] needs: - coverage timeout-minutes: 10 steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.6 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.7 with: name: coverage-${{ matrix.python-version }} path: coverage.xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4.4.1 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} name: blinkpy fronzbot-blinkpy-098d43b/.github/workflows/lint.yml000066400000000000000000000021171463463503200224570ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Lint on: push: branches: [ master, dev ] pull_request: branches: [ master, dev ] jobs: lint: runs-on: ubuntu-latest strategy: max-parallel: 2 matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_test.txt - name: Ruff run: | ruff check blinkpy tests blinkapp - name: Black run: | black --check --color --diff blinkpy tests blinkapp - name: RST-Lint run: | rst-lint README.rst CHANGES.rst CONTRIBUTING.rst fronzbot-blinkpy-098d43b/.github/workflows/publish.yml000066400000000000000000000015051463463503200231570ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip pip install twine build - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python -m build twine upload dist/* fronzbot-blinkpy-098d43b/.github/workflows/stale.yml000066400000000000000000000035331463463503200226240ustar00rootroot00000000000000# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. # # You can adjust the behavior by modifying this file. # For more information, see: # https://github.com/actions/stale name: Stale on: schedule: - cron: '13 * * * *' jobs: stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - name: stale-issues uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 60 days-before-close: 7 days-before-pr-stale: -1 days-before-pr-close: -1 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted,priority" stale-issue-message: > There hasn't been any activity on this issue recently. Please make sure to update to the latest blinkpy version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions. - name: stale-pulls uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 days-before-close: 7 days-before-issue-stale: -1 days-before-issue-close: -1 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. Thank you for your contributions. fronzbot-blinkpy-098d43b/.github/workflows/tests.yml000066400000000000000000000016171463463503200226570ustar00rootroot00000000000000name: tests on: push: branches: [ master, dev ] pull_request: branches: [ master, dev ] jobs: pytest: runs-on: ${{ matrix.platform }} strategy: max-parallel: 4 matrix: platform: - ubuntu-latest python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Check out code from GitHub uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_test.txt pip install . - name: Tests run: | python -m pytest \ --timeout=30 \ --durations=10 \ --cov=blinkpy \ --cov-report term-missing fronzbot-blinkpy-098d43b/.gitignore000066400000000000000000000003531463463503200173610ustar00rootroot00000000000000.pytest_cache/* .cache/* .tox/* __pycache__/* htmlcov/* .coverage .coverage.* coverage.xml *.pyc *.egg*/* dist/* .sh build/* docs/_build *.log venv .session* Pipfile Pipfile.lock blink.json blinktest.py .vscode/* fronzbot-blinkpy-098d43b/API.md000066400000000000000000000254261463463503200163340ustar00rootroot00000000000000# BlinkMonitorProtocol Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring & Alert System. Copied from https://github.com/MattTW/BlinkMonitorProtocol I am not affiliated with the company in any way - this documentation is strictly **"AS-IS"**. My goal was to uncover enough to arm and disarm the system programatically so that I can issue those commands in sync with my home alarm system arm/disarm. Just some raw notes at this point but should be enough for creating programmatic APIs. Lots more to be discovered and documented - feel free to contribute! The Client API is a straightforward REST API using JSON and HTTPS. ## Login Client login to the Blink Servers. **Request:** >curl -H "Host: prod.immedia-semi.com" -H "Content-Type: application/json" --data-binary '{ > "password" : "*your blink password*", > "client_specifier" : "iPhone 9.2 | 2.2 | 222", > "email" : "*your blink login/email*" >}' --compressed https://rest.prod.immedia-semi.com/login **Response:** >{"authtoken":{"authtoken":"*an auth token*","message":"auth"},"networks":{"*network id*":{"name":"*name*","onboarded":true}},"region":{"*regioncode for endpoint*":"*region name"}} **Notes:** The authtoken value is passed in a header in future calls. The region code for endpoint is required to form the URL of the REST endpoint for future calls. Depending on the region you are registered you will need to change the REST endpoints below: - from `https://rest.prod.immedia-semi.com` - to `https://rest.prde.immedia-semi.com` if e.g. your device is registered in Germany Please note that at this moment it seems that all regions are not implemented equally: not all endpoints are available in all regions ## Networks Obtain information about the Blink networks defined for the logged in user. **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/networks **Response:** JSON response containing information including Network ID and Account ID. **Notes:** Network ID is needed to issue arm/disarm calls ## Sync Modules Obtain information about the Blink Sync Modules on the given network. **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules **Response:** JSON response containing information about the known state of the Sync module, most notably if it is online **Notes:** Probably not strictly needed but checking result can verify that the sync module is online and will respond to requests to arm/disarm, etc. ## Arm Arm the given network (start recording/reporting motion events) **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/arm **Response:** JSON response containing information about the arm command request, including the command/request ID **Notes:** When this call returns, it does not mean the arm request is complete, the client must gather the request ID from the response and poll for the status of the command. ## Disarm Disarm the given network (stop recording/reporting motion events) **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm **Response:** JSON response containing information about the disarm command request, including the command/request ID **Notes:** When this call returns, it does not mean the disarm request is complete, the client must gather the request ID from the response and poll for the status of the command. ## Command Status Get status info on the given command **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/command/*command_id* **Response:** JSON response containing state information of the given command, most notably whether it has completed and was successful. **Notes:** After an arm/disarm command, the client appears to poll this URL every second or so until the response indicates the command is complete. **Known Commands:** lv_relay, arm, disarm, thumbnail, clip ## Home Screen Return information displayed on the home screen of the mobile client **Request:** >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/homescreen **Response:** JSON response containing information that the mobile client displays on the home page, including: status, armed state, links to thumbnails for each camera, etc. **Notes:** Not necessary to as part of issuing arm/disarm commands, but contains good summary info. ## Events, thumbnails & video captures **Request** Get events for a given network (sync module) -- Need network ID from home >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/events/network/*network__id* **Response** A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip **Request** Get a video clip from the events list >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.mp4** > video.mp4 **Response** The mp4 video **Request** Get a thumbnail from the events list >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.jpg** > video_thumb.jpg **Response** The jpg bytes. **Notes** Note that you replace the 'mp4' with a 'jpg' to get the thumbnail **Request** Captures a new thumbnail for a camera >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail **Response** Command information. **Request** Captures a new video for a camera >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip **Response** Command information. ## Video Information **Request** Get the total number of videos in the system >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/count **Response** JSON response containing the total video count. **Request** Gets a paginated set of video information >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/page/0 **Response** JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length **Request** Gets information for a specific video by ID >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id* **Response** JSON response containing video information **Request** Gets a list of unwatched videos >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/unwatched **Response** JSON response containing unwatched video information **Request** Deletes a video >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*/delete **Response** Unknown - not tested **Request** Deletes all videos >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/videos/deleteall **Response** Unknown - not tested ## Cameras **Request** Gets a list of cameras >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/cameras **Response** JSON response containing camera information **Request** Gets information for one camera >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id* **Response** JSON response containing camera information **Request** Gets camera sensor information >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals **Response** JSON response containing camera sensor information, such as wifi strength, temperature, and battery level **Request** Enables motion detection for one camera >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable **Response** JSON response containing camera information **Request** Disables motion detection for one camera >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable **Response** JSON response containing camera information *Note*: enabling or disabling motion detection is independent of arming or disarming the system. No motion detection or video recording will take place unless the system is armed. ## Miscellaneous **Request** Gets information about devices that have connected to the blink service >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/account/clients **Response** JSON response containing client information, including: type, name, connection time, user ID **Request** Gets information about supported regions >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/regions **Response** JSON response containing region information **Request** Gets information about system health >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/health **Response** "all ports tested are open" **Request** Gets information about programs >curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v1/networks/*network_id*/programs **Response** Unknown. fronzbot-blinkpy-098d43b/CHANGES.rst000066400000000000000000000571541463463503200172060ustar00rootroot00000000000000========= Changelog ========= A list of changes between each release 0.23.0 (2024-06-19) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ See release notes: (`0.23.0 `__) 0.22.7 (2024-04-15) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ See release notes: (`0.22.7 `__) 0.22.6 (2024-01-24) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Test for None type in poll_local_storage_manifest (`@mkmer #859 `__) - Update image after snap by (`@mkmer #861 `__) - fix missing ':' before port number in rtsps adress (`@Rosi2143 #863 `__) - New temperature location (`@mkmer #867 `__) **Other changes** - Add properties for version information (`@mkmer #854 `__) - Complete header match (`@mkmer #856 `__) - Bump ruff from 0.1.11 to 0.1.13 (`@dependabot #858 `__) - Add option to change agent from Auth() init (`@mkmer #860 `__) - Add notification key to login (`@mkmer #862 `__) - Bump ruff from 0.1.13 to 0.1.14 (`@dependabot #868 `__) 0.22.5 (2024-01-07) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Warning: This release removes support for Python 3.8 and adds Python 3.12 support. **Bugfixes** - Add new keys for wifi, lfr, and battery (`@mkmer #835 `__) - Battery level (`@mkmer #837 `__) - Address not awaited warning (`@mkmer #838 `__) - Catch ContentTypeError in 2FA (`@mkmer #843 `__) - Handle empty put response in wait_command (`@mkmer #847 `__) - Change default user agent to fix API calls (`@gingerm0nkey #848 `__) - Android for new user agent (`@mkmer #850 `__) **Other changes** - Remove Py3.8, add 3.12 (`@fronzbot #839 `__) - Deprecate py38 (`@fronzbot #840 `__) - Add/extract firmware version (`@mkmer #842 `__) - Additional logging, fix blinksync json (`@mkmer #844 `__) - Log text response (`@mkmer #845 `__) - Add tests for logging (`@mkmer #846 `__) - Bump ruff to 0.1.11 - Bump black to 23.12.1 - Bump coverage to 7.4.0 - Bump pytest to 7.4.4 0.22.4 (2023-12-18) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Allow kwargs to throttled functions, await sleep in throttle (`@mkmer #823 `__) - add missing entry in type_key_map (`@Rosi2143 #813 `__) ** Other Changes ** - Delete ReadTheDocs - python formatter - docstring format changes - Bump ruff to 0.1.8 - Bump black to 23.12.0 - Bump pygments to 2.17.2 0.22.3 (2023-11-05) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Check for none and empty dict (fix of home-assistant/core#103312) (`@mkmer #800 `__) ** Other Changes ** - Bump ruff to 0.1.3 - Bump pytest to 7.4.3 - Bump black to 23.10.1 0.22.2 (2023-10-13) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Same as 0.22.1 (pypi upload issue) 0.22.1 (2023-10-13) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Fix night vision toggling for older devices (owl) (`@cocasema #756 `__) - Add missing await to blinkapp.py (`@mkmer #768 `__) - Add check command to POST commands (`@mkmer #772 `__) - Fix blinkapp session call (`@mkmer #783 `__) **Other Changes** - Cleanup readme, add breaking change warning - Migrate to puproject.toml + ruff - Bump ruff to 0.0.292 - Bump black to 23.9.1 - Bump coverage to 7.3.2 - Bump build to 1.0.3 - Bump pytest to 7.4.2 - Bump pytest-timeout to 2.2.0 - Fix 'stale' github action 0.22.0 (2023-08-16) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** -None **New Features** - Asyncio conversion (`@mkmer #723 `__) **Other Changes** - Various fixes to codebase to support asyncio - Upgrade flake8 to 6.1.0 - Upgrade pylint to 2.17.5 - Upgrade pytest to 7.4.0 - Upgrade black to 23.7.0 - Upgrade pytest-cov to 4.1.0 - Upgrade pygments to 2.16.1 - Upgrade coverage to 7.3.0 0.21.0 (2023-05-28) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - None **New Features** - Add get_videos_metadata function (`@rhhayward #685 `__) - Add night vision toggling support (`@jrhunger #717 `__) - Add doorbell arming functionality (`@mkmer #719 `__) **Other Changes** - Upgrade pylint to 2.17.4 - Upgrade coverage to 7.2.5 - Upgrade pygments to 2.15.1 - Upgrade pytest to 7.3.1 - Upgrade pytest-sugar to 0.9.7 - Upgrade black to 23.3.0 0.20.0 (2023-01-29) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Misc doorbell fixes (`@jeffothy #623 `__) **New Features** - Add support for local storage API (`@perdue #650 `__) **Other Changes** - Deprecate py3.7 (`@fronzbot #644 `__) - Upgrade pytest to 7.20 - Upgrade pylint to 2.15.10 - Upgrade pre-commit to 3.0.2 - Upgrade black to 22.12.0 - Upgrade flake8 to 6.0.0 - Upgrade coverage to 7.1.0 - Upgrade pydocstyle to 6.3.0 - Upgrade flake8-docstrings to 1.7.0 - Upgrade pygments to 2.14.0 - Upgrade pytest-sugar to 0.9.6 0.19.2 (2022-07-26) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Fix doorbell mapping (`@uvjim #599 `__) - Fix the errors for the Blink doorbell camera (`@ruby-dev #603 `__) **Other Changes** - dev version bump (`@fronzbot #593 `__) - Fix typo in README regarding disarm syntax (`@dashrb #597 `__) 0.19.1 (2022-06-26) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes** - Fix getting doorbell details (`@uvjim #584 `__) - Potential fix for mixed camera usage (`@fronzbot #590 `__) **Other Changes** - doc update (`@dwaltsch #579 `__) - Test re-factoring (`@fronzbot #591 `__) - Bump pylint to 2.14.3 - Bump coverage to 6.41 - Bump black to 22.3.0 0.19.0 (2022-03-20) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes:** - Debug log in prase download method fix (`@tieum #540 `__) - Fix issue with malformed thumbnails (`@fronzbot #550 `__) - Fully support new thumbnail API (`@gdoermann #552 `__) **New Features:** - Support for arm/disarm of Blink Mini cameras (`@mstratford #546 `__) - Add product_type to BlinkCamera class to report type of camera (`@fronzbot #553 `__) - Remove python 3.6 support, add python 3.10 support (`@fronzbot #554 `__) **Other:** - Make code that determines need for unique class (Mini + Doorbells) generic (`@fronzbot #553 `__) - Bump pre-commit to 2.17.0 - Bump pytest-timeout to 2.1.0 - Bump pygments to 2.11.2 - Bump black to 22.1.0 - Bump coverage to 6.3.2 - Bump pytest to 7.1.1 - Bump restructuredtext-lint to 1.4.0 0.18.0 (2021-12-11) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes:** - None **New Features:** - Support for Blink Doorbell (`@magicalyak #526 `__) **Other:** - Bump pytest-cov to 3.0.0 - Bump pre-commit to 2.15.0 - Bump pytest to 6.2.5 - Bump pylint to 2.10.2 - Bump pygments to 2.10.0 - Bump flake8-docstrings to 1.6.0 - Bump pydocstyle to 6.0.0 - Bump coverage to 5.5 0.17.1 (2021-02-18) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Add delay parameter to Blink.download_videos method in order to throttle API during video retrieval (`@fronzbot #437 `__) - Bump pylint to 2.6.2 0.17.0 (2021-02-15) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes:** - Fix video downloading bug (`@fronzbot #424 `__) - Fix repeated authorization email bug (`@fronzbot #432 `__ and `@fronzbot #428 `__) **New Features:** - Add logout method (`@fronzbot #429 `__) - Add camera record method (`@fronzbot #430 `__) **Other:** - Add debug script to main repo to help with general debug - Upgrade login endpoint from v4 to v5 - Add python 3.9 support - Bump coverage to 5.4 - Bump pytest to 6.2.2 - Bump pytest-cov to 2.11.1 - Bump pygments to 2.8.0 - Bump pre-commit to 2.10.1 - Bump restructuredtext-lint to 1.3.2 0.16.4 (2020-11-22) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Bugfixes:** - Updated liveview endpoint (`@fronzbot #389 `__) - Fixed mini thumbnail not updating (`@fronzbot #388 `__) - Add exception catch to prevent NoneType error on refresh, added test to check behavior as well (`@fronzbot #401 `__) - Unrelated: had to add two force methods to refresh for testing purposes. Should not change normal usage. - Fix malformed stream url (`@fronzbot #395 `__) **All:** - Moved testtools to requirements_test.txt (`@fronzbot #387 `__) - Bumped pytest to 6.1.1 - Bumped flake8 to 3.8.4 - Fixed README spelling (`@rohitsud #381 `__) - 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) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Add user-agent to all headers 0.16.2 (2020-08-01) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Add user-agent to header at login - Remove extra data parameters at login (not-needed) - Bump pytest to 6.0.1 0.16.1 (2020-07-29) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Unpin requirements, set minimum version instead - Bump coverage to 5.2.1 - Bump pytest to 6.0.0 0.16.0 (2020-07-20) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Breaking Changes:** - Add arm property to camera, deprecate motion enable method (`@fronzbot #273 `__) - Complete refactoring of auth logic (breaks all pre-0.16.0 setups!) (`@fronzbot #261 `__) **New Features:** - Add is_errored property to Auth class (`@fronzbot #275 `__) - Add new endpoint to get user infor (`@fronzbot #280 `__) - Add get_liveview command to camera module (`@fronzbot #289 `__) - Add blink Mini Camera support (`@fronzbot #290 `__) - Add option to skip homescreen check (`@fronzbot #305 `__) - Add different timeout for video and image retrieval (`@fronzbot #323 `__) - Modifiy session to use HTTPAdapter and handle retries (`@fronzbot #324 `__) - Add retry option overrides (`@fronzbot #339 `__) **All changes:** Please see the change list in the (`Release Notes `__) 0.15.1 (2020-07-11) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Bugfix: remove "Host" from auth header (`@fronzbot #330 `__) 0.15.0 (2020-05-08) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Breaking Changes:** - Removed support for Python 3.5 (3.6 is now the minimum supported version) - Deprecated ``Blink.login()`` method. Please only use the ``Blink.start()`` method for logging in. **New Functions** - Add ``device_id`` override when logging in (for debug and to differentiate applications) (`@fronzbot #245 `__) This can be used by instantiating the Blink class with the ``device_id`` parameter. **All Changes:** - Fix setup.py use of internal pip structure (`@fronzbot #233 `__) - Update python-slugify requirement from ~=3.0.2 to ~=4.0.0 (`@fronzbot #234 `__) - Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1 (`@fronzbot #230 `__) - Bump requests from 2.22.0 to 2.23.0 (`@fronzbot #231 `__) - Refactor login logic in preparation for 2FA (`@fronzbot #241 `__) - Add 2FA Support (`@fronzbot #242 `__) (fixes (`#210 `__)) - Re-set key_required and available variables after setup (`@fronzbot #245 `__) - Perform system refresh after setup (`@fronzbot #245 `__) - Fix typos (`@fronzbot #244 `__) 0.14.3 (2020-04-22) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Add time check on recorded videos before determining motion - Fix motion detection variable suck to ``True`` - Add ability to load credentials from a json file - Only allow ``motion_detected`` variable to trigger if system was armed - Log response message from server if not attempting a re-authorization 0.14.2 (2019-10-12) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Update dependencies - Dockerize (`@3ch01c #198 `__) 0.14.1 (2019-06-20) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fix timeout problems blocking blinkpy startup - Updated login urls using ``rest-region`` subdomain - Removed deprecated thumbanil recovery from homescreen 0.14.0 (2019-05-23) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Breaking Changes:** - ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery. - Previous logic for calculating percentage was incorrect - raw battery voltage can be accessed via ``BlinkCamera.battery_voltage`` **Bug Fixes:** - Updated video endpoint (fixes broken motion detection) - Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups - Slugify downloaded video names to allow for OS interoperability - Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call. **Everything else:** - Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``. - Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them. - Use UTC for time conversions, rather than local timezone 0.13.1 (2019-03-01) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Remove throttle decorator from network status request 0.13.0 (2019-03-01) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Breaking change:** Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter. - Adds throttle decorator - Decorate following functions with 4s throttle (call method with ``force=True`` to override): - request_network_status - request_syncmodule - request_system_arm - request_system_disarm - request_sync_events - request_new_image - request_new_video - request_video_count - request_cameras - request_camera_info - request_camera_sensors - request_motion_detection_enable - request_motion_detection_disable - Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work. - Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function. - Add ability to cycle through login api endpoints to anticipate future endpoint deprecation 0.12.1 (2019-01-31) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Remove logging improvements since they were incompatible with home-assistant logging 0.12.0 (2019-01-31) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fix video api endpoint, re-enables motion detection - Add improved logging capability - Add download video method - Prevent blinkpy from failing at setup due to api error 0.11.2 (2019-01-23) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Hotfix to prevent platform from stalling due to API change - Motion detection and video recovery broken until new API endpoint discovered 0.11.1 (2019-01-02) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fixed incorrect backup login url - Added calibrated temperature property for cameras 0.11.0 (2018-11-23) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Added support for multiple sync modules 0.10.3 (2018-11-18) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil `__) - Fix incorrect command status endpoint (`@md-reddevil `__) - Add extra debug logging - Remove error prior to re-authorization (only log error when re-auth failed) 0.10.2 (2018-10-30) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases. - When multiple networks detected, changed log level to ``warning`` from ``error`` 0.10.1 (2018-10-18) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fix re-authorization bug (fixes `#101 `__) - Log an error if saving video that doesn't exist 0.10.0 (2018-10-16) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Moved all API calls to own module for easier maintainability - Added network ids to sync module and cameras to allow for multi-network use - Removed dependency on video existance prior to camera setup (fixes `#93 `__) - Camera wifi_strength now reported in wifi "bars" rather than dBm due to API endpoint change - Use homescreen thumbnail as fallback in case it's not in the camera endpoint - Removed "armed" and "status" attributes from camera (status of camera only reported by "motion_enabled" now) - Added serial number attributes to sync module and cameras - Check network_id from login response and verify that network is onboarded (fixes `#90 `__) - Check if retrieved clip is "None" prior to storing in cache 0.9.0 (2018-09-27) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Complete code refactoring to enable future multi-sync module support - Add image and video caching to the cameras - Add internal throttling of system refresh - Use session for http requests **Breaking change:** - Cameras now accessed through sync module ``Blink.sync.cameras`` 0.8.1 (2018-09-24) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Update requirements_test.txt - Update linter versions - Fix pylint warnings - Remove object from class declarations - Remove useless returns from functions - Fix pylint errors - change if comparison to fix (consider-using-in) - Disabled no else-if-return check - Fix useless-import-alias - Disable no-else-return - Fix motion detection - Use an array of recent video clips to determine if motion has been detected. - Reset the value every system refresh 0.8.0 (2018-05-21) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Added support for battery voltage level (fixes `#64 `__) - Added motion detection per camera - Added fully accessible camera configuration dict - Added celcius property to camera (fixes `#60 `__) 0.7.1 (2018-05-09) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fixed pip 10 import issue during setup (`@fronzbot `__) 0.7.0 (2018-02-08) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fixed style errors for bumped pydocstring and pylint versions - Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 `__) - Changed api endpoint for video extraction (fixes `#35 `__ and `#41 `__) - Removed last_motion() function from Blink class - Refactored code for better organization - Moved some request calls out of @property methods (enables future CLI support) - Renamed get_summary() method to summary and changed to @property - Added ability to download most recent video clip - Improved camera arm/disarm handling (`@b10m `__) - Added authentication to ``login()`` function and deprecated ``setup_system()`` in favor of ``start()`` - Added ``attributes`` dictionary to camera object 0.6.0 (2017-05-12) ~~~~~~~~~~~~~~~~~~ - Removed redundent properties that only called hidden variables - Revised request wrapper function to be more intelligent - Added tests to ensure exceptions are caught and handled (100% coverage!) - Added auto-reauthorization (token refresh) when a request fails due to an expired token (`@tySwift93 `__) - Added battery level string to reduce confusion with the way Blink reports battery level as integer from 0 to 3 0.5.2 (2017-03-12) ~~~~~~~~~~~~~~~~~~ - Fixed packaging mishap, same as 0.5.0 otherwise 0.5.0 (2017-03-12) ~~~~~~~~~~~~~~~~~~ - Fixed region handling problem - Added rest.piri subdomain as a backup if region can't be found - Improved the file writing function - Large test coverage increase 0.4.4 (2017-03-06) ~~~~~~~~~~~~~~~~~~ - Fixed bug where region id was not being set in the header 0.4.3 (2017-03-05) ~~~~~~~~~~~~~~~~~~ - Changed to bdist_wheel release 0.4.2 (2017-01-28) ~~~~~~~~~~~~~~~~~~ - Fixed inability to retrieve motion data due to Key Error 0.4.1 (2017-01-27) ~~~~~~~~~~~~~~~~~~ - Fixed refresh bug (0.3.1 did not actually fix the problem) - Image refresh routine added (per camera) - Dictionary of thumbnails per camera added - Improved test coverage 0.3.1 (2017-01-25) ~~~~~~~~~~~~~~~~~~ - Fixed refresh bug (Key Error) 0.3.0 (2017-01-25) ~~~~~~~~~~~~~~~~~~ - Added device id to camera lookup table - Added image to file method 0.2.0 (2017-01-21) ~~~~~~~~~~~~~~~~~~ - Initial release of blinkpy fronzbot-blinkpy-098d43b/CONTRIBUTING.rst000066400000000000000000000106311463463503200200320ustar00rootroot00000000000000======================== Contributing to blinkpy ======================== Everyone is welcome to contribute to blinkpy! The process to get started is described below. Fork the Repository ------------------- You can do this right in github: just click the 'fork' button at the top right. Start Developing ----------------- 1. Setup Local Repository .. code:: bash $ git clone https://github.com//blinkpy.git $ cd blinkpy $ git remote add upstream https://github.com/fronzbot/blinkpy.git 2. Create virtualenv and install dependencies .. code:: bash $ python -m venv venv $ source venv/bin/activate $ pip install -r requirements.txt $ pip install -r requirements_test.txt $ pre-commit install 3. Create a Local Branch First, you will want to create a new branch to hold your changes: ``git checkout -b `` 4. Make changes Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section) 5. Commit Your Changes To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub: .. code:: bash $ git add . $ git commit $ git push origin HEAD 6. Submit your pull request on GitHub - On GitHub, navigate to the `blinkpy `__ repository. - In the "Branch" menu, choose the branch that contains your commits (from your fork). - To the right of the Branch menu, click New pull request. - The base branch dropdown menu should read ``dev``. Use the compare branch drop-down menu to choose the branch you made your changes in. - Type a title and complete the provided description for your pull request. - Click Create pull request. - More detailed instructions can be found here: `Creating a Pull Request` `__ 7. Prior to merge approval Finally, the ``blinkpy`` repository uses continuous integration tools to run tests prior to merging. If there are any problems, you will see a red 'X' next to your pull request. Testing ------- It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions. First, you need to locally install ``tox`` .. code:: bash $ pip install tox You can then run all of the tests with the following command: .. code:: bash $ tox **Tips** If you only want to see if you can pass the local tests, you can run ``tox -e py39`` (or whatever python version you have installed. Only ``py39`` through ``py312`` will be accepted). If you just want to check for style violations, you can run ``tox -e lint``. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters. If you need to change anything in ``requirements.txt`` for any reason, you'll want to regenerate the virtual envrionments used by ``tox`` by running with the ``-r`` flag: ``tox -r`` If you want to run a single test (perhaps you only changed a small thing in one file) you can run ``tox -e py37 -- tests/.py -x``. This will run the test ``.py`` and stop testing upon the first failure, making it easier to figure out why a particular test might be failing. The test structure mimics the library structure, so if you changed something in ``sync_module.py``, the associated test file would be in ``test_sync_module.py`` (ie. the filename is prepended with ``test_``. Catching Up With Reality ------------------------- If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch): .. code:: bash $ git fetch upstream dev $ git rebase upstream/dev If rebase detects conflicts, repeat the following process until all changes have been resolved: 1. ``git status`` shows you the file with a conflict. You will need to edit that file and resolve the lines between ``<<<< | >>>>``. 2. Add the modified file: ``git add `` or ``git add .``. 3. Continue rebase: ``git rebase --continue``. 4. Repeat until all conflicts resolved. fronzbot-blinkpy-098d43b/Dockerfile000066400000000000000000000003411463463503200173600ustar00rootroot00000000000000FROM python:3.7-alpine LABEL maintainer="Kevin Fronczak " VOLUME /media RUN python -m pip install --upgrade pip RUN pip3 install blinkpy COPY blinkapp/ . ENTRYPOINT ["python", "./blinkapp.py"] CMD [] fronzbot-blinkpy-098d43b/LICENSE.md000066400000000000000000000020571463463503200170000ustar00rootroot00000000000000MIT License Copyright (c) 2017 Kevin Fronczak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fronzbot-blinkpy-098d43b/MANIFEST.in000066400000000000000000000001141463463503200171220ustar00rootroot00000000000000include README.rst include LICENSE.md include API.md include tests/*.py fronzbot-blinkpy-098d43b/README.rst000066400000000000000000000270011463463503200170570ustar00rootroot00000000000000blinkpy |Build Status| |Coverage Status| |PyPi Version| |Codestyle| ============================================================================================= A Python library for the Blink Camera system (Python 3.9+) Like the library? Consider buying me a cup of coffee! `Buy me a Coffee! `__ **BREAKING CHANGE WARNING:** As of ``0.22.0`` the library uses asyncio which will break any user scripts used prior to this version. Please see the updated examples below and the ``blinkapp.py`` or ``blinksync.py`` examples in the ``blinkapp/`` directory for examples on how to migrate. **Disclaimer:** Published under the MIT license - See LICENSE file for more details. "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. I am in no way affiliated with Blink, nor Immedia Inc. Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol API calls faster than 60 seconds is not recommended as it can overwhelm Blink's servers. Please use this module responsibly. Installation ------------- ``pip install blinkpy`` Installing Development Version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To install the current development version, perform the following steps. Note that the following will create a blinkpy directory in your home area: .. code:: bash $ cd ~ $ git clone https://github.com/fronzbot/blinkpy.git $ cd blinkpy $ pip install . If you'd like to contribute to this library, please read the `contributing instructions `__. Purpose ------- This library was built with the intention of allowing easy communication with Blink camera systems, specifically to support the `Blink component `__ in `homeassistant `__. Quick Start ============= The simplest way to use this package from a terminal is to call ``await Blink.start()`` which will prompt for your Blink username and password and then log you in. In addition, http requests are throttled internally via use of the ``Blink.refresh_rate`` variable, which can be set at initialization and defaults to 30 seconds. .. code:: python import asyncio from aiohttp import ClientSession from blinkpy.blinkpy import Blink async def start(): blink = Blink(session=ClientSession()) await blink.start() return blink blink = asyncio.run(start()) This flow will prompt you for your username and password. Once entered, if you likely will need to send a 2FA key to the blink servers (this pin is sent to your email address). When you receive this pin, enter at the prompt and the Blink library will proceed with setup. Starting blink without a prompt ------------------------------- In some cases, having an interactive command-line session is not desired. In this case, you will need to set the ``Blink.auth.no_prompt`` value to ``True``. In addition, since you will not be prompted with a username and password, you must supply the login data to the blink authentication handler. This is best done by instantiating your own auth handler with a dictionary containing at least your username and password. .. code:: python import asyncio from aiohttp import ClientSession from blinkpy.blinkpy import Blink from blinkpy.auth import Auth async def start(): blink = Blink(session=ClientSession()) # Can set no_prompt when initializing auth handler auth = Auth({"username": , "password": }, no_prompt=True) blink.auth = auth await blink.start() return blink blink = asyncio.run(start()) Since you will not be prompted for any 2FA pin, you must call the ``blink.auth.send_auth_key`` function. There are two required parameters: the ``blink`` object as well as the ``key`` you received from Blink for 2FA: .. code:: python await auth.send_auth_key(blink, ) await blink.setup_post_verify() Supplying credentials from file -------------------------------- Other use cases may involved loading credentials from a file. This file must be ``json`` formatted and contain a minimum of ``username`` and ``password``. A built in function in the ``blinkpy.helpers.util`` module can aid in loading this file. Note, if ``no_prompt`` is desired, a similar flow can be followed as above. .. code:: python import asyncio from aiohttp import ClientSession from blinkpy.blinkpy import Blink from blinkpy.auth import Auth from blinkpy.helpers.util import json_load async def start(): blink = Blink() auth = Auth(await json_load("")) blink.auth = auth await blink.start() return blink blink = asyncio.run(start()) Saving credentials ------------------- This library also allows you to save your credentials to use in future sessions. Saved information includes authentication tokens as well as unique ids which should allow for a more streamlined experience and limits the frequency of login requests. This data can be saved as follows (it can then be loaded by following the instructions above for supplying credentials from a file): .. code:: python await blink.save("") Getting cameras ---------------- Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App). The below code will display cameras and their available attributes: .. code:: python for name, camera in blink.cameras.items(): print(name) # Name of the camera print(camera.attributes) # Print available attributes of camera The most recent images and videos can be accessed as a bytes-object via internal variables. These can be updated with calls to ``Blink.refresh()`` but will only make a request if motion has been detected or other changes have been found. This can be overridden with the ``force`` flag, but this should be used for debugging only since it overrides the internal request throttling. .. code:: python camera = blink.cameras['SOME CAMERA NAME'] await blink.refresh(force=True) # force a cache update USE WITH CAUTION camera.image_from_cache # bytes-like image object (jpg) camera.video_from_cache # bytes-like video object (mp4) The ``blinkpy`` api also allows for saving images and videos to a file and snapping a new picture from the camera remotely: .. code:: python camera = blink.cameras['SOME CAMERA NAME'] await camera.snap_picture() # Take a new picture with the camera await blink.refresh() # Get new information from server await camera.image_to_file('/local/path/for/image.jpg') await camera.video_to_file('/local/path/for/video.mp4') Arming Blink ------------- Methods exist to arm/disarm the sync module, as well as enable/disable motion detection for individual cameras. This is done as follows: .. code:: python # Arm a sync module await blink.sync["SYNC MODULE NAME"].async_arm(True) # Disarm a sync module await blink.sync["SYNC MODULE NAME"].async_arm(False) # Print arm status of a sync module - a system refresh should be performed first await blink.refresh() sync = blink.sync["SYNC MODULE NAME"] print(f"{sync.name} status: {sync.arm}") Similar methods exist for individual cameras: .. code:: python camera = blink.cameras["SOME CAMERA NAME"] # Enable motion detection on a camera await camera.async_arm(True) # Disable motion detection on a camera await camera.async_arm( False) # Print arm status of a sync module - a system refresh should be performed first await blink.refresh() print(f"{camera.name} status: {camera.arm}") 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 ``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. If you are downloading many items, setting the ``delay`` parameter is advised in order to throttle sequential calls to the API. By default this is set to ``1`` but can be any integer representing the number of seconds to delay between calls. Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory with a 2s delay between calls: .. code:: python await blink.download_videos('/home/blink', since='2018/07/04 09:34', delay=2) Sync Module Local Storage ========================= Description of how I think the local storage API is used by Blink ----------------------------------------------------------------- Since local storage is within a customer's residence, there are no guarantees for latency and availability. As a result, the API seems to be built to deal with these conditions. In general, the approach appears to be this: The Blink app has to query the sync module for all information regarding the stored clips. On a click to view a clip, the app asks for the full list of stored clips, finds the clip in question, uploads the clip to the cloud, and then downloads the clip back from a cloud URL. Each interaction requires polling for the response since networking conditions are uncertain. The app also caches recent clips and the manifest. API steps --------- 1. Request the local storage manifest be created by the sync module. * POST **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/request** * Returns an ID that is used to get the manifest. 2. Retrieve the local storage manifest. * GET **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/request/{manifest_request_id}** * Returns full manifest. * Extract the manifest ID from the response. 3. Find a clip ID in the clips list from the manifest to retrieve, and request an upload. * POST **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/{manifest_id}/clip/request/{clip_id}** * When the response is returned, the upload has finished. 4. Download the clip using the same clip ID. * GET **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/{manifest_id}/clip/request/{clip_id}** .. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg :target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild .. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg :target: https://codecov.io/gh/fronzbot/blinkpy .. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg :target: https://pypi.python.org/pypi/blinkpy .. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black fronzbot-blinkpy-098d43b/blinkapp/000077500000000000000000000000001463463503200171705ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/blinkapp/__init__.py000066400000000000000000000000501463463503200212740ustar00rootroot00000000000000"""Python init file for blinkapp.py.""" fronzbot-blinkpy-098d43b/blinkapp/blinkapp.py000066400000000000000000000021671463463503200213500ustar00rootroot00000000000000"""Script to run blinkpy as an blinkapp.""" from os import environ import asyncio from datetime import datetime, timedelta from aiohttp import ClientSession from blinkpy.blinkpy import Blink from blinkpy.auth import Auth from blinkpy.helpers.util import json_load CREDFILE = environ.get("CREDFILE") TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1)) def get_date(): """Return now - timedelta for blinkpy.""" return (datetime.now() - TIMEDELTA).isoformat() async def download_videos(blink, save_dir="/media"): """Make request to download videos.""" await blink.download_videos(save_dir, since=get_date()) async def start(session: ClientSession): """Startup blink app.""" blink = Blink(session=session) blink.auth = Auth(await json_load(CREDFILE), session=session) await blink.start() return blink async def main(): """Run the blink app.""" session = ClientSession() blink = await start(session) await download_videos(blink) await blink.save(CREDFILE) await session.close() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) fronzbot-blinkpy-098d43b/blinkapp/build.sh000066400000000000000000000000671463463503200206260ustar00rootroot00000000000000#!/bin/bash docker build -t fronzbot/blinkpy:latest ./ fronzbot-blinkpy-098d43b/blinkapp/run.sh000066400000000000000000000010621463463503200203270ustar00rootroot00000000000000#!/bin/bash # bash run.sh [username] [password] if [ "$#" -ne 2 ]; then echo "" echo "ERROR: Requries Blink username and password as arguments." echo "bash run.sh [username] [password]" echo "" exit 1 fi set -ex USER=fronzbot IMAGE=blinkpy CONFIG=$HOME/blinkpy_media USERNAME=$1 PASSWORD=$2 mkdir -p $CONFIG result=$(docker images -q $IMAGE) if [ $result ]; then docker rm $IMAGE fi docker run -it --name ${IMAGE} \ -v $CONFIG:/media \ -e USERNAME=${USERNAME} \ -e PASSWORD=${PASSWORD} \ $USER/$IMAGE \ /bin/bash fronzbot-blinkpy-098d43b/blinkpy/000077500000000000000000000000001463463503200170405ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/blinkpy/__init__.py000066400000000000000000000000351463463503200211470ustar00rootroot00000000000000"""Init file for blinkpy.""" fronzbot-blinkpy-098d43b/blinkpy/api.py000066400000000000000000000374461463463503200202010ustar00rootroot00000000000000"""Implements known blink API calls.""" import logging import string from json import dumps from asyncio import sleep from blinkpy.helpers.util import ( get_time, Throttle, local_storage_clip_url_template, ) from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT, DEFAULT_USER_AGENT _LOGGER = logging.getLogger(__name__) MIN_THROTTLE_TIME = 5 COMMAND_POLL_TIME = 1 MAX_RETRY = 120 async def request_login( auth, url, login_data, is_retry=False, ): """ Login request. :param auth: Auth instance. :param url: Login url. :param login_data: Dictionary containing blink login data. :param is_retry: """ headers = { "Host": DEFAULT_URL, "Content-Type": "application/json", "user-agent": DEFAULT_USER_AGENT, } data = dumps( { "email": login_data["username"], "password": login_data["password"], "unique_id": login_data["uid"], "device_identifier": login_data["device_id"], "client_name": "Computer", "reauth": True, } ) return await auth.query( url=url, headers=headers, data=data, json_resp=False, reqtype="post", is_retry=is_retry, ) async def request_verify(auth, blink, verify_key): """Send verification key to blink servers.""" url = ( f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}" f"/users/{blink.auth.user_id}" f"/clients/{blink.client_id}/client_verification/pin/verify" ) data = dumps({"pin": verify_key}) return await auth.query( url=url, headers=auth.header, data=data, json_resp=False, reqtype="post", ) async def request_logout(blink): """Logout of blink servers.""" url = ( f"{blink.urls.base_url}/api/v4/account/{blink.account_id}" f"/client/{blink.client_id}/logout" ) return await http_post(blink, url=url) async def request_networks(blink): """Request all networks information.""" url = f"{blink.urls.base_url}/networks" return await http_get(blink, url) async def request_network_update(blink, network): """ Request network update. :param blink: Blink instance. :param network: Sync module network id. """ url = f"{blink.urls.base_url}/network/{network}/update" response = await http_post(blink, url) await wait_for_command(blink, response) return response async def request_user(blink): """Get user information from blink servers.""" url = f"{blink.urls.base_url}/user" return await http_get(blink, url) async def request_network_status(blink, network): """ Request network information. :param blink: Blink instance. :param network: Sync module network id. """ url = f"{blink.urls.base_url}/network/{network}" return await http_get(blink, url) async def request_syncmodule(blink, network): """ Request sync module info. :param blink: Blink instance. :param network: Sync module network id. """ url = f"{blink.urls.base_url}/network/{network}/syncmodules" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) async def request_system_arm(blink, network, **kwargs): """ Arm system. :param blink: Blink instance. :param network: Sync module network id. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/state/arm" ) response = await http_post(blink, url) await wait_for_command(blink, response) return response @Throttle(seconds=MIN_THROTTLE_TIME) async def request_system_disarm(blink, network, **kwargs): """ Disarm system. :param blink: Blink instance. :param network: Sync module network id. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/state/disarm" ) response = await http_post(blink, url) await wait_for_command(blink, response) return response async def request_notification_flags(blink, **kwargs): """ Get system notification flags. :param blink: Blink instance. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" "/notifications/configuration" ) response = await http_get(blink, url) await wait_for_command(blink, response) return response async def request_set_notification_flag(blink, data_dict): """ Set a system notification flag. :param blink: Blink instance. :param data_dict: Dictionary of notifications to set. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" "/notifications/configuration" ) data = dumps({"notifications": data_dict}) response = await http_post(blink, url, data=data, json=False) await wait_for_command(blink, response) return response async def request_command_status(blink, network, command_id): """ Request command status. :param blink: Blink instance. :param network: Sync module network id. :param command_id: Command id to check. """ url = f"{blink.urls.base_url}/network/{network}/command/{command_id}" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) async def request_homescreen(blink, **kwargs): """Request homescreen info.""" url = f"{blink.urls.base_url}/api/v3/accounts/{blink.account_id}/homescreen" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) async def request_sync_events(blink, network, **kwargs): """ Request events from sync module. :param blink: Blink instance. :param network: Sync module network id. """ url = f"{blink.urls.base_url}/events/network/{network}" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) async def request_new_image(blink, network, camera_id, **kwargs): """ Request to capture new thumbnail for camera. :param blink: Blink instance. :param network: Sync module network 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" response = await http_post(blink, url) await wait_for_command(blink, response) return response @Throttle(seconds=MIN_THROTTLE_TIME) async def request_new_video(blink, network, camera_id, **kwargs): """ Request to capture new video clip. :param blink: Blink instance. :param network: Sync module network 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" response = await http_post(blink, url) await wait_for_command(blink, response) return response @Throttle(seconds=MIN_THROTTLE_TIME) async def request_video_count(blink, **kwargs): """Request total video count.""" url = f"{blink.urls.base_url}/api/v2/videos/count" return await http_get(blink, url) async def request_videos(blink, time=None, page=0): """ Perform a request for videos. :param blink: Blink instance. :param time: Get videos since this time. In epoch seconds. :param page: Page number to get videos from. """ timestamp = get_time(time) url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/media/changed?since={timestamp}&page={page}" ) return await http_get(blink, url) async def request_cameras(blink, network): """ Request all camera information. :param Blink: Blink instance. :param network: Sync module network id. """ url = f"{blink.urls.base_url}/network/{network}/cameras" return await http_get(blink, url) async def request_camera_info(blink, network, camera_id): """ Request camera info for one camera. :param blink: Blink instance. :param network: Sync module network id. :param camera_id: Camera ID of camera to request info from. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config" return await http_get(blink, url) async def request_camera_usage(blink): """ Request camera status. :param blink: Blink instance. """ url = f"{blink.urls.base_url}/api/v1/camera/usage" return await http_get(blink, url) async def request_camera_liveview(blink, network, camera_id): """ Request camera liveview. :param blink: Blink instance. :param network: Sync module network id. :param camera_id: Camera ID of camera to request liveview from. """ url = ( f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}" f"/networks/{network}/cameras/{camera_id}/liveview" ) response = await http_post(blink, url) await wait_for_command(blink, response) return response async def request_camera_sensors(blink, network, camera_id): """ Request camera sensor info for one camera. :param blink: Blink instance. :param network: Sync module network id. :param camera_id: Camera ID of camera to request sensor info from. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/signals" return await http_get(blink, url) @Throttle(seconds=MIN_THROTTLE_TIME) async def request_motion_detection_enable(blink, network, camera_id, **kwargs): """ Enable motion detection for a camera. :param blink: Blink instance. :param network: Sync module network id. :param camera_id: Camera ID of camera to enable. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/enable" response = await http_post(blink, url) await wait_for_command(blink, response) return response @Throttle(seconds=MIN_THROTTLE_TIME) async def request_motion_detection_disable(blink, network, camera_id, **kwargs): """ Disable motion detection for a camera. :param blink: Blink instance. :param network: Sync module network id. :param camera_id: Camera ID of camera to disable. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/disable" response = await http_post(blink, url) await wait_for_command(blink, response) return response async def request_local_storage_manifest(blink, network, sync_id): """ Update local manifest. Request creation of an updated manifest of video clips stored in sync module local storage. :param blink: Blink instance. :param network: Sync module network id. :param sync_id: ID of sync module. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/sync_modules/{sync_id}" f"/local_storage/manifest/request" ) 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): """ Request manifest of video clips stored in sync module local storage. :param blink: Blink instance. :param network: Sync module network id. :param sync_id: ID of sync module. :param manifest_request_id: Request ID of local storage manifest \ (requested creation of new manifest). """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/sync_modules/{sync_id}" f"/local_storage/manifest/request/{manifest_request_id}" ) return await http_get(blink, url) async def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_id): """ Prepare video clip stored in the sync module to be downloaded. :param blink: Blink instance. :param network: Sync module network id. :param sync_id: ID of sync module. :param manifest_id: ID of local storage manifest (returned in manifest response). :param clip_id: ID of the clip. """ url = blink.urls.base_url + string.Template( local_storage_clip_url_template() ).substitute( account_id=blink.account_id, network_id=network, sync_id=sync_id, manifest_id=manifest_id, clip_id=clip_id, ) 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"): """ Get camera 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" """ if product_type == "owl": url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/owls/{camera_id}/config" ) elif product_type == "catalina": url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config" else: _LOGGER.info( "Camera %s with product type %s config get not implemented.", camera_id, product_type, ) return None return await http_get(blink, url) async def request_update_config( blink, network, camera_id, product_type="owl", data=None ): """ Update camera 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 == "owl": url = ( f"{blink.urls.base_url}/api/v1/accounts/" f"{blink.account_id}/networks/{network}/owls/{camera_id}/config" ) elif product_type == "catalina": url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/update" else: _LOGGER.info( "Camera %s with product type %s config update not implemented.", camera_id, product_type, ) return None return await http_post(blink, url, json=False, data=data) async def http_get( blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT ): """ Perform an http get request. :param url: URL to perform get request. :param stream: Stream response? True/FALSE :param json: Return json response? TRUE/False :param is_retry: Is this part of a re-auth attempt? """ _LOGGER.debug("Making GET request to %s", url) return await blink.auth.query( url=url, headers=blink.auth.header, reqtype="get", stream=stream, json_resp=json, is_retry=is_retry, ) async def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TIMEOUT): """ Perform an http post request. :param url: URL to perform post request. :param is_retry: Is this part of a re-auth attempt? :param data: str body for post request :param json: Return json response? TRUE/False """ _LOGGER.debug("Making POST request to %s", url) return await blink.auth.query( url=url, headers=blink.auth.header, reqtype="post", is_retry=is_retry, 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) try: network_id = json_data.get("network_id") command_id = json_data.get("id") except AttributeError: return False if command_id and network_id: for _ in range(0, MAX_RETRY): _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: if status.get("status_code", 0) != 908: return False if status.get("complete"): return True await sleep(COMMAND_POLL_TIME) fronzbot-blinkpy-098d43b/blinkpy/auth.py000066400000000000000000000220251463463503200203540ustar00rootroot00000000000000"""Login handler for blink.""" import logging from aiohttp import ( ClientSession, ClientConnectionError, ContentTypeError, ClientResponse, ) from blinkpy import api from blinkpy.helpers import util from blinkpy.helpers.constants import ( BLINK_URL, APP_BUILD, DEFAULT_USER_AGENT, LOGIN_ENDPOINT, TIMEOUT, ) _LOGGER = logging.getLogger(__name__) class Auth: """Class to handle login communication.""" def __init__( self, login_data=None, no_prompt=False, session=None, agent=DEFAULT_USER_AGENT, app_build=APP_BUILD, ): """ Initialize auth handler. :param login_data: dictionary for login data must contain the following: - username - password :param no_prompt: Should any user input prompts be suppressed? True/FALSE """ if login_data is None: login_data = {} self.data = login_data self.token = login_data.get("token", None) self.host = login_data.get("host", None) self.region_id = login_data.get("region_id", None) self.client_id = login_data.get("client_id", None) self.account_id = login_data.get("account_id", None) self.user_id = login_data.get("user_id", None) self.login_response = None self.is_errored = False self.no_prompt = no_prompt self._agent = agent self._app_build = app_build self.session = session if session else ClientSession() @property def login_attributes(self): """Return a dictionary of login attributes.""" self.data["token"] = self.token self.data["host"] = self.host self.data["region_id"] = self.region_id self.data["client_id"] = self.client_id self.data["account_id"] = self.account_id self.data["user_id"] = self.user_id return self.data @property def header(self): """Return authorization header.""" if self.token is None: return None return { "APP-BUILD": self._app_build, "TOKEN_AUTH": self.token, "User-Agent": self._agent, "Content-Type": "application/json", } def validate_login(self): """Check login information and prompt if not available.""" self.data["username"] = self.data.get("username", None) self.data["password"] = self.data.get("password", None) if not self.no_prompt: self.data = util.prompt_login_data(self.data) self.data = util.validate_login_data(self.data) async def login(self, login_url=LOGIN_ENDPOINT): """Attempt login to blink servers.""" self.validate_login() _LOGGER.info("Attempting login with %s", login_url) response = await api.request_login( self, login_url, self.data, is_retry=False, ) try: if response.status == 200: return await response.json() raise LoginError except AttributeError as error: raise LoginError from error def logout(self, blink): """Log out.""" return api.request_logout(blink) async def refresh_token(self): """Refresh auth token.""" self.is_errored = True try: _LOGGER.info("Token expired, attempting automatic refresh.") self.login_response = await self.login() self.extract_login_info() self.is_errored = False except LoginError as error: _LOGGER.error("Login endpoint failed. Try again later.") raise TokenRefreshFailed from error except (TypeError, KeyError) as error: _LOGGER.error("Malformed login response: %s", self.login_response) raise TokenRefreshFailed from error return True def extract_login_info(self): """Extract login info from login response.""" self.region_id = self.login_response["account"]["tier"] self.host = f"{self.region_id}.{BLINK_URL}" self.token = self.login_response["auth"]["token"] self.client_id = self.login_response["account"]["client_id"] self.account_id = self.login_response["account"]["account_id"] self.user_id = self.login_response["account"].get("user_id", None) async def startup(self): """Initialize tokens for communication.""" self.validate_login() if None in self.login_attributes.values(): await self.refresh_token() async def validate_response(self, response: ClientResponse, json_resp): """Check for valid response.""" if not json_resp: self.is_errored = False return response self.is_errored = True try: if response.status in [101, 401]: raise UnauthorizedError if response.status == 404: raise ClientConnectionError json_data = await response.json() except (AttributeError, ValueError) as error: raise BlinkBadResponse from error except ContentTypeError as error: _LOGGER.warning("Got text for JSON response: %s", await response.text()) raise BlinkBadResponse from error self.is_errored = False return json_data async def query( self, url=None, data=None, headers=None, reqtype="get", stream=False, json_resp=True, is_retry=False, timeout=TIMEOUT, ): """Perform server requests. :param url: URL to perform request :param data: Data to send :param headers: Headers to send :param reqtype: Can be 'get' or 'post' (default: 'get') :param stream: Stream response? True/FALSE :param json_resp: Return JSON response? TRUE/False :param is_retry: Is this part of a re-auth attempt? True/FALSE """ try: if reqtype == "get": response = await self.session.get( url=url, data=data, headers=headers, timeout=timeout ) else: response = await self.session.post( url=url, data=data, headers=headers, timeout=timeout ) return await self.validate_response(response, json_resp) except (ClientConnectionError, TimeoutError) as er: _LOGGER.error( "Connection error. Endpoint %s possibly down or throttled. Error: %s", url, er, ) except BlinkBadResponse: code = None reason = None try: code = response.status reason = response.reason except AttributeError: pass _LOGGER.error( "Expected json response from %s, but received: %s: %s", url, code, reason, ) except UnauthorizedError: try: if not is_retry: await self.refresh_token() return await self.query( url=url, data=data, headers=self.header, reqtype=reqtype, stream=stream, json_resp=json_resp, is_retry=True, timeout=timeout, ) _LOGGER.error("Unable to access %s after token refresh.", url) except TokenRefreshFailed: _LOGGER.error("Unable to refresh token.") return None async def send_auth_key(self, blink, key): """Send 2FA key to blink servers.""" if key is not None: response = await api.request_verify(self, blink, key) try: json_resp = await response.json() blink.available = json_resp["valid"] if not blink.available: _LOGGER.error("%s", json_resp["message"]) return False except (KeyError, TypeError, ContentTypeError) as er: _LOGGER.error( "Did not receive valid response from server. Error: %s", er, ) return False return True def check_key_required(self): """Check if 2FA key is required.""" try: if self.login_response["account"]["client_verification_required"]: return True except (KeyError, TypeError): pass return False class TokenRefreshFailed(Exception): """Class to throw failed refresh exception.""" class LoginError(Exception): """Class to throw failed login exception.""" class BlinkBadResponse(Exception): """Class to throw bad json response exception.""" class UnauthorizedError(Exception): """Class to throw an unauthorized access error.""" fronzbot-blinkpy-098d43b/blinkpy/blinkpy.py000066400000000000000000000406731463463503200210740ustar00rootroot00000000000000""" blinkpy is an unofficial api for the Blink security camera system. repo url: https://github.com/fronzbot/blinkpy Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol Published under the MIT license - See LICENSE file for more details. "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. blinkpy is in no way affiliated with Blink, nor Immedia Inc. """ import os.path import time import logging import datetime import aiofiles import aiofiles.ospath from requests.structures import CaseInsensitiveDict from dateutil.parser import parse from slugify import slugify from blinkpy import api from blinkpy.sync_module import BlinkSyncModule, BlinkOwl, BlinkLotus from blinkpy.helpers import util from blinkpy.helpers.constants import ( DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME, TIMEOUT_MEDIA, ) from blinkpy.helpers.constants import __version__ from blinkpy.auth import Auth, TokenRefreshFailed, LoginError _LOGGER = logging.getLogger(__name__) class Blink: """Class to initialize communication.""" def __init__( self, refresh_rate=DEFAULT_REFRESH, motion_interval=DEFAULT_MOTION_INTERVAL, no_owls=False, session=None, ): """ Initialize Blink system. :param refresh_rate: Refresh rate of blink information. Defaults to 30 (seconds) :param motion_interval: How far back to register motion in minutes. Defaults to last refresh time. Useful for preventing motion_detected property from de-asserting too quickly. :param no_owls: Disable searching for owl entries (blink mini cameras \ only known entity). Prevents an unnecessary API call \ if you don't have these in your network. """ self.auth = Auth(session=session) self.account_id = None self.client_id = None self.network_ids = [] self.urls = None self.sync = CaseInsensitiveDict({}) self.last_refresh = None self.refresh_rate = refresh_rate self.networks = [] self.cameras = CaseInsensitiveDict({}) self.video_list = CaseInsensitiveDict({}) self.motion_interval = motion_interval self.version = __version__ self.available = False self.key_required = False self.homescreen = {} self.no_owls = no_owls @util.Throttle(seconds=MIN_THROTTLE_TIME) async def refresh(self, force=False, force_cache=False): """ Perform a system refresh. :param force: Used to override throttle, resets refresh :param force_cache: Used to force update without overriding throttle """ if force or force_cache or self.check_if_ok_to_update(): if not self.available: await self.setup_post_verify() await self.get_homescreen() for sync_name, sync_module in self.sync.items(): _LOGGER.debug("Attempting refresh of blink.sync['%s']", sync_name) await 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()) last_refresh = datetime.datetime.fromtimestamp(self.last_refresh) _LOGGER.debug("last_refresh = %s", last_refresh) return True return False async def start(self): """Perform full system setup.""" try: await self.auth.startup() self.setup_login_ids() self.setup_urls() await self.get_homescreen() except (LoginError, TokenRefreshFailed, BlinkSetupError): _LOGGER.error("Cannot setup Blink platform.") self.available = False return False self.key_required = self.auth.check_key_required() if self.key_required: if self.auth.no_prompt: return True await self.setup_prompt_2fa() if not self.last_refresh: # Initialize last_refresh to be just before the refresh delay period. self.last_refresh = int(time.time() - self.refresh_rate * 1.05) _LOGGER.debug( "Initialized last_refresh to %s == %s", self.last_refresh, datetime.datetime.fromtimestamp(self.last_refresh), ) return await self.setup_post_verify() async def setup_prompt_2fa(self): """Prompt for 2FA.""" email = self.auth.data["username"] pin = input(f"Enter code sent to {email}: ") result = await self.auth.send_auth_key(self, pin) self.key_required = not result async def setup_post_verify(self): """Initialize blink system after verification.""" try: if not self.homescreen: await self.get_homescreen() await self.setup_networks() networks = self.setup_network_ids() cameras = await self.setup_camera_list() except BlinkSetupError: self.available = False return False for name, network_id in networks.items(): sync_cameras = cameras.get(network_id, {}) await self.setup_sync_module(name, network_id, sync_cameras) self.cameras = self.merge_cameras() self.available = True self.key_required = False return True async def setup_sync_module(self, name, network_id, cameras): """Initialize a sync module.""" self.sync[name] = BlinkSyncModule(self, name, network_id, cameras) await self.sync[name].start() async def get_homescreen(self): """Get homescreen information.""" if self.no_owls: _LOGGER.debug("Skipping owl extraction.") self.homescreen = {} return self.homescreen = await api.request_homescreen(self) _LOGGER.debug("homescreen = %s", util.json_dumps(self.homescreen)) async def setup_owls(self): """Check for mini cameras.""" network_list = [] camera_list = [] try: for owl in self.homescreen["owls"]: name = owl["name"] network_id = str(owl["network_id"]) if network_id in self.network_ids: camera_list.append( {network_id: {"name": name, "id": network_id, "type": "mini"}} ) continue if owl["onboarded"]: network_list.append(str(network_id)) self.sync[name] = BlinkOwl(self, name, network_id, owl) await self.sync[name].start() except (KeyError, TypeError): # No sync-less devices found pass self.network_ids.extend(network_list) return camera_list async def setup_lotus(self): """Check for doorbells cameras.""" network_list = [] camera_list = [] try: for lotus in self.homescreen["doorbells"]: name = lotus["name"] network_id = str(lotus["network_id"]) if network_id in self.network_ids: camera_list.append( { network_id: { "name": name, "id": network_id, "type": "doorbell", } } ) continue if lotus["onboarded"]: network_list.append(str(network_id)) self.sync[name] = BlinkLotus(self, name, network_id, lotus) await self.sync[name].start() except (KeyError, TypeError): # No sync-less devices found pass self.network_ids.extend(network_list) return camera_list async def setup_camera_list(self): """Create camera list for onboarded networks.""" all_cameras = {} response = await api.request_camera_usage(self) try: for network in response["networks"]: _LOGGER.info("network = %s", util.json_dumps(network)) camera_network = str(network["network_id"]) if camera_network not in all_cameras: all_cameras[camera_network] = [] for camera in network["cameras"]: all_cameras[camera_network].append( {"name": camera["name"], "id": camera["id"], "type": "default"} ) mini_cameras = await self.setup_owls() lotus_cameras = await self.setup_lotus() for camera in mini_cameras: for network, camera_info in camera.items(): all_cameras[network].append(camera_info) for camera in lotus_cameras: for network, camera_info in camera.items(): all_cameras[network].append(camera_info) return all_cameras except (KeyError, TypeError) as ex: _LOGGER.error("Unable to retrieve cameras from response %s", response) raise BlinkSetupError from ex def setup_login_ids(self): """Retrieve login id numbers from login response.""" self.client_id = self.auth.client_id self.account_id = self.auth.account_id def setup_urls(self): """Create urls for api.""" try: self.urls = util.BlinkURLHandler(self.auth.region_id) except TypeError as ex: _LOGGER.error( "Unable to extract region is from response %s", self.auth.login_response ) raise BlinkSetupError from ex async def setup_networks(self): """Get network information.""" response = await api.request_networks(self) try: self.networks = response["summary"] except (KeyError, TypeError) as ex: raise BlinkSetupError from ex def setup_network_ids(self): """Create the network ids for onboarded networks.""" all_networks = [] network_dict = {} try: for network, status in self.networks.items(): if status["onboarded"]: all_networks.append(f"{network}") network_dict[status["name"]] = network except AttributeError as ex: _LOGGER.error( "Unable to retrieve network information from %s", self.networks ) raise BlinkSetupError from ex self.network_ids = all_networks return network_dict def check_if_ok_to_update(self): """Check if it is ok to perform an http request.""" current_time = int(time.time()) last_refresh = self.last_refresh if last_refresh is None: last_refresh = 0 if current_time >= (last_refresh + self.refresh_rate): return True return False def merge_cameras(self): """Merge all sync camera dicts into one.""" combined = CaseInsensitiveDict({}) for sync in self.sync: combined = util.merge_dicts(combined, self.sync[sync].cameras) return combined async def save(self, file_name): """Save login data to file.""" await util.json_save(self.auth.login_attributes, file_name) async def get_status(self): """Get the blink system notification status.""" response = await api.request_notification_flags(self) return response.get("notifications", response) async def set_status(self, data_dict={}): """ Set the blink system notification status. :param data_dict: Dictionary of notification keys to modify. Example: {'low_battery': False, 'motion': False} """ response = await api.request_set_notification_flag(self, data_dict) return response async def download_videos( self, path, since=None, camera="all", stop=10, delay=1, debug=False ): """ Download all videos from server since specified time. :param path: Path to write files. /path/_.mp4 :param since: Date and time to get videos from. Ex: "2018/07/28 12:33:00" to retrieve videos since July 28th 2018 at 12:33:00 :param camera: Camera name to retrieve. Defaults to "all". Use a list for multiple cameras. :param stop: Page to stop on (~25 items per page. Default page 10). :param delay: Number of seconds to wait in between subsequent video downloads. :param debug: Set to TRUE to prevent downloading of items. Instead of downloading, entries will be printed to log. """ if not isinstance(camera, list): camera = [camera] results = await self.get_videos_metadata(since=since, stop=stop) await self._parse_downloaded_items(results, camera, path, delay, debug) async def get_videos_metadata(self, since=None, camera="all", stop=10): """ Fetch and return video metadata. :param since: Date and time to get videos from. Ex: "2018/07/28 12:33:00" to retrieve videos since July 28th 2018 at 12:33:00 :param stop: Page to stop on (~25 items per page. Default page 10). """ videos = [] if since is None: since_epochs = self.last_refresh else: parsed_datetime = parse(since, fuzzy=True) since_epochs = parsed_datetime.timestamp() formatted_date = util.get_time(time_to_convert=since_epochs) _LOGGER.info("Retrieving videos since %s", formatted_date) for page in range(1, stop): response = await api.request_videos(self, time=since_epochs, page=page) _LOGGER.debug("Processing page %s", page) try: result = response["media"] if not result: raise KeyError videos.extend(result) except (KeyError, TypeError): _LOGGER.info("No videos found on page %s. Exiting.", page) break return videos async def do_http_get(self, address): """ Do an http_get on address. :param address: address to be added to base_url. """ response = await api.http_get( self, url=f"{self.urls.base_url}{address}", stream=True, json=False, timeout=TIMEOUT_MEDIA, ) return response async def _parse_downloaded_items(self, result, camera, path, delay, debug): """Parse downloaded videos.""" for item in result: try: created_at = item["created_at"] camera_name = item["device_name"] is_deleted = item["deleted"] address = item["media"] except KeyError: _LOGGER.info("Missing clip information, skipping...") continue if camera_name not in camera and "all" not in camera: _LOGGER.debug("Skipping videos for %s.", camera_name) continue if is_deleted: _LOGGER.debug("%s: %s is marked as deleted.", camera_name, address) continue filename = f"{camera_name}-{created_at}" filename = f"{slugify(filename)}.mp4" filename = os.path.join(path, filename) if not debug: if await aiofiles.ospath.isfile(filename): _LOGGER.info("%s already exists, skipping...", filename) continue response = await self.do_http_get(address) async with aiofiles.open(filename, "wb") as vidfile: await vidfile.write(await response.read()) _LOGGER.info("Downloaded video to %s", filename) else: print( f"Camera: {camera_name}, Timestamp: {created_at}, " f"Address: {address}, Filename: {filename}" ) if delay > 0: time.sleep(delay) class BlinkSetupError(Exception): """Class to handle setup errors.""" fronzbot-blinkpy-098d43b/blinkpy/camera.py000066400000000000000000000544161463463503200206540ustar00rootroot00000000000000"""Defines Blink cameras.""" import copy import string import os import logging import datetime from json import dumps import traceback import aiohttp from aiofiles import open from requests.compat import urljoin from blinkpy import api from blinkpy.helpers.constants import TIMEOUT_MEDIA from blinkpy.helpers.util import to_alphanumeric _LOGGER = logging.getLogger(__name__) class BlinkCamera: """Class to initialize individual camera.""" def __init__(self, sync): """Initialize BlinkCamera.""" self.sync = sync self.name = None self.camera_id = None self.network_id = None self.thumbnail = None self.serial = None self._version = None self.motion_enabled = None self.battery_level = None self._battery_voltage = None self.clip = None # A clip remains in the recent clips list until is has # been downloaded or has been expired. self.recent_clips = [] self.temperature = None self.temperature_calibrated = None self.battery_state = None self.motion_detected = None self.wifi_strength = None self.last_record = None self._cached_image = None self._cached_video = None self.camera_type = "" self.product_type = None self.sync_signal_strength = None @property def attributes(self): """Return dictionary of all camera attributes.""" attributes = { "name": self.name, "camera_id": self.camera_id, "serial": self.serial, "version": self._version, "temperature": self.temperature, "temperature_c": self.temperature_c, "temperature_calibrated": self.temperature_calibrated, "battery": self.battery, "battery_level": self.battery_level, "battery_voltage": self._battery_voltage, "thumbnail": self.thumbnail, "video": self.clip, "recent_clips": self.recent_clips, "motion_enabled": self.motion_enabled, "motion_detected": self.motion_detected, "wifi_strength": self.wifi_strength, "network_id": self.sync.network_id, "sync_module": self.sync.name, "sync_signal_strength": self.sync_signal_strength, "last_record": self.last_record, "type": self.product_type, } return attributes @property def battery(self): """Return battery as string.""" return self.battery_state @property def battery_voltage(self): """Return battery voltage as a number in 100ths of volts, so 165 = 1.65v.""" return self._battery_voltage @property def temperature_c(self): """Return temperature in celsius.""" try: return round((self.temperature - 32) / 9.0 * 5.0, 1) except TypeError: return None @property def image_from_cache(self): """Return the most recently cached image.""" if self._cached_image: return self._cached_image return None @property def video_from_cache(self): """Return the most recently cached video.""" if self._cached_video: return self._cached_video return None @property def version(self): """Return the camera Firmware version.""" return self._version @property def arm(self): """Return arm status of camera.""" return self.motion_enabled async def async_arm(self, value): """Set camera arm status.""" if value: return await api.request_motion_detection_enable( self.sync.blink, self.network_id, self.camera_id ) return await api.request_motion_detection_disable( self.sync.blink, self.network_id, self.camera_id ) @property async def night_vision(self): """Return night_vision 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 if self.product_type == "catalina": res = res.get("camera", [{}])[0] if res["illuminator_enable"] in [0, 1, 2]: res["illuminator_enable"] = ["off", "on", "auto"][ res.get("illuminator_enable") ] nv_keys = [ "night_vision_control", "illuminator_enable", "illuminator_enable_v2", ] return {key: res.get(key) for key in nv_keys} async def async_set_night_vision(self, value): """Set camera night_vision status.""" if value not in ["on", "off", "auto"]: return None if self.product_type == "catalina": value = {"off": 0, "on": 1, "auto": 2}.get(value, None) data = dumps({"illuminator_enable": value}) res = await api.request_update_config( self.sync.blink, self.network_id, self.camera_id, product_type=self.product_type, data=data, ) if res and res.status == 200: return await res.json() return None async def record(self): """Initiate clip recording.""" return await api.request_new_video( self.sync.blink, self.network_id, self.camera_id ) async def get_media(self, media_type="image") -> aiohttp.ClientRequest: """Download media (image or video).""" if media_type.lower() == "video": return await self.get_video_clip() return await self.get_thumbnail() async def get_thumbnail(self, url=None): """Download thumbnail image.""" if not url: url = self.thumbnail if not url: _LOGGER.warning("Thumbnail URL not available: self.thumbnail=%s", url) return None return await api.http_get( self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA, ) async def get_video_clip(self, url=None): """Download video clip.""" if not url: url = self.clip if not url: _LOGGER.warning("Video clip URL not available: self.clip=%s", url) return None return await api.http_get( self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA, ) async def snap_picture(self): """Take a picture with camera to create a new thumbnail.""" ret_val = await api.request_new_image( self.sync.blink, self.network_id, self.camera_id ) response = await self.get_media() if response and response.status == 200: self._cached_image = await response.read() return ret_val async def set_motion_detect(self, enable): """Set motion detection.""" _LOGGER.warning( "Method is deprecated as of v0.16.0 and will be removed in " "a future version. Please use the BlinkCamera.arm property instead." ) if enable: return await api.request_motion_detection_enable( self.sync.blink, self.network_id, self.camera_id ) return await api.request_motion_detection_disable( self.sync.blink, self.network_id, self.camera_id ) async def update(self, config, force_cache=False, expire_clips=True, **kwargs): """Update camera info.""" if bool(config): self.extract_config_info(config) await self.get_sensor_info() await self.update_images( config, force_cache=force_cache, expire_clips=expire_clips ) def extract_config_info(self, config): """Extract info from config.""" self.name = config.get("name", "unknown") self.camera_id = str(config.get("id", "unknown")) self.network_id = str(config.get("network_id", "unknown")) self.serial = config.get("serial") self._version = config.get("fw_version") self.motion_enabled = config.get("enabled", "unknown") self._battery_voltage = config.get("battery_voltage", None) self.battery_state = config.get("battery_state") or config.get("battery") self.wifi_strength = config.get("wifi_strength") if signals := config.get("signals"): self.battery_level = signals.get("battery") self.sync_signal_strength = signals.get("lfr") self.temperature = signals.get("temp") else: self.temperature = config.get("temperature") self.product_type = config.get("type") async def get_sensor_info(self): """Retrieve calibrated temperature from special endpoint.""" resp = await api.request_camera_sensors( self.sync.blink, self.network_id, self.camera_id ) try: self.temperature_calibrated = resp["temp"] except (TypeError, KeyError): self.temperature_calibrated = self.temperature _LOGGER.warning( "Could not retrieve calibrated temperature response %s.", resp ) _LOGGER.warning( "for network_id (%s) and camera_id (%s)", self.network_id, self.camera_id, ) async def update_images(self, config, force_cache=False, expire_clips=True): """Update images for camera.""" new_thumbnail = None thumb_addr = None thumb_string = None if config.get("thumbnail", False): thumb_addr = config["thumbnail"] try: # API update only returns the timestamp! int(thumb_addr) thumb_string = ( "/api/v3/media/accounts/" f"{self.sync.blink.account_id}/networks/" f"{self.network_id}/{self.product_type}/" f"{self.camera_id}/thumbnail/" f"thumbnail.jpg?ts={thumb_addr}&ext=" ) except ValueError: # This is the old API and has the full url thumb_string = f"{thumb_addr}.jpg" # Check that new full api url has not been returned: if thumb_addr.endswith("&ext="): thumb_string = thumb_addr if thumb_string is not None: new_thumbnail = urljoin(self.sync.urls.base_url, thumb_string) else: _LOGGER.warning("Could not find thumbnail for camera %s.", self.name) try: self.motion_detected = self.sync.motion[self.name] except KeyError: self.motion_detected = False clip_addr = None try: def timesort(record): rec_time = record["time"] iso_time = datetime.datetime.fromisoformat(rec_time) stamp = int(iso_time.timestamp()) return stamp if ( len(self.sync.last_records) > 0 and len(self.sync.last_records[self.name]) > 0 ): last_records = sorted(self.sync.last_records[self.name], key=timesort) for rec in last_records: clip_addr = rec["clip"] self.clip = f"{self.sync.urls.base_url}{clip_addr}" self.last_record = rec["time"] if self.motion_detected: recent = {"time": self.last_record, "clip": self.clip} # Prevent duplicates. if recent not in self.recent_clips: self.recent_clips.append(recent) if len(self.recent_clips) > 0: _LOGGER.debug( "Found %s recent clips for %s", len(self.recent_clips), self.name, ) _LOGGER.debug( "Most recent clip for %s was created at %s : %s", self.name, self.last_record, self.clip, ) except (KeyError, IndexError): ex = traceback.format_exc() trace = "".join(traceback.format_stack()) _LOGGER.error("Error getting last records for '%s': %s", self.name, ex) _LOGGER.debug("\n%s", trace) # If the thumbnail or clip have changed, update the cache update_cached_image = False if new_thumbnail != self.thumbnail or self._cached_image is None: update_cached_image = True self.thumbnail = new_thumbnail update_cached_video = False if self._cached_video is None or self.motion_detected: update_cached_video = True if new_thumbnail is not None and (update_cached_image or force_cache): response = await self.get_media() if response and response.status == 200: self._cached_image = await response.read() if clip_addr is not None and (update_cached_video or force_cache): response = await self.get_media(media_type="video") if response and response.status == 200: self._cached_video = await response.read() # Don't let the recent clips list grow without bound. if expire_clips: await self.expire_recent_clips() async def expire_recent_clips(self, delta=datetime.timedelta(hours=1)): """Remove recent clips from list when they get too old.""" to_keep = [] for clip in self.recent_clips: timedelta = (datetime.datetime.now() - delta).timestamp() clip_time = datetime.datetime.fromisoformat(clip["time"]).timestamp() if clip_time > timedelta: to_keep.append(clip) num_expired = len(self.recent_clips) - len(to_keep) if num_expired > 0: _LOGGER.info("Expired %s clips from '%s'", num_expired, self.name) self.recent_clips = copy.deepcopy(to_keep) if len(self.recent_clips) > 0: _LOGGER.info( "'%s' has %s clips available for download", self.name, len(self.recent_clips), ) for clip in self.recent_clips: url = clip["clip"] if "local_storage" in url: await api.http_post(self.sync.blink, url) async def get_liveview(self): """Get liveview rtsps link.""" response = await api.request_camera_liveview( self.sync.blink, self.sync.network_id, self.camera_id ) return response["server"] async def image_to_file(self, path): """ Write image to file. :param path: Path to write file """ _LOGGER.debug("Writing image from %s to %s", self.name, path) response = await self.get_media() if response and response.status == 200: async with open(path, "wb") as imagefile: await imagefile.write(await response.read()) else: _LOGGER.error("Cannot write image to file, response %s", response.status) async def video_to_file(self, path): """ Write video to file. :param path: Path to write file """ _LOGGER.debug("Writing video from %s to %s", self.name, path) response = await self.get_media(media_type="video") if response is None: _LOGGER.error("No saved video exists for %s.", self.name) return async with open(path, "wb") as vidfile: await vidfile.write(await response.read()) async def save_recent_clips( self, output_dir="/tmp", file_pattern="${created}_${name}.mp4" ): """Save all recent clips using timestamp file name pattern.""" if output_dir[-1] != "/": output_dir += "/" recent = copy.deepcopy(self.recent_clips) num_saved = 0 for clip in recent: clip_time = datetime.datetime.fromisoformat(clip["time"]) clip_time_local = clip_time.replace( tzinfo=datetime.timezone.utc ).astimezone(tz=None) created_at = clip_time_local.strftime("%Y%m%d_%H%M%S") clip_addr = clip["clip"] file_name = string.Template(file_pattern).substitute( created=created_at, name=to_alphanumeric(self.name) ) path = os.path.join(output_dir, file_name) _LOGGER.debug("Saving %s to %s", clip_addr, path) media = await self.get_video_clip(clip_addr) if media and media.status == 200: async with open(path, "wb") as clip_file: await clip_file.write(await media.read()) num_saved += 1 try: # Remove recent clip from the list once the download has finished. self.recent_clips.remove(clip) _LOGGER.debug("Removed %s from recent clips", clip) except ValueError: ex = traceback.format_exc() _LOGGER.error("Error removing clip from list: %s", ex) trace = "".join(traceback.format_stack()) _LOGGER.debug("\n%s", trace) if len(recent) == 0: _LOGGER.info("No recent clips to save for '%s'.", self.name) else: _LOGGER.info( "Saved %s of %s recent clips from '%s' to directory %s", num_saved, len(recent), self.name, output_dir, ) class BlinkCameraMini(BlinkCamera): """Define a class for a Blink Mini camera.""" def __init__(self, sync): """Initialize a Blink Mini cameras.""" super().__init__(sync) self.camera_type = "mini" @property def arm(self): """Return camera arm status.""" return self.sync.arm async def async_arm(self, value): """Set camera arm status.""" url = ( f"{self.sync.urls.base_url}/api/v1/accounts/" f"{self.sync.blink.account_id}/networks/" f"{self.network_id}/owls/{self.camera_id}/config" ) data = dumps({"enabled": value}) response = await api.http_post(self.sync.blink, url, data=data) await api.wait_for_command(self.sync.blink, response) return response async def record(self): """Initiate clip recording for a blink mini camera.""" url = ( f"{self.sync.urls.base_url}/api/v1/accounts/" f"{self.sync.blink.account_id}/networks/" f"{self.network_id}/owls/{self.camera_id}/clip" ) 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 mini camera.""" url = ( f"{self.sync.urls.base_url}/api/v1/accounts/" f"{self.sync.blink.account_id}/networks/" f"{self.network_id}/owls/{self.camera_id}/thumbnail" ) 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.""" 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.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" link = ":".join(server_split) return link class BlinkDoorbell(BlinkCamera): """Define a class for a Blink Doorbell camera.""" def __init__(self, sync): """Initialize a Blink Doorbell.""" super().__init__(sync) self.camera_type = "doorbell" @property def arm(self): """Return camera arm status.""" return self.motion_enabled async def async_arm(self, value): """Set camera arm status.""" 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}" ) if value: url = f"{url}/enable" else: url = f"{url}/disable" response = await api.http_post(self.sync.blink, url) await api.wait_for_command(self.sync.blink, response) return response async def record(self): """Initiate clip recording for a blink doorbell camera.""" 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}/clip" ) 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.""" 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}/thumbnail" ) 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.""" 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 fronzbot-blinkpy-098d43b/blinkpy/helpers/000077500000000000000000000000001463463503200205025ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/blinkpy/helpers/__init__.py000066400000000000000000000000571463463503200226150ustar00rootroot00000000000000"""Init file for blinkpy helper functions.""" fronzbot-blinkpy-098d43b/blinkpy/helpers/constants.py000066400000000000000000000012261463463503200230710ustar00rootroot00000000000000"""Generates constants for use in blinkpy.""" import importlib.metadata __version__ = importlib.metadata.version("blinkpy") """ URLS """ BLINK_URL = "immedia-semi.com" DEFAULT_URL = f"rest-prod.{BLINK_URL}" BASE_URL = f"https://{DEFAULT_URL}" LOGIN_ENDPOINT = f"{BASE_URL}/api/v5/account/login" """ Dictionaries """ ONLINE = {"online": True, "offline": False} """ OTHER """ APP_BUILD = "ANDROID_28373244" DEFAULT_USER_AGENT = "27.0ANDROID_28373244" DEVICE_ID = "Blinkpy" TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z" DEFAULT_MOTION_INTERVAL = 1 DEFAULT_REFRESH = 30 MIN_THROTTLE_TIME = 2 SIZE_NOTIFICATION_KEY = 152 SIZE_UID = 16 TIMEOUT = 10 TIMEOUT_MEDIA = 90 fronzbot-blinkpy-098d43b/blinkpy/helpers/errors.py000066400000000000000000000006701463463503200223730ustar00rootroot00000000000000"""Module to define error types.""" USERNAME = (0, "Username must be a string") PASSWORD = (1, "Password must be a string") AUTHENTICATE = ( 2, "Cannot authenticate since either password or username has not been set", ) AUTH_TOKEN = ( 3, "Authentication header incorrect. Are you sure you received your token?", ) REQUEST = (4, "Cannot perform request (get/post type incorrect)") BLINK_ERRORS = [400, 404] fronzbot-blinkpy-098d43b/blinkpy/helpers/util.py000066400000000000000000000122351463463503200220340ustar00rootroot00000000000000"""Useful functions for blinkpy.""" import json import random import logging import time import secrets import re from asyncio import sleep from calendar import timegm from functools import wraps from getpass import getpass import aiofiles import dateutil.parser from blinkpy.helpers import constants as const _LOGGER = logging.getLogger(__name__) async def json_load(file_name): """Load json credentials from file.""" try: async with aiofiles.open(file_name, "r") as json_file: test = await json_file.read() data = json.loads(test) return data except FileNotFoundError: _LOGGER.error("Could not find %s", file_name) except json.decoder.JSONDecodeError: _LOGGER.error("File %s has improperly formatted json", file_name) return None async def json_save(data, file_name): """Save data to file location.""" async with aiofiles.open(file_name, "w") as json_file: await json_file.write(json.dumps(data, indent=4)) def json_dumps(json_in, indent=2): """Return a well formated json string.""" return json.dumps(json_in, indent=indent) def gen_uid(size, uid_format=False): """Create a random string.""" if uid_format: token = ( f"BlinkCamera_{secrets.token_hex(4)}-" f"{secrets.token_hex(2)}-{secrets.token_hex(2)}-" f"{secrets.token_hex(2)}-{secrets.token_hex(6)}" ) else: token = secrets.token_hex(size) return token def time_to_seconds(timestamp): """Convert TIMESTAMP_FORMAT time to seconds.""" try: dtime = dateutil.parser.isoparse(timestamp) except ValueError: _LOGGER.error("Incorrect timestamp format for conversion: %s.", timestamp) return False return timegm(dtime.timetuple()) def get_time(time_to_convert=None): """Create blink-compatible timestamp.""" if time_to_convert is None: time_to_convert = time.time() return time.strftime(const.TIMESTAMP_FORMAT, time.gmtime(time_to_convert)) def merge_dicts(dict_a, dict_b): """Merge two dictionaries into one.""" duplicates = [val for val in dict_a if val in dict_b] if duplicates: _LOGGER.warning( ("Duplicates found during merge: %s. " "Renaming is recommended."), duplicates, ) return {**dict_a, **dict_b} def prompt_login_data(data): """Prompt user for username and password.""" if data["username"] is None: data["username"] = input("Username:") if data["password"] is None: data["password"] = getpass("Password:") return data def validate_login_data(data): """Check for missing keys.""" data["uid"] = data.get("uid", gen_uid(const.SIZE_UID, uid_format=True)) data["device_id"] = data.get("device_id", const.DEVICE_ID) return data def local_storage_clip_url_template(): """Return URL template for local storage clip download location.""" return ( "/api/v1/accounts/$account_id/networks/$network_id/sync_modules/$sync_id" "/local_storage/manifest/$manifest_id/clip/request/$clip_id" ) def backoff_seconds(retry=0, default_time=1): """Calculate number of seconds to back off for retry.""" return default_time * 2**retry + random.uniform(0, 1) def to_alphanumeric(name): """Convert name to one with only alphanumeric characters.""" return re.sub(r"\W+", "", name) class BlinkException(Exception): """Class to throw general blink exception.""" def __init__(self, errcode): """Initialize BlinkException.""" super().__init__() self.errid = errcode[0] self.message = errcode[1] class BlinkAuthenticationException(BlinkException): """Class to throw authentication exception.""" class BlinkURLHandler: """Class that handles Blink URLS.""" def __init__(self, region_id): """Initialize the urls.""" if region_id is None: raise TypeError self.subdomain = f"rest-{region_id}" self.base_url = f"https://{self.subdomain}.{const.BLINK_URL}" self.home_url = f"{self.base_url}/homescreen" self.event_url = f"{self.base_url}/events/network" self.network_url = f"{self.base_url}/network" self.networks_url = f"{self.base_url}/networks" self.video_url = f"{self.base_url}/api/v2/videos" _LOGGER.debug("Setting base url to %s.", self.base_url) class Throttle: """Class for throttling api calls.""" def __init__(self, seconds=10): """Initialize throttle class.""" self.throttle_time = seconds self.last_call = 0 def __call__(self, method): """Throttle caller method.""" @wraps(method) async def wrapper(*args, **kwargs): """Wrap that checks for throttling.""" force = kwargs.get("force", False) now = int(time.time()) last_call_delta = now - self.last_call if force or last_call_delta > self.throttle_time: self.last_call = now else: self.last_call = now + last_call_delta await sleep(self.throttle_time - last_call_delta) return await method(*args, **kwargs) return wrapper fronzbot-blinkpy-098d43b/blinkpy/sync_module.py000066400000000000000000000713641463463503200217460ustar00rootroot00000000000000"""Defines a sync module for Blink.""" import logging import string import datetime import traceback import asyncio import aiofiles from sortedcontainers import SortedSet from requests.structures import CaseInsensitiveDict from blinkpy import api from blinkpy.camera import BlinkCamera, BlinkCameraMini, BlinkDoorbell from blinkpy.helpers.util import ( time_to_seconds, backoff_seconds, to_alphanumeric, json_dumps, ) from blinkpy.helpers.constants import ONLINE _LOGGER = logging.getLogger(__name__) class BlinkSyncModule: """Class to initialize sync module.""" def __init__(self, blink, network_name, network_id, camera_list): """ Initialize Blink sync module. :param blink: Blink class instantiation """ self.blink = blink self.network_id = network_id self.region_id = blink.auth.region_id self.name = network_name self.serial = None self._version = None self.status = "offline" self.sync_id = None self.host = None self.summary = None self.network_info = None self.events = [] self.cameras = CaseInsensitiveDict({}) self.motion_interval = blink.motion_interval self.motion = {} # A dictionary where keys are the camera names, and # values are lists of recent clips. self.last_records = {} self.camera_list = camera_list self.available = False # type_key_map is only for the mini's and the doorbells. # Outdoor cameras have their own URL API which must be queried. self.type_key_map = { "mini": "owls", "doorbell": "doorbells", } self._names_table = {} self._local_storage = { "enabled": False, "compatible": False, "status": False, "last_manifest_id": None, "manifest": SortedSet(), "manifest_stale": True, "last_manifest_read": datetime.datetime(1970, 1, 1, 0, 0, 0).isoformat(), } @property def attributes(self): """Return sync attributes.""" attr = { "name": self.name, "id": self.sync_id, "network_id": self.network_id, "serial": self.serial, "version": self._version, "status": self.status, "region_id": self.region_id, "local_storage": self.local_storage, } return attr @property def urls(self): """Return device urls.""" return self.blink.urls @property def online(self): """Return boolean system online status.""" try: return ONLINE[self.status] except KeyError: _LOGGER.error("Unknown sync module status %s", self.status) self.available = False return False @property def version(self): """Return the Syncmodule Firmware version.""" return self._version @property def arm(self): """Return status of sync module: armed/disarmed.""" try: return self.network_info["network"]["armed"] except (KeyError, TypeError): self.available = False return None @property def local_storage(self): """Indicate if local storage is activated or not (True/False).""" return self._local_storage["status"] @property def local_storage_manifest_ready(self): """Indicate if the manifest is up-to-date.""" return not self._local_storage["manifest_stale"] async def async_arm(self, value): """Arm or disarm camera.""" if value: return await api.request_system_arm(self.blink, self.network_id) return await api.request_system_disarm(self.blink, self.network_id) async def start(self): """Initialize the system.""" _LOGGER.debug("Initializing the sync module") response = await self.sync_initialize() if not response: return False try: self.sync_id = self.summary["id"] self.serial = self.summary["serial"] self.status = self.summary["status"] except KeyError: _LOGGER.error("Could not extract some sync module info: %s", response) is_ok = await self.get_network_info() if not is_ok or not await self.update_cameras(): self.available = False return False self.available = True return True async def sync_initialize(self): """Initialize a sync module.""" # Doesn't include local store info for some reason. response = await api.request_syncmodule(self.blink, self.network_id) try: self.summary = response["syncmodule"] self.network_id = self.summary["network_id"] await self._init_local_storage(self.summary["id"]) except (TypeError, KeyError): _LOGGER.error( "Could not retrieve sync module information with response: %s", response ) return False self._version = self.summary.get("fw_version") return response async def _init_local_storage(self, sync_id): """Initialize local storage from homescreen dictionary.""" home_screen = self.blink.homescreen sync_module = None try: sync_modules = home_screen["sync_modules"] for mod in sync_modules: if mod["id"] == sync_id: self._local_storage["enabled"] = mod["local_storage_enabled"] self._local_storage["compatible"] = mod["local_storage_compatible"] self._local_storage["status"] = ( mod["local_storage_status"] == "active" ) self._local_storage["last_manifest_read"] = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=10) ).isoformat() sync_module = mod except (TypeError, KeyError): _LOGGER.error( "Could not retrieve sync module information from home screen: %s", home_screen, ) return False return sync_module async def update_cameras(self, camera_type=BlinkCamera): """Update cameras from server.""" type_map = { "mini": BlinkCameraMini, "doorbell": BlinkDoorbell, "default": BlinkCamera, } try: _LOGGER.debug("Updating cameras") for camera_config in self.camera_list: _LOGGER.debug("Updating camera_config %s", json_dumps(camera_config)) if "name" not in camera_config: break blink_camera_type = camera_config.get("type", "") name = camera_config["name"] self.motion[name] = False unique_info = self.get_unique_info(name) if blink_camera_type in type_map: camera_type = type_map[blink_camera_type] self.cameras[name] = camera_type(self) camera_info = await self.get_camera_info( camera_config["id"], unique_info=unique_info ) self._names_table[to_alphanumeric(name)] = name await self.cameras[name].update( camera_info, force_cache=True, force=True ) except KeyError: _LOGGER.error("Could not create camera instances for %s", self.name) return False return True def get_unique_info(self, name): """Extract unique information for Minis and Doorbells.""" try: for type_key in self.type_key_map.values(): for device in self.blink.homescreen[type_key]: _LOGGER.debug("checking device %s", device) if device["name"] == name: _LOGGER.debug("Found unique_info %s", device) return device except (TypeError, KeyError): pass return None async def get_events(self, **kwargs): """Retrieve events from server.""" force = kwargs.pop("force", False) response = await api.request_sync_events( self.blink, self.network_id, force=force ) try: return response["event"] except (TypeError, KeyError): _LOGGER.error("Could not extract events: %s", response) return False async def get_camera_info(self, camera_id, **kwargs): """Retrieve camera information.""" unique = kwargs.get("unique_info", None) if unique is not None: return unique response = await api.request_camera_info(self.blink, self.network_id, camera_id) try: return response["camera"][0] except (TypeError, KeyError): _LOGGER.error( "Could not extract camera info for %s: %s", camera_id, response ) return {} async def get_network_info(self): """Retrieve network status.""" self.network_info = await api.request_network_update( self.blink, self.network_id ) try: if self.network_info["network"]["sync_module_error"]: raise KeyError except (TypeError, KeyError): self.available = False return False return True async def refresh(self, force_cache=False): """Get all blink cameras and pulls their most recent status.""" if not await self.get_network_info(): return await self.update_local_storage_manifest() await self.check_new_videos() for camera_name in self.cameras: camera_id = self.cameras[camera_name].camera_id camera_info = await self.get_camera_info( camera_id, unique_info=self.get_unique_info(camera_name), ) await self.cameras[camera_name].update(camera_info, force_cache=force_cache) self.available = True async def check_new_videos(self): """Check if new videos since last refresh.""" _LOGGER.debug("Checking for new videos") try: interval = self.blink.last_refresh - self.motion_interval * 60 last_refresh = datetime.datetime.fromtimestamp(self.blink.last_refresh) _LOGGER.debug("last_refresh = %s", last_refresh) _LOGGER.debug("interval = %s", interval) except TypeError: # This is the first start, so refresh hasn't happened yet. # No need to check for motion. ex = traceback.format_exc() _LOGGER.error( "Error calculating interval (last_refresh = %s): %s", self.blink.last_refresh, ex, ) trace = "".join(traceback.format_stack()) _LOGGER.debug("\n%s", trace) _LOGGER.info("No new videos since last refresh.") return False resp = await api.request_videos(self.blink, time=interval, page=1) last_record = {} for camera in self.cameras: # Initialize the list if doesn't exist yet. if camera not in self.last_records: self.last_records[camera] = [] # Hang on to the last record if there is one. if len(self.last_records[camera]) > 0: last_record[camera] = self.last_records[camera][-1] # Reset in preparation for processing new entries. self.last_records[camera] = [] self.motion[camera] = False try: info = resp["media"] except (KeyError, TypeError): _LOGGER.warning("Could not check for motion. Response: %s", resp) return False for entry in info: try: name = entry["device_name"] clip_url = entry["media"] timestamp = entry["created_at"] if self.check_new_video_time(timestamp): self.motion[name] = True and self.arm record = {"clip": clip_url, "time": timestamp} self.last_records[name].append(record) except KeyError: last_refresh = datetime.datetime.fromtimestamp(self.blink.last_refresh) _LOGGER.debug( "No new videos for %s since last refresh at %s.", entry, last_refresh, ) # Process local storage if active and if the manifest is ready. last_manifest_read = datetime.datetime.fromisoformat( self._local_storage["last_manifest_read"] ) _LOGGER.debug("last_manifest_read = %s", last_manifest_read) _LOGGER.debug("Manifest ready? %s", self.local_storage_manifest_ready) if self.local_storage and self.local_storage_manifest_ready: _LOGGER.debug("Processing updated manifest") manifest = self._local_storage["manifest"] last_manifest_id = self._local_storage["last_manifest_id"] last_manifest_read = self._local_storage["last_manifest_read"] last_read_local = ( datetime.datetime.fromisoformat(last_manifest_read) .replace(tzinfo=datetime.timezone.utc) .astimezone(tz=None) ) last_clip_time = None num_new = 0 for item in reversed(manifest): iso_timestamp = item.created_at.isoformat() _LOGGER.debug( "Checking '%s': clip_time = %s, manifest_read = %s", item.name, iso_timestamp, last_manifest_read, ) # Exit the loop once there are no new videos in the list. if not self.check_new_video_time(iso_timestamp, last_manifest_read): _LOGGER.info( "No new local storage videos since last manifest " "read at %s.", last_read_local, ) break _LOGGER.debug("Found new item in local storage manifest: %s", item) name = item.name clip_url = item.url(last_manifest_id) await item.prepare_download(self.blink) self.motion[name] = True record = {"clip": clip_url, "time": iso_timestamp} self.last_records[name].append(record) last_clip_time = item.created_at num_new += 1 # The manifest became ready, and we read recent clips from it. if num_new > 0: last_manifest_read = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=10) ).isoformat() self._local_storage["last_manifest_read"] = last_manifest_read _LOGGER.debug("Updated last_manifest_read to %s", last_manifest_read) _LOGGER.debug("Last clip time was %s", last_clip_time) # We want to keep the last record when no new motion was detected. for camera in self.cameras: # Check if there are no new records, indicating motion. if len(self.last_records[camera]) == 0: # If no new records, check if we had a previous last record. if camera in last_record: # Put the last record back into the empty list. self.last_records[camera].append(last_record[camera]) return True def check_new_video_time(self, timestamp, reference=None): """Check if video has timestamp since last refresh. :param timestamp ISO-formatted timestamp string :param reference ISO-formatted reference timestamp string """ if not reference: return time_to_seconds(timestamp) > self.blink.last_refresh return time_to_seconds(timestamp) > time_to_seconds(reference) async def update_local_storage_manifest(self): """Update local storage manifest, which lists all stored clips.""" if not self.local_storage: self._local_storage["manifest_stale"] = True return None _LOGGER.debug("Updating local storage manifest") response = await self.poll_local_storage_manifest() try: manifest_request_id = response["id"] except (TypeError, KeyError): _LOGGER.error( "Could not extract manifest request ID from response: %s", response ) self._local_storage["manifest_stale"] = True return None response = await self.poll_local_storage_manifest(manifest_request_id) try: manifest_id = response["manifest_id"] except (TypeError, KeyError): _LOGGER.error("Could not extract manifest ID from response: %s", response) self._local_storage["manifest_stale"] = True return None self._local_storage["last_manifest_id"] = manifest_id template = string.Template(api.local_storage_clip_url_template()).substitute( account_id=self.blink.account_id, network_id=self.network_id, sync_id=self.sync_id, manifest_id="$manifest_id", clip_id="$clip_id", ) num_stored = len(self._local_storage["manifest"]) try: for item in response["clips"]: alphanumeric_name = item["camera_name"] if alphanumeric_name in self._names_table: camera_name = self._names_table[alphanumeric_name] self._local_storage["manifest"].add( LocalStorageMediaItem( item["id"], camera_name, item["created_at"], item["size"], manifest_id, template, ) ) num_added = len(self._local_storage["manifest"]) - num_stored if num_added > 0: _LOGGER.info( "Found %s new clip(s) in local storage manifest id = %s", num_added, manifest_id, ) except (TypeError, KeyError): ex = traceback.format_exc() _LOGGER.error("Could not extract clips list from response: %s", ex) trace = "".join(traceback.format_stack()) _LOGGER.debug("\n%s", trace) self._local_storage["manifest_stale"] = True return None self._local_storage["manifest_stale"] = False return True async def poll_local_storage_manifest( self, manifest_request_id=None, max_retries=4 ): """Poll for local storage manifest.""" # The sync module may be busy processing another request # (like saving a new clip). # Poll the endpoint until it is ready, backing off each retry. response = None for retry in range(max_retries): # Request building the manifest. if not manifest_request_id: response = await api.request_local_storage_manifest( self.blink, self.network_id, self.sync_id ) if response and "id" in response: break # Get the manifest. else: response = await api.get_local_storage_manifest( self.blink, self.network_id, self.sync_id, manifest_request_id ) if response and "clips" in response: break seconds = backoff_seconds(retry=retry, default_time=3) _LOGGER.debug("[retry=%d] Retrying in %d seconds", retry + 1, seconds) await asyncio.sleep(seconds) return response class BlinkOwl(BlinkSyncModule): """Representation of a sync-less device.""" def __init__(self, blink, name, network_id, response): """Initialize a sync-less object.""" cameras = [{"name": name, "id": response["id"]}] super().__init__(blink, name, network_id, cameras) self.sync_id = response["id"] self.serial = response["serial"] self.status = response["enabled"] if not self.serial: self.serial = f"{network_id}-{self.sync_id}" async def sync_initialize(self): """Initialize a sync-less module.""" self.summary = { "id": self.sync_id, "name": self.name, "serial": self.serial, "status": self.status, "onboarded": True, "account_id": self.blink.account_id, "network_id": self.network_id, } return self.summary async def update_cameras(self, camera_type=BlinkCameraMini): """Update sync-less cameras.""" return await super().update_cameras(camera_type=BlinkCameraMini) async def get_camera_info(self, camera_id, **kwargs): """Retrieve camera information.""" try: for owl in self.blink.homescreen["owls"]: if owl["name"] == self.name: self.status = owl["enabled"] return owl except (TypeError, KeyError): pass return None async def get_network_info(self): """Get network info for sync-less module.""" return True @property def network_info(self): """Format owl response to resemble sync module.""" return { "network": { "id": self.network_id, "name": self.name, "armed": self.status, "sync_module_error": False, "account_id": self.blink.account_id, } } @network_info.setter def network_info(self, value): """Set network_info property.""" class BlinkLotus(BlinkSyncModule): """Representation of a sync-less device.""" def __init__(self, blink, name, network_id, response): """Initialize a sync-less object.""" cameras = [{"name": name, "id": response["id"]}] super().__init__(blink, name, network_id, cameras) self.sync_id = response["id"] self.serial = response["serial"] self.status = response["enabled"] if not self.serial: self.serial = f"{network_id}-{self.sync_id}" async def sync_initialize(self): """Initialize a sync-less module.""" self.summary = { "id": self.sync_id, "name": self.name, "serial": self.serial, "status": self.status, "onboarded": True, "account_id": self.blink.account_id, "network_id": self.network_id, } return self.summary async def update_cameras(self, camera_type=BlinkDoorbell): """Update sync-less cameras.""" return await super().update_cameras(camera_type=BlinkDoorbell) async def get_camera_info(self, camera_id, **kwargs): """Retrieve camera information.""" try: for doorbell in self.blink.homescreen["doorbells"]: if doorbell["name"] == self.name: self.status = doorbell["enabled"] return doorbell except (TypeError, KeyError): pass return None async def get_network_info(self): """Get network info for sync-less module.""" return True @property def network_info(self): """Format lotus response to resemble sync module.""" return { "network": { "id": self.network_id, "name": self.name, "armed": self.status, "sync_module_error": False, "account_id": self.blink.account_id, } } @network_info.setter def network_info(self, value): """Set network_info property.""" class LocalStorageMediaItem: """Metadata of media item in the local storage manifest.""" def __init__( self, item_id, camera_name, created_at, size, manifest_id, url_template ): """Initialize media item. :param item_id: ID of the manifest item. :param camera_name: Name of camera that took the video. :param created_at: ISO-formatted time stamp for creation time. :param size: Size of the video file. """ self._id = int(item_id) self._camera_name = camera_name self._created_at = datetime.datetime.fromisoformat(created_at) self._size = size self._url_template = url_template self._manifest_id = manifest_id def _build_url(self, manifest_id, clip_id): return string.Template(self._url_template).substitute( manifest_id=manifest_id, clip_id=clip_id ) @property def id(self): """Return media item ID.""" return self._id @property def name(self): """Return name of camera that captured this media item.""" return self._camera_name @property def created_at(self): """Return the ISO-formatted creation time stamp of this media item.""" return self._created_at @property def size(self): """Return the reported size of this media item.""" return self._size def url(self, manifest_id=None): """Build the URL. Builds the url new each time since the media item is cached, and the manifest is possibly rebuilt each refresh. :param manifest_id: ID of new manifest (if it changed) :return: URL for clip retrieval """ if manifest_id: self._manifest_id = manifest_id return self._build_url(self._manifest_id, self._id) 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 = 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: """Delete video from sync module.""" delete_url = blink.urls.base_url + self.url() delete_url = delete_url.replace("request", "delete") for retry in range(max_retries): delete = await api.http_post( blink, delete_url, json=False ) # Delete the video if delete.status == 200: return True seconds = backoff_seconds(retry=retry, default_time=3) _LOGGER.debug("[retry=%d] Retrying in %d seconds", retry + 1, seconds) await asyncio.sleep(seconds) return False async def download_video(self, blink, file_name, max_retries=4) -> bool: """Download a previously prepared video from sync module.""" for retry in range(max_retries): url = blink.urls.base_url + self.url() video = await api.http_get(blink, url, json=False) if video.status == 200: async with aiofiles.open(file_name, "wb") as vidfile: await vidfile.write(await video.read()) # download the video return True 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) return False async def download_video_delete(self, blink, file_name, max_retries=4) -> bool: """Delete local videos. Initiate upload of media item from the sync module to Blink cloud servers then download to local filesystem and delete from sync. """ if await self.prepare_download(blink): if await self.download_video(blink, file_name): if await self.delete_video(blink): return True return False def __repr__(self): """Create string representation.""" return ( f"LocalStorageMediaItem(id={self._id}, camera_name={self._camera_name}, " f"created_at={self._created_at}" + f", size={self._size}, manifest_id={self._manifest_id}, " f"url_template={self._url_template})" ) def __str__(self): """Create string representation.""" return self.__repr__() def cmp_key(self): """Return key to use for comparison.""" return self._created_at def __eq__(self, other): """Check equality.""" return self.cmp_key() == other.cmp_key() def __lt__(self, other): """Check less than.""" return self.cmp_key() < other.cmp_key() def __hash__(self): """Return unique hash value.""" return self._id fronzbot-blinkpy-098d43b/blinksync/000077500000000000000000000000001463463503200173645ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/blinksync/blinksync.py000066400000000000000000000074201463463503200217350ustar00rootroot00000000000000import json import asyncio import wx import logging import aiohttp import sys from sortedcontainers import SortedSet from forms import LoginDialog, VideosForm, DELAY, CLOSE, DELETE, DOWNLOAD, REFRESH from blinkpy.blinkpy import Blink, BlinkSyncModule from blinkpy.auth import Auth async def main(): """Main loop for blink test.""" session = aiohttp.ClientSession() blink = Blink(session=session) app = wx.App() try: with wx.DirDialog(None) as dlg: if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() else: sys.exit(0) with open(f"{path}/blink.json", "rt", encoding="ascii") as j: blink.auth = Auth(json.loads(j.read()), session=session) except (StopIteration, FileNotFoundError): with LoginDialog() as userdlg: userdlg.ShowModal() userpass = userdlg.getUserPassword() if userpass is not None: blink.auth = Auth( userpass, session=session, ) await blink.save(f"{path}/blink.json") else: sys.exit(0) with wx.BusyInfo("Blink is Working....") as working: cursor = wx.BusyCursor() if await blink.start(): await blink.setup_post_verify() elif blink.auth.check_key_required(): print("I failed to authenticate") print(f"Sync status: {blink.network_ids}") print(f"Sync :{blink.networks}") if len(blink.networks) == 0: exit() my_sync: BlinkSyncModule = blink.sync[ blink.networks[list(blink.networks)[0]]["name"] ] cursor = None working = None while True: with wx.BusyInfo("Blink is Working....") as working: cursor = wx.BusyCursor() for name, camera in blink.cameras.items(): print(name) print(camera.attributes) my_sync._local_storage["manifest"] = SortedSet() await my_sync.refresh() if my_sync.local_storage and my_sync.local_storage_manifest_ready: print("Manifest is ready") print(f"Manifest {my_sync._local_storage['manifest']}") else: print("Manifest not ready") for name, camera in blink.cameras.items(): print(f"{camera.name} status: {blink.cameras[name].arm}") new_vid = await my_sync.check_new_videos() print(f"New videos?: {new_vid}") manifest = my_sync._local_storage["manifest"] cursor = None working = None frame = VideosForm(manifest) button = frame.ShowModal() with wx.BusyInfo("Blink is Working....") as working: cursor = wx.BusyCursor() if button == CLOSE: break if button == REFRESH: continue # Download and delete all videos from sync module for item in reversed(manifest): if item.id in frame.ItemList: if button == DOWNLOAD: await item.prepare_download(blink) await item.download_video( blink, f"{path}/{item.name}_{item.created_at.astimezone().isoformat().replace(':','_')}.mp4", ) if button == DELETE: await item.delete_video(blink) await asyncio.sleep(DELAY) cursor = None working = None frame = None await session.close() await blink.save(f"{path}/blink.json") # Run the program if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() loop.run_until_complete(main()) fronzbot-blinkpy-098d43b/blinksync/forms.py000066400000000000000000000103601463463503200210640ustar00rootroot00000000000000import wx DELETE = 1 CLOSE = 2 DOWNLOAD = 3 REFRESH = 4 DELAY = 5 class VideosForm(wx.Dialog): """My delete form.""" def __init__(self,manifest): wx.Frame.__init__(self, None, wx.ID_ANY, "Select List to Download and Delete",size = (450,550)) # Add a panel so it looks the correct on all platforms panel = wx.Panel(self, wx.ID_ANY) #self.Bind(wx.EVT,self._when_closed) self.index = 0 self.ItemList = [] self.list_ctrl = wx.ListCtrl(panel, size=(-1,400), style=wx.LC_REPORT |wx.BORDER_SUNKEN ) self.list_ctrl.InsertColumn(0, 'Name') self.list_ctrl.InsertColumn(1, 'Camera') self.list_ctrl.InsertColumn(2, 'Date', width=225) self.list_ctrl.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK,self.download_line) btn = wx.Button(panel, label="Download") btn.Bind(wx.EVT_BUTTON, self.download_line) deletebtn = wx.Button(panel, label="Delete") deletebtn.Bind(wx.EVT_BUTTON, self.delete_line) closeBtn = wx.Button(panel, label="Close") closeBtn.Bind(wx.EVT_BUTTON, self._when_closed) refrestBtn = wx.Button(panel, label="Refresh") refrestBtn.Bind(wx.EVT_BUTTON, self._refresh) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 20) sizer_buttons = wx.BoxSizer(wx.HORIZONTAL) sizer_buttons.Add(btn, 0, wx.ALL|wx.CENTER, 5) sizer_buttons.Add(deletebtn,0,wx.ALL|wx.CENTER,5) sizer_buttons.Add(refrestBtn,0,wx.ALL|wx.CENTER,5) sizer_buttons.Add(closeBtn,0,wx.ALL|wx.CENTER, 5) sizer.Add(sizer_buttons,0,wx.ALL|wx.CENTER,5) panel.SetSizer(sizer) for item in reversed(manifest): self.list_ctrl.InsertItem(self.index, str(item.id)) self.list_ctrl.SetItem(self.index, 1, item.name) self.list_ctrl.SetItem(self.index, 2, item.created_at.astimezone().isoformat()) self.index += 1 #---------------------------------------------------------------------- def download_line(self, event): """Add to list and return DOWNLOAD""" for count in range(self.list_ctrl.ItemCount): if self.list_ctrl.IsSelected(count): self.ItemList.append(int(self.list_ctrl.GetItem(count).Text)) self.EndModal(DOWNLOAD) def delete_line(self, event): """Add to list and return DOWNLOAD""" for count in range(self.list_ctrl.ItemCount): if self.list_ctrl.IsSelected(count): self.ItemList.append(int(self.list_ctrl.GetItem(count).Text)) self.EndModal(DELETE) def _when_closed(self,event): self.EndModal(CLOSE) def _refresh(self,event): self.EndModal(REFRESH) class LoginDialog(wx.Dialog): """ Class to define login dialog """ #---------------------------------------------------------------------- def __init__(self): """Constructor""" wx.Dialog.__init__(self, None, title="Login") # user info user_sizer = wx.BoxSizer(wx.HORIZONTAL) user_lbl = wx.StaticText(self, label="Username:") user_sizer.Add(user_lbl, 0, wx.ALL|wx.CENTER, 5) self.user = wx.TextCtrl(self) user_sizer.Add(self.user, 0, wx.ALL, 5) # pass info p_sizer = wx.BoxSizer(wx.HORIZONTAL) p_lbl = wx.StaticText(self, label="Password:") p_sizer.Add(p_lbl, 0, wx.ALL|wx.CENTER, 5) self.password = wx.TextCtrl(self, style=wx.TE_PASSWORD|wx.TE_PROCESS_ENTER) p_sizer.Add(self.password, 0, wx.ALL, 5) main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(user_sizer, 0, wx.ALL, 5) main_sizer.Add(p_sizer, 0, wx.ALL, 5) btn = wx.Button(self, label="Login") btn.Bind(wx.EVT_BUTTON, self.onLogin) main_sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5) self.SetSizer(main_sizer) #---------------------------------------------------------------------- def onLogin(self, event): """ Check credentials and login """ self.account = {"username":self.user.Value,"password":self.password.Value} self.EndModal(wx.ID_OK) def getUserPassword(self): return self.account fronzbot-blinkpy-098d43b/codecov.yml000066400000000000000000000004431463463503200175360ustar00rootroot00000000000000codecov: branch: dev bot: codecov-io max_report_age: 24 disable_default_path_fixes: no require_ci_to_pass: yes notify: wait_for_ci: yes coverage: precision: 1 round: down range: 85..100 status: project: default: target: auto threshold: 5% fronzbot-blinkpy-098d43b/pylintrc000066400000000000000000000016241463463503200171620ustar00rootroot00000000000000[MASTER] reports=no # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - it's annoying # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # no-else-return - I don't see any reason to enforce this. both forms are readable # no-self-use - stupid and only annoying # unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb disable= format, bad-continuation, locally-disabled, unused-argument, duplicate-code, implicit-str-concat, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, too-many-lines, too-few-public-methods, no-else-return, no-self-use, unexpected-keyword-arg, fronzbot-blinkpy-098d43b/pyproject.toml000066400000000000000000000064521463463503200203130ustar00rootroot00000000000000[build-system] requires = ["setuptools~=68.0", "wheel~=0.40.0"] build-backend = "setuptools.build_meta" [project] name = "blinkpy" version = "0.23.0" license = {text = "MIT"} description = "A Blink camera Python Library." readme = "README.rst" authors = [ {name = "Kevin Fronczak", email = "kfronczak@gmail.com"}, ] classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Home Automation", ] requires-python = ">=3.9.0" dynamic = ["dependencies"] [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} [project.urls] "Source Code" = "https://github.com/fronzbot/blinkpy" "Bug Reports" = "https://github.com/fronzbot/blinkpy/issues" [tool.setuptools] platforms = ["any"] include-package-data = true [tool.setuptools.packages.find] include = ["blinkpy*"] [tool.ruff] lint.select = [ "C", # complexity "D", # docstrings "E", # pydocstyle "F", # pyflakes/autoflake "G", # flake8-logging-format "N815", # Varible {name} in class scope should not be mixedCase "PGH004", # Use specific rule codes when using noqa "PLC", # pylint "PLE", # pylint "PLR", # pylint "PLW", # pylint "Q000", # Double quotes found but single quotes preferred "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() "TRY004", # Prefer TypeError exception for invalid type "B904", # Use raise from to specify exception cause "UP", # pyupgrade "W", # pycodestyle ] lint.ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D212", # Multi-line docstring summary should start at the first line "D213", # Multi-line docstring summary should start at the second line "D406", # Section name should end with a newline "D407", # Section name underlining "E731", # do not assign a lambda expression, use a def "G004", # I don't care if logging uses an f string "PLC1901", # Lots of false positives # False positives https://github.com/astral-sh/ruff/issues/5386 "PLC0208", # Use a sequence type instead of a `set` when iterating over values "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "UP006", # keep type annotation style as is "UP007", # keep type annotation style as is "UP015", # Unnecessary open mode parameters "UP017", # UTC stuff # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` ] line-length = 88 target-version = "py312" [tool.ruff.lint.per-file-ignores] [tool.ruff.lint.mccabe] max-complexity = 25 fronzbot-blinkpy-098d43b/pytest.ini000066400000000000000000000000701463463503200174160ustar00rootroot00000000000000[pytest] filterwarnings = ignore::DeprecationWarningfronzbot-blinkpy-098d43b/requirements.txt000066400000000000000000000001721463463503200206540ustar00rootroot00000000000000python-dateutil>=2.8.1 requests>=2.24.0 python-slugify>=4.0.1 sortedcontainers~=2.4.0 aiohttp>=3.8.4 aiofiles>=23.1.0fronzbot-blinkpy-098d43b/requirements_test.txt000066400000000000000000000003571463463503200217200ustar00rootroot00000000000000ruff==0.4.9 black==24.4.2 build==1.2.1 coverage==7.5.3 pytest==8.2.2 pytest-cov==5.0.0 pytest-sugar==1.0.0 pytest-timeout==2.3.1 restructuredtext-lint==1.4.0 pygments==2.18.0 testtools>=2.4.0 sortedcontainers~=2.4.0 pytest-asyncio>=0.21.0 fronzbot-blinkpy-098d43b/tests/000077500000000000000000000000001463463503200165325ustar00rootroot00000000000000fronzbot-blinkpy-098d43b/tests/__init__.py000066400000000000000000000000461463463503200206430ustar00rootroot00000000000000"""Init file for tests directory.""" fronzbot-blinkpy-098d43b/tests/mock_responses.py000066400000000000000000000016541463463503200221440ustar00rootroot00000000000000"""Simple mock responses definitions.""" from unittest import mock class MockResponse: """Class for mock request response.""" def __init__( self, json_data, status_code, headers={}, raw_data=None, raise_error=None, ): """Initialize mock get response.""" self.json_data = json_data self.status = status_code self.raw_data = raw_data self.reason = "foobar" self.headers = headers self.read = mock.AsyncMock(return_value=self.raw_data) self.raise_error = raise_error self.text = mock.AsyncMock(return_vlaue="some text") async def json(self): """Return json data from get_request.""" if self.raise_error: raise self.raise_error("I'm broken", "") return self.json_data def get(self, name): """Return field for json.""" return self.json_data[name] fronzbot-blinkpy-098d43b/tests/test_api.py000066400000000000000000000173711463463503200207250ustar00rootroot00000000000000"""Test api functions.""" from unittest import mock from unittest import IsolatedAsyncioTestCase from blinkpy import api from blinkpy.blinkpy import Blink, util 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_COMPLETE_BAD = {"complete": True, "status_code": 999} COMMAND_NOT_COMPLETE = {"complete": False, "status_code": 908} @mock.patch("blinkpy.auth.Auth.query") class TestAPI(IsolatedAsyncioTestCase): """Test the API class in blinkpy.""" def setUp(self): """Set up Login Handler.""" self.blink = Blink(session=mock.AsyncMock()) self.auth = Auth() self.blink.available = True self.blink.urls = util.BlinkURLHandler("region_id") self.blink.account_id = 1234 self.blink.client_id = 5678 def tearDown(self): """Clean up after test.""" self.blink = None self.auth = None async def test_request_verify(self, mock_resp): """Test api request verify.""" mock_resp.return_value = mresp.MockResponse({}, 200) response = await api.request_verify(self.auth, self.blink, "test key") self.assertEqual(response.status, 200) async def test_request_logout(self, mock_resp): """Test request_logout.""" mock_resp.return_value = mresp.MockResponse({}, 200) response = await api.request_logout(self.blink) self.assertEqual(response.status, 200) async def test_request_networks(self, mock_resp): """Test request networks.""" mock_resp.return_value = {"networks": "1234"} self.assertEqual(await api.request_networks(self.blink), {"networks": "1234"}) async def test_request_user(self, mock_resp): """Test request_user.""" mock_resp.return_value = {"user": "userid"} self.assertEqual(await api.request_user(self.blink), {"user": "userid"}) async def test_request_network_status(self, mock_resp): """Test request network status.""" mock_resp.return_value = {"user": "userid"} self.assertEqual( await api.request_network_status(self.blink, "network"), {"user": "userid"} ) async def test_request_command_status(self, mock_resp): """Test command_status.""" mock_resp.side_effect = ({"command": "done"}, COMMAND_COMPLETE) self.assertEqual( await api.request_command_status(self.blink, "network", "command"), {"command": "done"}, ) async def test_request_new_image(self, mock_resp): """Test api request new image.""" 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.side_effect = ( mresp.MockResponse(COMMAND_RESPONSE, 200), COMMAND_COMPLETE, ) response = await api.request_new_video(self.blink, "network", "camera") self.assertEqual(response.status, 200) async def test_request_video_count(self, mock_resp): """Test api request video count.""" mock_resp.return_value = {"count": "10"} self.assertEqual(await api.request_video_count(self.blink), {"count": "10"}) async def test_request_cameras(self, mock_resp): """Test api request cameras.""" mock_resp.return_value = {"cameras": {"camera_id": 1}} self.assertEqual( await api.request_cameras(self.blink, "network"), {"cameras": {"camera_id": 1}}, ) async def test_request_camera_usage(self, mock_resp): """Test api request cameras.""" mock_resp.return_value = {"cameras": "1111"} self.assertEqual( await api.request_camera_usage(self.blink), {"cameras": "1111"} ) async def test_request_notification_flags(self, mock_resp): """Test notification flag request.""" mock_resp.return_value = {"notifications": {"some_key": False}} self.assertEqual( await api.request_notification_flags(self.blink), {"notifications": {"some_key": False}}, ) async def test_request_set_notification_flag(self, mock_resp): """Test set of notifiaction flags.""" mock_resp.side_effect = ( mresp.MockResponse(COMMAND_RESPONSE, 200), COMMAND_COMPLETE, ) response = await api.request_set_notification_flag(self.blink, {}) self.assertEqual(response.status, 200) async def test_request_motion_detection_enable(self, mock_resp): """Test Motion detect enable.""" mock_resp.side_effect = ( mresp.MockResponse(COMMAND_RESPONSE, 200), COMMAND_COMPLETE, ) response = await api.request_motion_detection_enable( self.blink, "network", "camera" ) self.assertEqual(response.status, 200) async def test_request_motion_detection_disable(self, mock_resp): """Test Motion detect enable.""" mock_resp.side_effect = ( mresp.MockResponse(COMMAND_RESPONSE, 200), COMMAND_COMPLETE, ) response = await api.request_motion_detection_disable( self.blink, "network", "camera" ) self.assertEqual(response.status, 200) async def test_request_local_storage_clip(self, mock_resp): """Test Motion detect enable.""" 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" ) self.assertEqual(response.status, 200) async def test_request_get_config(self, mock_resp): """Test request get config.""" mock_resp.return_value = {"config": "values"} self.assertEqual( await api.request_get_config(self.blink, "network", "camera_id", "owl"), {"config": "values"}, ) self.assertEqual( await api.request_get_config( self.blink, "network", "camera_id", "catalina" ), {"config": "values"}, ) async def test_request_update_config(self, mock_resp): """Test Motion detect enable.""" mock_resp.return_value = mresp.MockResponse(COMMAND_RESPONSE, 200) response = await api.request_update_config( self.blink, "network", "camera_id", "owl" ) self.assertEqual(response.status, 200) response = await api.request_update_config( self.blink, "network", "camera_id", "catalina" ) self.assertEqual(response.status, 200) self.assertIsNone( await api.request_update_config( 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, COMMAND_NOT_COMPLETE, None) # response = await api.wait_for_command(self.blink, COMMAND_RESPONSE) # self.assertFalse(response) mock_resp.side_effect = (COMMAND_COMPLETE_BAD, {}) response = await api.wait_for_command(self.blink, COMMAND_RESPONSE) self.assertFalse(response) response = await api.wait_for_command(self.blink, None) self.assertFalse(response) fronzbot-blinkpy-098d43b/tests/test_auth.py000066400000000000000000000272021463463503200211070ustar00rootroot00000000000000"""Test login handler.""" from unittest import mock from unittest import IsolatedAsyncioTestCase from aiohttp import ClientConnectionError, ContentTypeError from blinkpy.auth import ( Auth, TokenRefreshFailed, BlinkBadResponse, UnauthorizedError, ) import blinkpy.helpers.constants as const import tests.mock_responses as mresp USERNAME = "foobar" PASSWORD = "deadbeef" class TestAuth(IsolatedAsyncioTestCase): """Test the Auth class in blinkpy.""" def setUp(self): """Set up Login Handler.""" self.auth = Auth() def tearDown(self): """Clean up after test.""" self.auth = None @mock.patch("blinkpy.helpers.util.gen_uid") @mock.patch("blinkpy.auth.util.getpass") def test_empty_init(self, getpwd, genuid): """Test initialization with no params.""" auth = Auth() self.assertDictEqual(auth.data, {}) getpwd.return_value = "bar" genuid.return_value = 1234 with mock.patch("builtins.input", return_value="foo"): auth.validate_login() expected_data = { "username": "foo", "password": "bar", "uid": 1234, "device_id": const.DEVICE_ID, } self.assertDictEqual(auth.data, expected_data) @mock.patch("blinkpy.helpers.util.gen_uid") @mock.patch("blinkpy.auth.util.getpass") def test_barebones_init(self, getpwd, genuid): """Test basebones initialization.""" login_data = {"username": "foo", "password": "bar"} auth = Auth(login_data) self.assertDictEqual(auth.data, login_data) getpwd.return_value = "bar" genuid.return_value = 1234 with mock.patch("builtins.input", return_value="foo"): auth.validate_login() expected_data = { "username": "foo", "password": "bar", "uid": 1234, "device_id": const.DEVICE_ID, } self.assertDictEqual(auth.data, expected_data) def test_full_init(self): """Test full initialization.""" login_data = { "username": "foo", "password": "bar", "token": "token", "host": "host", "region_id": "region_id", "client_id": "client_id", "account_id": "account_id", "uid": 1234, "notification_key": 4321, "device_id": "device_id", } auth = Auth(login_data) self.assertEqual(auth.token, "token") self.assertEqual(auth.host, "host") self.assertEqual(auth.region_id, "region_id") self.assertEqual(auth.client_id, "client_id") self.assertEqual(auth.account_id, "account_id") auth.validate_login() self.assertDictEqual(auth.login_attributes, login_data) async def test_bad_response_code(self): """Check bad response code from server.""" self.auth.is_errored = False fake_resp = mresp.MockResponse({"code": 404}, 404) with self.assertRaises(ClientConnectionError): await self.auth.validate_response(fake_resp, True) self.assertTrue(self.auth.is_errored) self.auth.is_errored = False fake_resp = mresp.MockResponse({"code": 101}, 401) with self.assertRaises(UnauthorizedError): await self.auth.validate_response(fake_resp, True) self.assertTrue(self.auth.is_errored) fake_resp = mresp.MockResponse({"code": 101}, 406, raise_error=ContentTypeError) with self.assertRaises(BlinkBadResponse): await self.auth.validate_response(fake_resp, True) self.assertTrue(self.auth.is_errored) async def test_good_response_code(self): """Check good response code from server.""" fake_resp = mresp.MockResponse({"foo": "bar"}, 200) self.auth.is_errored = True self.assertEqual( await self.auth.validate_response(fake_resp, True), {"foo": "bar"} ) self.assertFalse(self.auth.is_errored) async def test_response_not_json(self): """Check response when not json.""" fake_resp = "foobar" self.auth.is_errored = True self.assertEqual(await self.auth.validate_response(fake_resp, False), "foobar") self.assertFalse(self.auth.is_errored) async def test_response_bad_json(self): """Check response when not json but expecting json.""" self.auth.is_errored = False with self.assertRaises(BlinkBadResponse): await self.auth.validate_response(None, True) self.assertTrue(self.auth.is_errored) def test_header(self): """Test header data.""" self.auth.token = "bar" expected_header = { "APP-BUILD": const.APP_BUILD, "TOKEN_AUTH": "bar", "User-Agent": const.DEFAULT_USER_AGENT, "Content-Type": "application/json", } self.assertDictEqual(self.auth.header, expected_header) def test_header_no_token(self): """Test header without token.""" self.auth.token = None self.assertEqual(self.auth.header, None) @mock.patch("blinkpy.auth.Auth.validate_login") @mock.patch("blinkpy.auth.Auth.refresh_token") async def test_auth_startup(self, mock_validate, mock_refresh): """Test auth startup.""" await self.auth.startup() @mock.patch("blinkpy.auth.Auth.query") async def test_refresh_token(self, mock_resp): """Test refresh token method.""" mock_resp.return_value.json = mock.AsyncMock( return_value={ "account": {"account_id": 5678, "client_id": 1234, "tier": "test"}, "auth": {"token": "foobar"}, } ) mock_resp.return_value.status = 200 self.auth.no_prompt = True self.assertTrue(await self.auth.refresh_token()) self.assertEqual(self.auth.region_id, "test") self.assertEqual(self.auth.token, "foobar") self.assertEqual(self.auth.client_id, 1234) self.assertEqual(self.auth.account_id, 5678) self.assertEqual(self.auth.user_id, None) mock_resp.return_value.status = 400 with self.assertRaises(TokenRefreshFailed): await self.auth.refresh_token() mock_resp.return_value.status = 200 mock_resp.return_value.json = mock.AsyncMock(side_effect=AttributeError) with self.assertRaises(TokenRefreshFailed): await self.auth.refresh_token() @mock.patch("blinkpy.auth.Auth.login") async def test_refresh_token_failed(self, mock_login): """Test refresh token failed.""" mock_login.return_value = {} self.auth.is_errored = False with self.assertRaises(TokenRefreshFailed): await self.auth.refresh_token() self.assertTrue(self.auth.is_errored) def test_check_key_required(self): """Check key required method.""" self.auth.login_response = {} self.assertFalse(self.auth.check_key_required()) self.auth.login_response = {"account": {"client_verification_required": False}} self.assertFalse(self.auth.check_key_required()) self.auth.login_response = {"account": {"client_verification_required": True}} self.assertTrue(self.auth.check_key_required()) @mock.patch("blinkpy.auth.api.request_logout") async def test_logout(self, mock_req): """Test logout method.""" mock_blink = MockBlink(None) mock_req.return_value = True self.assertTrue(await self.auth.logout(mock_blink)) @mock.patch("blinkpy.auth.api.request_verify") async def test_send_auth_key(self, mock_req): """Check sending of auth key.""" mock_blink = MockBlink(None) mock_req.return_value = mresp.MockResponse({"valid": True}, 200) self.assertTrue(await self.auth.send_auth_key(mock_blink, 1234)) self.assertTrue(mock_blink.available) mock_req.return_value = mresp.MockResponse(None, 200) self.assertFalse(await self.auth.send_auth_key(mock_blink, 1234)) mock_req.return_value = mresp.MockResponse({}, 200) self.assertFalse(await self.auth.send_auth_key(mock_blink, 1234)) self.assertTrue(await self.auth.send_auth_key(mock_blink, None)) @mock.patch("blinkpy.auth.api.request_verify") async def test_send_auth_key_fail(self, mock_req): """Check handling of auth key failure.""" mock_blink = MockBlink(None) mock_req.return_value = mresp.MockResponse(None, 200) self.assertFalse(await self.auth.send_auth_key(mock_blink, 1234)) mock_req.return_value = mresp.MockResponse({}, 200) self.assertFalse(await self.auth.send_auth_key(mock_blink, 1234)) mock_req.return_value = mresp.MockResponse( {"valid": False, "message": "Not good"}, 200 ) self.assertFalse(await self.auth.send_auth_key(mock_blink, 1234)) @mock.patch( "blinkpy.auth.Auth.validate_response", mock.AsyncMock(side_effect=[UnauthorizedError, "foobar"]), ) @mock.patch("blinkpy.auth.Auth.refresh_token", mock.AsyncMock(return_value=True)) @mock.patch("blinkpy.auth.Auth.query", mock.AsyncMock(return_value="foobar")) async def test_query_retry(self): # , mock_refresh, mock_validate): """Check handling of request retry.""" self.auth.session = MockSession() self.assertEqual(await self.auth.query(url="http://example.com"), "foobar") @mock.patch("blinkpy.auth.Auth.validate_response") @mock.patch("blinkpy.auth.Auth.refresh_token") async def test_query_retry_failed(self, mock_refresh, mock_validate): """Check handling of failed retry request.""" self.auth.session = MockSession() mock_validate.side_effect = [ BlinkBadResponse, UnauthorizedError, TokenRefreshFailed, ] mock_refresh.return_value = True self.assertEqual(await self.auth.query(url="http://example.com"), None) self.assertEqual(await self.auth.query(url="http://example.com"), None) @mock.patch("blinkpy.auth.Auth.validate_response") async def test_query(self, mock_validate): """Test query functions.""" self.auth.session = MockSession_with_data() await self.auth.query("URL", "data", "headers", "get") await self.auth.query("URL", "data", "headers", "post") mock_validate.side_effect = ClientConnectionError self.assertIsNone(await self.auth.query("URL", "data", "headers", "get")) mock_validate.side_effect = BlinkBadResponse self.assertIsNone(await self.auth.query("URL", "data", "headers", "post")) mock_validate.side_effect = UnauthorizedError self.auth.refresh_token = mock.AsyncMock() self.assertIsNone(await self.auth.query("URL", "data", "headers", "post")) class MockSession: """Object to mock a session.""" async def get(self, *args, **kwargs): """Mock send function.""" return None async def post(self, *args, **kwargs): """Mock send function.""" return None class MockSession_with_data: """Object to mock a session.""" async def get(self, *args, **kwargs): """Mock send function.""" response = mock.AsyncMock response.status = 400 response.reason = "Some Reason" return response async def post(self, *args, **kwargs): """Mock send function.""" response = mock.AsyncMock response.status = 400 response.reason = "Some Reason" return response class MockBlink: """Object to mock basic blink class.""" def __init__(self, login_response): """Initialize mock blink class.""" self.available = False self.login_response = login_response fronzbot-blinkpy-098d43b/tests/test_blink_functions.py000066400000000000000000000275271463463503200233470ustar00rootroot00000000000000"""Tests camera and system functions.""" from unittest import mock, IsolatedAsyncioTestCase import time import random from io import BufferedIOBase import aiofiles 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 blink sync module object.""" async def get_network_info(self): """Mock network info method.""" return True class MockCamera(BlinkCamera): """Mock blink camera object.""" def __init__(self, sync): """Initialize mock camera.""" super().__init__(sync) self.camera_id = random.randint(1, 100000) async def update(self, config, force_cache=False, **kwargs): """Mock camera update method.""" class TestBlinkFunctions(IsolatedAsyncioTestCase): """Test Blink and BlinkCamera functions in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = blinkpy.Blink(session=mock.AsyncMock()) self.blink.urls = BlinkURLHandler("test") def tearDown(self): """Clean up after test.""" self.blink = None def test_merge_cameras(self): """Test merge camera functionality.""" first_dict = {"foo": "bar", "test": 123} next_dict = {"foobar": 456, "bar": "foo"} self.blink.sync["foo"] = BlinkSyncModule(self.blink, "foo", 1, []) self.blink.sync["bar"] = BlinkSyncModule(self.blink, "bar", 2, []) self.blink.sync["foo"].cameras = first_dict self.blink.sync["bar"].cameras = next_dict result = self.blink.merge_cameras() expected = {"foo": "bar", "test": 123, "foobar": 456, "bar": "foo"} self.assertEqual(expected, result) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_download_video_exit(self, mock_req): """Test we exit method when provided bad response.""" blink = blinkpy.Blink(session=mock.AsyncMock()) blink.last_refresh = 0 mock_req.return_value = {} formatted_date = get_time(blink.last_refresh) expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "INFO:blinkpy.blinkpy:No videos found on page 1. Exiting.", ] with self.assertLogs(level="DEBUG") as dl_log: await blink.download_videos("/tmp") self.assertListEqual(dl_log.output, expected_log) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_parse_downloaded_items(self, mock_req): """Test ability to parse downloaded items list.""" blink = blinkpy.Blink(session=mock.AsyncMock()) generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": True, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} blink.last_refresh = 0 formatted_date = get_time(blink.last_refresh) expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.", ] with self.assertLogs(level="DEBUG") as dl_log: await blink.download_videos("/tmp", stop=2, delay=0) self.assertListEqual(dl_log.output, expected_log) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_parse_downloaded_throttle(self, mock_req): """Test ability to parse downloaded items list.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": False, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} self.blink.last_refresh = 0 start = time.time() await self.blink.download_videos("/tmp", stop=2, delay=0, debug=True) now = time.time() delta = now - start self.assertTrue(delta < 0.1) start = time.time() await self.blink.download_videos("/tmp", stop=2, delay=0.1, debug=True) now = time.time() delta = now - start self.assertTrue(delta >= 0.1) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_get_videos_metadata(self, mock_req): """Test ability to fetch videos metadata.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": True, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} self.blink.last_refresh = 0 results = await self.blink.get_videos_metadata(stop=2) self.assertListEqual(results, result) results = await self.blink.get_videos_metadata( since="2018/07/28 12:33:00", stop=2 ) self.assertListEqual(results, result) mock_req.return_value = {"media": None} results = await self.blink.get_videos_metadata(stop=2) self.assertListEqual(results, []) @mock.patch("blinkpy.blinkpy.api.http_get") async def test_do_http_get(self, mock_req): """Test ability to do_http_get.""" blink = blinkpy.Blink(session=mock.AsyncMock()) blink.urls = BlinkURLHandler("test") response = await blink.do_http_get("/path/to/request") self.assertTrue(response is not None) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_download_videos_deleted(self, mock_req): """Test ability to download videos.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": True, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} self.blink.last_refresh = 0 formatted_date = get_time(self.blink.last_refresh) expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.", ] with self.assertLogs(level="DEBUG") as dl_log: await self.blink.download_videos("/tmp", camera="foo", stop=2, delay=0) self.assertListEqual(dl_log.output, expected_log) @mock.patch("blinkpy.blinkpy.api.request_videos") @mock.patch("aiofiles.ospath.isfile") async def test_download_videos_file(self, mock_isfile, mock_req): """Test ability to download videos to a file.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": False, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} mock_isfile.return_value = False self.blink.last_refresh = 0 aiofiles.threadpool.wrap.register(mock.MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( *args, **kwargs ) ) mock_file = mock.MagicMock(spec=BufferedIOBase) with mock.patch("aiofiles.threadpool.sync_open", return_value=mock_file): await self.blink.download_videos("/tmp", camera="foo", stop=2, delay=0) assert mock_file.write.call_count == 1 @mock.patch("blinkpy.blinkpy.api.request_videos") @mock.patch("aiofiles.ospath.isfile") async def test_download_videos_file_exists(self, mock_isfile, mock_req): """Test ability to download videos with file exists.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": False, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} mock_isfile.return_value = True self.blink.last_refresh = 0 formatted_date = get_time(self.blink.last_refresh) expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "INFO:blinkpy.blinkpy:/tmp/foo-1970.mp4 already exists, skipping...", ] with self.assertLogs(level="DEBUG") as dl_log: await self.blink.download_videos("/tmp", camera="foo", stop=2, delay=0) assert expected_log[0] in dl_log.output assert expected_log[1] in dl_log.output assert expected_log[2] in dl_log.output @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_parse_camera_not_in_list(self, mock_req): """Test ability to parse downloaded items list.""" generic_entry = { "created_at": "1970", "device_name": "foo", "deleted": True, "media": "/bar.mp4", } result = [generic_entry] mock_req.return_value = {"media": result} self.blink.last_refresh = 0 formatted_date = get_time(self.blink.last_refresh) expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "DEBUG:blinkpy.blinkpy:Skipping videos for foo.", ] with self.assertLogs(level="DEBUG") as dl_log: await self.blink.download_videos("/tmp", camera="bar", stop=2, delay=0) self.assertListEqual(dl_log.output, expected_log) @mock.patch("blinkpy.blinkpy.api.request_videos") async def test_parse_malformed_entry(self, mock_req): """Test ability to parse downloaded items in malformed list.""" self.blink.last_refresh = 0 formatted_date = get_time(self.blink.last_refresh) generic_entry = { "created_at": "1970", } result = [generic_entry] mock_req.return_value = {"media": result} expected_log = [ f"INFO:blinkpy.blinkpy:Retrieving videos since {formatted_date}", "DEBUG:blinkpy.blinkpy:Processing page 1", "INFO:blinkpy.blinkpy:Missing clip information, skipping...", ] with self.assertLogs(level="DEBUG") as dl_log: await self.blink.download_videos("/tmp", camera="bar", stop=2, delay=0) self.assertListEqual(dl_log.output, expected_log) @mock.patch("blinkpy.blinkpy.api.request_network_update") @mock.patch("blinkpy.auth.Auth.query") async 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(await self.blink.refresh()) @mock.patch("blinkpy.blinkpy.api.request_notification_flags") async def test_get_status(self, mock_req): """Test get of notification flags.""" mock_req.return_value = {"notifications": {"foo": True}} self.assertDictEqual(await self.blink.get_status(), {"foo": True}) @mock.patch("blinkpy.blinkpy.api.request_notification_flags") async def test_get_status_malformed(self, mock_req): """Test get of notification flags with malformed response.""" mock_req.return_value = {"nobueno": {"foo": False}} self.assertDictEqual(await self.blink.get_status(), {"nobueno": {"foo": False}}) @mock.patch("blinkpy.blinkpy.api.request_set_notification_flag") async def test_set_status(self, mock_req): """Test set of notification flags.""" mock_req.return_value = True self.assertTrue(await self.blink.set_status()) fronzbot-blinkpy-098d43b/tests/test_blinkpy.py000066400000000000000000000535301463463503200216210ustar00rootroot00000000000000""" Test full system. Tests the system initialization and attributes of the main Blink system. Tests if we properly catch any communication related errors at startup. """ from unittest import mock from unittest import IsolatedAsyncioTestCase import time from blinkpy.blinkpy import Blink, BlinkSetupError, LoginError, TokenRefreshFailed from blinkpy.sync_module import BlinkOwl, BlinkLotus from blinkpy.helpers.constants import __version__ SPECIAL = "!@#$%^&*()_+-=[]{}|/<>?,.'" class TestBlinkSetup(IsolatedAsyncioTestCase): """Test the Blink class in blinkpy.""" def setUp(self): """Initialize blink test object.""" self.blink = Blink(session=mock.AsyncMock()) self.blink.available = True def tearDown(self): """Cleanup blink test object.""" self.blink = None def test_initialization(self): """Verify we can initialize blink.""" blink = Blink() self.assertEqual(blink.version, __version__) def test_network_id_failure(self): """Check that with bad network data a setup error is raised.""" self.blink.networks = None with self.assertRaises(BlinkSetupError): self.blink.setup_network_ids() def test_multiple_networks(self): """Check that we handle multiple networks appropriately.""" self.blink.networks = { "0000": {"onboarded": False, "name": "foo"}, "5678": {"onboarded": True, "name": "bar"}, "1234": {"onboarded": False, "name": "test"}, } self.blink.setup_network_ids() self.assertTrue("5678" in self.blink.network_ids) def test_multiple_onboarded_networks(self): """Check that we handle multiple networks appropriately.""" self.blink.networks = { "0000": {"onboarded": False, "name": "foo"}, "5678": {"onboarded": True, "name": "bar"}, "1234": {"onboarded": True, "name": "test"}, } self.blink.setup_network_ids() self.assertTrue("0000" not in self.blink.network_ids) self.assertTrue("5678" in self.blink.network_ids) self.assertTrue("1234" in self.blink.network_ids) @mock.patch("blinkpy.blinkpy.time.time") async def test_throttle(self, mock_time): """Check throttling functionality.""" now = self.blink.refresh_rate + 1 mock_time.return_value = now self.assertEqual(self.blink.last_refresh, None) self.assertEqual(self.blink.check_if_ok_to_update(), True) self.assertEqual(self.blink.last_refresh, None) with ( mock.patch( "blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True ), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True), ): await self.blink.refresh(force=True) self.assertEqual(self.blink.last_refresh, now) self.assertEqual(self.blink.check_if_ok_to_update(), False) self.assertEqual(self.blink.last_refresh, now) async def test_not_available_refresh(self): """Check that setup_post_verify executes on refresh when not avialable.""" self.blink.available = False with ( mock.patch( "blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True ), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True), mock.patch("blinkpy.blinkpy.Blink.setup_post_verify", return_value=True), ): self.assertTrue(await self.blink.refresh(force=True)) with mock.patch("time.time", return_value=time.time() + 4): self.assertFalse(await self.blink.refresh()) def test_sync_case_insensitive_dict(self): """Check that we can access sync modules ignoring case.""" self.blink.sync["test"] = 1234 self.assertEqual(self.blink.sync["test"], 1234) self.assertEqual(self.blink.sync["TEST"], 1234) self.assertEqual(self.blink.sync["tEsT"], 1234) def test_sync_special_chars(self): """Check that special chars can be used as sync name.""" self.blink.sync[SPECIAL] = 1234 self.assertEqual(self.blink.sync[SPECIAL], 1234) @mock.patch("blinkpy.api.request_camera_usage") @mock.patch("blinkpy.api.request_homescreen") async def test_setup_cameras(self, mock_home, mock_req): """Check retrieval of camera information.""" mock_home.return_value = {} mock_req.return_value = { "networks": [ { "network_id": 1234, "cameras": [ {"id": 5678, "name": "foo"}, {"id": 5679, "name": "bar"}, {"id": 5779, "name": SPECIAL}, ], }, {"network_id": 4321, "cameras": [{"id": 0000, "name": "test"}]}, ] } result = await self.blink.setup_camera_list() self.assertEqual( result, { "1234": [ {"name": "foo", "id": 5678, "type": "default"}, {"name": "bar", "id": 5679, "type": "default"}, {"name": SPECIAL, "id": 5779, "type": "default"}, ], "4321": [{"name": "test", "id": 0000, "type": "default"}], }, ) @mock.patch("blinkpy.api.request_camera_usage") async def test_setup_cameras_failure(self, mock_home): """Check that on failure we raise a setup error.""" mock_home.return_value = {} with self.assertRaises(BlinkSetupError): await self.blink.setup_camera_list() mock_home.return_value = None with self.assertRaises(BlinkSetupError): await self.blink.setup_camera_list() def test_setup_urls(self): """Check setup of URLS.""" self.blink.auth.region_id = "test" self.blink.setup_urls() self.assertEqual(self.blink.urls.subdomain, "rest-test") def test_setup_urls_failure(self): """Check that on failure we raise a setup error.""" self.blink.auth.region_id = None with self.assertRaises(BlinkSetupError): self.blink.setup_urls() @mock.patch("blinkpy.api.request_networks") async def test_setup_networks(self, mock_networks): """Check setup of networks.""" mock_networks.return_value = {"summary": "foobar"} await self.blink.setup_networks() self.assertEqual(self.blink.networks, "foobar") @mock.patch("blinkpy.api.request_networks") async def test_setup_networks_failure(self, mock_networks): """Check that on failure we raise a setup error.""" mock_networks.return_value = {} with self.assertRaises(BlinkSetupError): await self.blink.setup_networks() mock_networks.return_value = None with self.assertRaises(BlinkSetupError): await self.blink.setup_networks() @mock.patch("blinkpy.blinkpy.Auth.send_auth_key") async def test_setup_prompt_2fa(self, mock_key): """Test setup with 2fa prompt.""" self.blink.auth.data["username"] = "foobar" self.blink.key_required = True mock_key.return_value = True with mock.patch("builtins.input", return_value="foo"): await self.blink.setup_prompt_2fa() self.assertFalse(self.blink.key_required) mock_key.return_value = False with mock.patch("builtins.input", return_value="foo"): await self.blink.setup_prompt_2fa() self.assertTrue(self.blink.key_required) @mock.patch("blinkpy.blinkpy.Blink.setup_camera_list") @mock.patch("blinkpy.api.request_homescreen") @mock.patch("blinkpy.api.request_networks") @mock.patch("blinkpy.blinkpy.Blink.setup_owls") @mock.patch("blinkpy.blinkpy.Blink.setup_lotus") @mock.patch("blinkpy.blinkpy.BlinkSyncModule.start") async def test_setup_post_verify( self, mock_sync, mock_lotus, mock_owl, mock_networks, mock_home, mock_camera ): """Test setup after verification.""" self.blink.available = False self.blink.key_required = True mock_lotus.return_value = True mock_owl.return_value = True mock_camera.side_effect = [ { "name": "bar", "id": "1323", "type": "default", } ] mock_networks.return_value = { "summary": {"foo": {"onboarded": True, "name": "bar"}} } mock_home.return_value = {} mock_camera.return_value = [] self.assertTrue(await self.blink.setup_post_verify()) self.assertTrue(self.blink.available) self.assertFalse(self.blink.key_required) @mock.patch("blinkpy.api.request_homescreen") @mock.patch("blinkpy.api.request_networks") async def test_setup_post_verify_failure(self, mock_networks, mock_home): """Test failed setup after verification.""" self.blink.available = False mock_networks.return_value = {} mock_home.return_value = {} self.assertFalse(await self.blink.setup_post_verify()) self.assertFalse(self.blink.available) def test_merge_cameras(self): """Test merging of cameras.""" self.blink.sync = { "foo": MockSync({"test": 123, "foo": "bar"}), "bar": MockSync({"fizz": "buzz", "bar": "foo"}), } combined = self.blink.merge_cameras() self.assertEqual(combined["test"], 123) self.assertEqual(combined["foo"], "bar") self.assertEqual(combined["fizz"], "buzz") self.assertEqual(combined["bar"], "foo") @mock.patch("blinkpy.blinkpy.BlinkOwl.start") async def test_initialize_blink_minis(self, mock_start): """Test blink mini initialization.""" mock_start.return_value = True self.blink.homescreen = { "owls": [ { "enabled": False, "id": 1, "name": "foo", "network_id": 2, "onboarded": True, "status": "online", "thumbnail": "/foo/bar", "serial": "1234", }, { "enabled": True, "id": 3, "name": "bar", "network_id": 4, "onboarded": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abcd", }, ] } self.blink.sync = {} await self.blink.setup_owls() self.assertEqual(self.blink.sync["foo"].__class__, BlinkOwl) self.assertEqual(self.blink.sync["bar"].__class__, BlinkOwl) self.assertEqual(self.blink.sync["foo"].arm, False) self.assertEqual(self.blink.sync["bar"].arm, True) self.assertEqual(self.blink.sync["foo"].name, "foo") self.assertEqual(self.blink.sync["bar"].name, "bar") async def test_blink_mini_cameras_returned(self): """Test that blink mini cameras are found if attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "owls": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", } ] } result = await self.blink.setup_owls() self.assertEqual(self.blink.network_ids, ["1234"]) self.assertEqual( result, [{"1234": {"name": "foo", "id": "1234", "type": "mini"}}] ) self.blink.no_owls = True self.blink.network_ids = [] await self.blink.get_homescreen() result = await self.blink.setup_owls() self.assertEqual(self.blink.network_ids, []) self.assertEqual(result, []) @mock.patch("blinkpy.api.request_camera_usage") async def test_blink_mini_attached_to_sync(self, mock_usage): """Test that blink mini cameras are properly attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "owls": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", } ] } mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]} result = await self.blink.setup_camera_list() self.assertEqual( result, {"1234": [{"name": "foo", "id": "1234", "type": "mini"}]} ) @mock.patch("blinkpy.blinkpy.BlinkLotus.start") async def test_initialize_blink_doorbells(self, mock_start): """Test blink doorbell initialization.""" mock_start.return_value = True self.blink.homescreen = { "doorbells": [ { "enabled": False, "id": 1, "name": "foo", "network_id": 2, "onboarded": True, "status": "online", "thumbnail": "/foo/bar", "serial": "1234", }, { "enabled": True, "id": 3, "name": "bar", "network_id": 4, "onboarded": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abcd", }, ] } self.blink.sync = {} await self.blink.setup_lotus() self.assertEqual(self.blink.sync["foo"].__class__, BlinkLotus) self.assertEqual(self.blink.sync["bar"].__class__, BlinkLotus) self.assertEqual(self.blink.sync["foo"].arm, False) self.assertEqual(self.blink.sync["bar"].arm, True) self.assertEqual(self.blink.sync["foo"].name, "foo") self.assertEqual(self.blink.sync["bar"].name, "bar") @mock.patch("blinkpy.api.request_camera_usage") async def test_blink_doorbell_attached_to_sync(self, mock_usage): """Test that blink doorbell cameras are properly attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "doorbells": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", } ] } mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]} result = await self.blink.setup_camera_list() self.assertEqual( result, {"1234": [{"name": "foo", "id": "1234", "type": "doorbell"}]} ) @mock.patch("blinkpy.api.request_camera_usage") async def test_blink_multi_doorbell(self, mock_usage): """Test that multiple doorbells are properly attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "doorbells": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", }, { "id": 2, "name": "bar", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/bar/foo", "serial": "zxc456", }, ] } expected = { "1234": [ {"name": "foo", "id": "1234", "type": "doorbell"}, {"name": "bar", "id": "1234", "type": "doorbell"}, ] } mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]} result = await self.blink.setup_camera_list() self.assertEqual(result, expected) @mock.patch("blinkpy.api.request_camera_usage") async def test_blink_multi_mini(self, mock_usage): """Test that multiple minis are properly attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "owls": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", }, { "id": 2, "name": "bar", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/bar/foo", "serial": "zxc456", }, ] } expected = { "1234": [ {"name": "foo", "id": "1234", "type": "mini"}, {"name": "bar", "id": "1234", "type": "mini"}, ] } mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]} result = await self.blink.setup_camera_list() self.assertEqual(result, expected) @mock.patch("blinkpy.api.request_camera_usage") async def test_blink_camera_mix(self, mock_usage): """Test that a mix of cameras are properly attached to sync module.""" self.blink.network_ids = ["1234"] self.blink.homescreen = { "doorbells": [ { "id": 1, "name": "foo", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/foo/bar", "serial": "abc123", }, { "id": 2, "name": "bar", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/bar/foo", "serial": "zxc456", }, ], "owls": [ { "id": 3, "name": "dead", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/dead/beef", "serial": "qwerty", }, { "id": 4, "name": "beef", "network_id": 1234, "onboarded": True, "enabled": True, "status": "online", "thumbnail": "/beef/dead", "serial": "dvorak", }, ], } expected = { "1234": [ {"name": "foo", "id": "1234", "type": "doorbell"}, {"name": "bar", "id": "1234", "type": "doorbell"}, {"name": "dead", "id": "1234", "type": "mini"}, {"name": "beef", "id": "1234", "type": "mini"}, {"name": "normal", "id": "1234", "type": "default"}, ] } mock_usage.return_value = { "networks": [ {"cameras": [{"name": "normal", "id": "1234"}], "network_id": 1234} ] } result = await self.blink.setup_camera_list() self.assertTrue("1234" in result) for element in result["1234"]: self.assertTrue(element in expected["1234"]) @mock.patch("blinkpy.blinkpy.Blink.get_homescreen") @mock.patch("blinkpy.blinkpy.Blink.setup_prompt_2fa") @mock.patch("blinkpy.auth.Auth.startup") @mock.patch("blinkpy.blinkpy.Blink.setup_login_ids") @mock.patch("blinkpy.blinkpy.Blink.setup_urls") @mock.patch("blinkpy.auth.Auth.check_key_required") @mock.patch("blinkpy.blinkpy.Blink.setup_post_verify") async def test_blink_start( self, mock_verify, mock_check_key, mock_urls, mock_ids, mock_auth_startup, mock_2fa, mock_homescreen, ): """Test blink_start funcion.""" self.assertTrue(await self.blink.start()) self.blink.auth.no_prompt = True self.assertTrue(await self.blink.start()) mock_homescreen.side_effect = [LoginError, TokenRefreshFailed] self.assertFalse(await self.blink.start()) self.assertFalse(await self.blink.start()) def test_setup_login_ids(self): """Test setup_login_ids function.""" self.blink.auth.client_id = 1 self.blink.auth.account_id = 2 self.blink.setup_login_ids() self.assertEqual(self.blink.client_id, 1) self.assertEqual(self.blink.account_id, 2) @mock.patch("blinkpy.blinkpy.util.json_save") async def test_save(self, mock_util): """Test save function.""" await self.blink.save("blah") self.assertEqual(mock_util.call_count, 1) class MockSync: """Mock sync module class.""" def __init__(self, cameras): """Initialize fake class.""" self.cameras = cameras fronzbot-blinkpy-098d43b/tests/test_camera_functions.py000066400000000000000000000363301463463503200234700ustar00rootroot00000000000000""" Test all camera attributes. Tests the camera initialization and attributes of individual BlinkCamera instantiations once the Blink system is set up. """ import datetime from unittest import mock from unittest import IsolatedAsyncioTestCase from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkSyncModule from blinkpy.camera import BlinkCamera, BlinkCameraMini, BlinkDoorbell import tests.mock_responses as mresp CONFIG = { "name": "new", "id": 1234, "network_id": 5678, "serial": "12345678", "enabled": False, "battery_state": "ok", "battery_voltage": 163, "wifi_strength": -38, "signals": {"lfr": 5, "wifi": 4, "battery": 3, "temp": 68}, "thumbnail": "/thumb", } @mock.patch("blinkpy.auth.Auth.query", return_value={}) class TestBlinkCameraSetup(IsolatedAsyncioTestCase): """Test the Blink class in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = Blink(session=mock.AsyncMock()) self.blink.urls = BlinkURLHandler("test") self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, []) self.camera = BlinkCamera(self.blink.sync["test"]) self.camera.name = "foobar" self.blink.sync["test"].cameras["foobar"] = self.camera def tearDown(self): """Clean up after test.""" self.blink = None self.camera = None async def test_camera_update(self, mock_resp): """Test that we can properly update camera properties.""" self.camera.last_record = ["1"] self.camera.sync.last_records = { "new": [{"clip": "/test.mp4", "time": "1970-01-01T00:00:00"}] } mock_resp.side_effect = [ {"temp": 71}, mresp.MockResponse({"test": 200}, 200, raw_data="test"), mresp.MockResponse({"foobar": 200}, 200, raw_data="foobar"), ] self.assertIsNone(self.camera.image_from_cache) await self.camera.update(CONFIG, expire_clips=False) self.assertEqual(self.camera.name, "new") self.assertEqual(self.camera.camera_id, "1234") self.assertEqual(self.camera.network_id, "5678") self.assertEqual(self.camera.serial, "12345678") self.assertEqual(self.camera.motion_enabled, False) self.assertEqual(self.camera.battery, "ok") self.assertEqual(self.camera.temperature, 68) self.assertEqual(self.camera.temperature_c, 20) self.assertEqual(self.camera.temperature_calibrated, 71) self.assertEqual(self.camera.battery_voltage, 163) self.assertEqual(self.camera.wifi_strength, -38) self.assertEqual( self.camera.thumbnail, "https://rest-test.immedia-semi.com/thumb.jpg" ) self.assertEqual( self.camera.clip, "https://rest-test.immedia-semi.com/test.mp4" ) self.assertEqual(self.camera.image_from_cache, "test") self.assertEqual(self.camera.video_from_cache, "foobar") # Check that thumbnail without slash processed properly mock_resp.side_effect = [ mresp.MockResponse({"test": 200}, 200, raw_data="thumb_no_slash") ] await self.camera.update_images( {"thumbnail": "thumb_no_slash"}, expire_clips=False ) self.assertEqual( self.camera.thumbnail, "https://rest-test.immedia-semi.com/thumb_no_slash.jpg", ) async def test_no_thumbnails(self, mock_resp): """Tests that thumbnail is 'None' if none found.""" mock_resp.return_value = "foobar" self.camera.last_record = ["1"] config = { **CONFIG, **{ "thumbnail": "", }, } self.camera.sync.homescreen = {"devices": []} self.assertEqual(self.camera.temperature_calibrated, None) with self.assertLogs() as logrecord: await self.camera.update(config, force=True, expire_clips=False) self.assertEqual(self.camera.thumbnail, None) self.assertEqual(self.camera.last_record, ["1"]) self.assertEqual(self.camera.temperature_calibrated, 68) self.assertEqual( logrecord.output, [ ( "WARNING:blinkpy.camera:Could not retrieve calibrated " f"temperature response {mock_resp.return_value}." ), ( f"WARNING:blinkpy.camera:for network_id ({config['network_id']}) " f"and camera_id ({self.camera.camera_id})" ), ("WARNING:blinkpy.camera:Could not find thumbnail for camera new."), ], ) async def test_no_video_clips(self, mock_resp): """Tests that we still proceed with camera setup with no videos.""" mock_resp.return_value = "foobar" config = { **CONFIG, **{ "thumbnail": "/foobar", }, } mock_resp.return_value = mresp.MockResponse({"test": 200}, 200, raw_data="") self.camera.sync.homescreen = {"devices": []} await self.camera.update(config, force_cache=True, expire_clips=False) self.assertEqual(self.camera.clip, None) self.assertEqual(self.camera.video_from_cache, None) async def test_recent_video_clips(self, mock_resp): """Test recent video clips. Tests that the last records in the sync module are added to the camera recent clips list. """ self.camera.sync.last_records["foobar"] = [] record2 = {"clip": "/clip2", "time": "2022-12-01 00:00:10+00:00"} self.camera.sync.last_records["foobar"].append(record2) record1 = {"clip": "/clip1", "time": "2022-12-01 00:00:00+00:00"} self.camera.sync.last_records["foobar"].append(record1) self.camera.sync.motion["foobar"] = True await self.camera.update_images(CONFIG, expire_clips=False) record1["clip"] = self.blink.urls.base_url + "/clip1" record2["clip"] = self.blink.urls.base_url + "/clip2" self.assertEqual(self.camera.recent_clips[0], record1) self.assertEqual(self.camera.recent_clips[1], record2) async def test_recent_video_clips_missing_key(self, mock_resp): """Tests that the missing key failst.""" self.camera.sync.last_records["foobar"] = [] record2 = {"clip": "/clip2"} self.camera.sync.last_records["foobar"].append(record2) self.camera.sync.motion["foobar"] = True with self.assertLogs(level="ERROR") as dl_log: await self.camera.update_images(CONFIG, expire_clips=False) self.assertIsNotNone(dl_log.output) async def test_expire_recent_clips(self, mock_resp): """Test expiration of recent clips.""" self.camera.recent_clips = [] now = datetime.datetime.now() self.camera.recent_clips.append( { "time": (now - datetime.timedelta(minutes=20)).isoformat(), "clip": "/clip1", }, ) self.camera.recent_clips.append( { "time": (now - datetime.timedelta(minutes=1)).isoformat(), "clip": "local_storage/clip2", }, ) await self.camera.expire_recent_clips(delta=datetime.timedelta(minutes=5)) self.assertEqual(len(self.camera.recent_clips), 1) @mock.patch( "blinkpy.api.request_motion_detection_enable", mock.AsyncMock(return_value="enable"), ) @mock.patch( "blinkpy.api.request_motion_detection_disable", mock.AsyncMock(return_value="disable"), ) async def test_motion_detection_enable_disable(self, mock_rep): """Test setting motion detection enable properly.""" self.assertEqual(await self.camera.set_motion_detect(True), "enable") self.assertEqual(await self.camera.set_motion_detect(False), "disable") async def test_night_vision(self, mock_resp): """Test Night Vision Camera functions.""" # MJK - I don't know what the "real" response is supposed to look like # Need to confirm and adjust this test to match reality? mock_resp.return_value = "blah" self.assertIsNone(await self.camera.night_vision) self.camera.product_type = "catalina" mock_resp.return_value = {"camera": [{"name": "123", "illuminator_enable": 1}]} self.assertIsNotNone(await self.camera.night_vision) self.assertIsNone(await self.camera.async_set_night_vision("0")) mock_resp.return_value = mresp.MockResponse({"code": 200}, 200) self.assertIsNotNone(await self.camera.async_set_night_vision("on")) mock_resp.return_value = mresp.MockResponse({"code": 400}, 400) self.assertIsNone(await self.camera.async_set_night_vision("on")) async def test_record(self, mock_resp): """Test camera record function.""" with mock.patch( "blinkpy.api.request_new_video", mock.AsyncMock(return_value=True) ): self.assertTrue(await self.camera.record()) with mock.patch( "blinkpy.api.request_new_video", mock.AsyncMock(return_value=False) ): self.assertFalse(await self.camera.record()) async def test_get_thumbnail(self, mock_resp): """Test get thumbnail without URL.""" self.assertIsNone(await self.camera.get_thumbnail()) async def test_get_video(self, mock_resp): """Test get video clip without URL.""" self.assertIsNone(await self.camera.get_video_clip()) @mock.patch( "blinkpy.api.request_new_image", mock.AsyncMock(return_value={"json": "Data"}) ) async def test_snap_picture(self, mock_resp): """Test camera snap picture function.""" self.assertIsNotNone(await self.camera.snap_picture()) @mock.patch("blinkpy.api.http_post", mock.AsyncMock(return_value={"json": "Data"})) async def test_snap_picture_blinkmini(self, mock_resp): """Test camera snap picture function.""" self.camera = BlinkCameraMini(self.blink.sync["test"]) self.assertIsNotNone(await self.camera.snap_picture()) @mock.patch("blinkpy.api.http_post", mock.AsyncMock(return_value={"json": "Data"})) async def test_snap_picture_blinkdoorbell(self, mock_resp): """Test camera snap picture function.""" self.camera = BlinkDoorbell(self.blink.sync["test"]) self.assertIsNotNone(await self.camera.snap_picture()) @mock.patch("blinkpy.camera.open", create=True) async def test_image_to_file(self, mock_open, mock_resp): """Test camera image to file.""" mock_resp.return_value = mresp.MockResponse({}, 200, raw_data="raw data") self.camera.thumbnail = "/thumbnail" await self.camera.image_to_file("my_path") @mock.patch("blinkpy.camera.open", create=True) async def test_image_to_file_error(self, mock_open, mock_resp): """Test camera image to file with error.""" mock_resp.return_value = mresp.MockResponse({}, 400, raw_data="raw data") self.camera.thumbnail = "/thumbnail" with self.assertLogs(level="DEBUG") as dl_log: await self.camera.image_to_file("my_path") self.assertEqual( dl_log.output[2], "ERROR:blinkpy.camera:Cannot write image to file, response 400", ) @mock.patch("blinkpy.camera.open", create=True) async def test_video_to_file_none_response(self, mock_open, mock_resp): """Test camera video to file.""" mock_resp.return_value = mresp.MockResponse({}, 200, raw_data="raw data") with self.assertLogs(level="DEBUG") as dl_log: await self.camera.video_to_file("my_path") self.assertEqual( dl_log.output[2], f"ERROR:blinkpy.camera:No saved video exists for {self.camera.name}.", ) @mock.patch("blinkpy.camera.open", create=True) async def test_video_to_file(self, mock_open, mock_resp): """Test camera vido to file with error.""" mock_resp.return_value = mresp.MockResponse({}, 400, raw_data="raw data") self.camera.clip = "my_clip" await self.camera.video_to_file("my_path") mock_open.assert_called_once() @mock.patch("blinkpy.camera.open", create=True) @mock.patch("blinkpy.camera.BlinkCamera.get_video_clip") async def test_save_recent_clips(self, mock_clip, mock_open, mock_resp): """Test camera save recent clips.""" with self.assertLogs(level="DEBUG") as dl_log: await self.camera.save_recent_clips() self.assertEqual( dl_log.output[0], f"INFO:blinkpy.camera:No recent clips to save for '{self.camera.name}'.", ) assert mock_open.call_count == 0 self.camera.recent_clips = [] now = datetime.datetime.now() self.camera.recent_clips.append( { "time": (now - datetime.timedelta(minutes=20)).isoformat(), "clip": "/clip1", }, ) self.camera.recent_clips.append( { "time": (now - datetime.timedelta(minutes=1)).isoformat(), "clip": "local_storage/clip2", }, ) mock_clip.return_value = mresp.MockResponse({}, 200, raw_data="raw data") with self.assertLogs(level="DEBUG") as dl_log: await self.camera.save_recent_clips() self.assertEqual( dl_log.output[4], "INFO:blinkpy.camera:Saved 2 of 2 recent clips from " f"'{self.camera.name}' to directory /tmp/", ) assert mock_open.call_count == 2 def remove_clip(self): """Remove all clips to raise an exception on second removal.""" self[0] *= 0 return mresp.MockResponse({}, 200, raw_data="raw data") @mock.patch("blinkpy.camera.open", create=True) @mock.patch( "blinkpy.camera.BlinkCamera.get_video_clip", create=True, side_effect=remove_clip, ) async def test_save_recent_clips_exception(self, mock_clip, mock_open, mock_resp): """Test corruption in recent clip list.""" self.camera.recent_clips = [] now = datetime.datetime.now() self.camera.recent_clips.append( { "time": (now - datetime.timedelta(minutes=20)).isoformat(), "clip": [self.camera.recent_clips], }, ) with self.assertLogs(level="ERROR") as dl_log: await self.camera.save_recent_clips() print(f"Output = {dl_log.output}") self.assertTrue( "ERROR:blinkpy.camera:Error removing clip from list:" in "\t".join(dl_log.output) ) assert mock_open.call_count == 1 async def test_missing_keys(self, mock_resp): """Tests missing signal keys.""" config = { **CONFIG, **{ "signals": {"junk": 1}, "thumbnail": "", }, } self.camera.sync.homescreen = {"devices": []} mock_resp.side_effect = [ {"temp": 71}, mresp.MockResponse({"test": 200}, 200, raw_data="test"), mresp.MockResponse({"foobar": 200}, 200, raw_data="foobar"), ] await self.camera.update(config, expire_clips=False, force=True) self.assertEqual(self.camera.battery_level, None) fronzbot-blinkpy-098d43b/tests/test_cameras.py000066400000000000000000000151311463463503200215570ustar00rootroot00000000000000""" Test all camera attributes. Tests the camera initialization and attributes of individual BlinkCamera instantiations once the Blink system is set up. """ from unittest import mock from unittest import IsolatedAsyncioTestCase from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkSyncModule from blinkpy.camera import BlinkCamera, BlinkCameraMini, BlinkDoorbell import tests.mock_responses as mresp CONFIG = { "name": "new", "id": 1234, "network_id": 5678, "serial": "12345678", "enabled": False, "battery_state": "ok", "temperature": 68, "thumbnail": 1357924680, "signals": {"lfr": 5, "wifi": 4, "battery": 3}, "type": "test", } @mock.patch("blinkpy.auth.Auth.query", return_value={}) class TestBlinkCameraSetup(IsolatedAsyncioTestCase): """Test the Blink class in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = Blink(session=mock.AsyncMock()) self.blink.urls = BlinkURLHandler("test") self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, []) self.camera = BlinkCamera(self.blink.sync["test"]) self.camera.name = "foobar" self.blink.sync["test"].cameras["foobar"] = self.camera def tearDown(self): """Clean up after test.""" self.blink = None self.camera = None @mock.patch( "blinkpy.api.request_motion_detection_enable", mock.AsyncMock(return_value="enable"), ) @mock.patch( "blinkpy.api.request_motion_detection_disable", mock.AsyncMock(return_value="disable"), ) async def test_camera_arm_status(self, mock_resp): """Test arming and disarming camera.""" self.camera.motion_enabled = None await self.camera.async_arm(None) self.assertFalse(self.camera.arm) await self.camera.async_arm(False) self.camera.motion_enabled = False self.assertFalse(self.camera.arm) await self.camera.async_arm(True) self.camera.motion_enabled = True self.assertTrue(self.camera.arm) self.camera = BlinkCameraMini(self.blink.sync["test"]) self.camera.motion_enabled = None await self.camera.async_arm(None) self.assertFalse(self.camera.arm) async def test_doorbell_camera_arm(self, mock_resp): """Test arming and disarming camera.""" self.blink.sync.arm = False doorbell_camera = BlinkDoorbell(self.blink.sync["test"]) doorbell_camera.motion_enabled = None await doorbell_camera.async_arm(None) self.assertFalse(doorbell_camera.arm) await doorbell_camera.async_arm(False) doorbell_camera.motion_enabled = False self.assertFalse(doorbell_camera.arm) await doorbell_camera.async_arm(True) doorbell_camera.motion_enabled = True self.assertTrue(doorbell_camera.arm) def test_missing_attributes(self, mock_resp): """Test that attributes return None if missing.""" self.camera.temperature = None self.camera.serial = None self.camera._version = None attr = self.camera.attributes self.assertEqual(attr["serial"], None) self.assertEqual(attr["temperature"], None) self.assertEqual(attr["temperature_c"], None) self.assertEqual(attr["version"], None) self.assertEqual(self.camera.version, None) def test_mini_missing_attributes(self, mock_resp): """Test that attributes return None if missing.""" camera = BlinkCameraMini(self.blink.sync) self.blink.sync.network_id = None self.blink.sync.name = None attr = camera.attributes for key in attr: if key == "recent_clips": self.assertEqual(attr[key], []) continue self.assertEqual(attr[key], None) def test_doorbell_missing_attributes(self, mock_resp): """Test that attributes return None if missing.""" camera = BlinkDoorbell(self.blink.sync) self.blink.sync.network_id = None self.blink.sync.name = None attr = camera.attributes for key in attr: if key == "recent_clips": self.assertEqual(attr[key], []) continue self.assertEqual(attr[key], None) async 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"]) doorbell_camera = BlinkDoorbell(self.blink.sync["test"]) self.assertEqual(await self.camera.get_liveview(), "rtsps://foo.bar") self.assertEqual(await mini_camera.get_liveview(), "rtsps://foo.bar") self.assertEqual(await doorbell_camera.get_liveview(), "rtsps://foo.bar") async def test_different_thumb_api(self, mock_resp): """Test that the correct url is created with new api.""" thumb_endpoint = "https://rest-test.immedia-semi.com/api/v3/media/accounts/9999/networks/5678/test/1234/thumbnail/thumbnail.jpg?ts=1357924680&ext=" mock_resp.side_effect = [ {"temp": 71}, mresp.MockResponse({"test": 200}, 200, raw_data="test"), ] self.camera.sync.blink.account_id = 9999 await self.camera.update(CONFIG, expire_clips=False) self.assertEqual(self.camera.thumbnail, thumb_endpoint) async def test_thumb_return_none(self, mock_resp): """Test that a 'None" thumbnail is doesn't break system.""" config = { **CONFIG, **{ "thumbnail": None, }, } mock_resp.side_effect = [ {"temp": 71}, "test", ] await self.camera.update(config, expire_clips=False) self.assertEqual(self.camera.thumbnail, None) async def test_new_thumb_url_returned(self, mock_resp): """Test that thumb handled properly if new url returned.""" thumb_return = ( "/api/v3/media/accounts/9999/networks/5678/" "test/1234/thumbnail/thumbnail.jpg?ts=1357924680&ext=" ) config = { **CONFIG, **{ "thumbnail": thumb_return, }, } mock_resp.side_effect = [ {"temp": 71}, mresp.MockResponse({"test": 200}, 200, raw_data="test"), ] self.camera.sync.blink.account_id = 9999 await self.camera.update(config, expire_clips=False) self.assertEqual( self.camera.thumbnail, f"https://rest-test.immedia-semi.com{thumb_return}" ) fronzbot-blinkpy-098d43b/tests/test_doorbell_as_sync.py000066400000000000000000000033521463463503200234670ustar00rootroot00000000000000"""Tests camera and system functions.""" from unittest import mock from unittest import IsolatedAsyncioTestCase import pytest from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkLotus from blinkpy.camera import BlinkDoorbell @mock.patch("blinkpy.auth.Auth.query") class TestBlinkDoorbell(IsolatedAsyncioTestCase): """Test BlinkDoorbell functions in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") response = { "name": "test", "id": 3, "serial": "test123", "enabled": True, "network_id": 1, "thumbnail": "/foo/bar", } self.blink.homescreen = {"doorbells": [response]} self.blink.sync["test"] = BlinkLotus(self.blink, "test", "1234", response) self.blink.sync["test"].network_info = {"network": {"armed": True}} def tearDown(self): """Clean up after test.""" self.blink = None def test_sync_attributes(self, mock_resp): """Test sync attributes.""" self.assertEqual(self.blink.sync["test"].attributes["name"], "test") self.assertEqual(self.blink.sync["test"].attributes["network_id"], "1234") @pytest.mark.asyncio async def test_lotus_start(self, mock_resp): """Test doorbell instantiation.""" self.blink.last_refresh = None lotus = self.blink.sync["test"] self.assertTrue(await lotus.start()) self.assertTrue("test" in lotus.cameras) self.assertEqual(lotus.cameras["test"].__class__, BlinkDoorbell) fronzbot-blinkpy-098d43b/tests/test_errors.py000066400000000000000000000010041463463503200214520ustar00rootroot00000000000000"""Test blink Utils errors.""" import unittest from blinkpy.helpers.errors import ( USERNAME, PASSWORD, AUTH_TOKEN, AUTHENTICATE, REQUEST, BLINK_ERRORS, ) class TestBlinkUtilsErrors(unittest.TestCase): """Test BlinkUtilErros functions in blinkpy.""" def test_helpers_errors(self) -> None: """Test the helper errors.""" assert USERNAME assert PASSWORD assert AUTH_TOKEN assert AUTHENTICATE assert REQUEST assert BLINK_ERRORS fronzbot-blinkpy-098d43b/tests/test_mini_as_sync.py000066400000000000000000000033431463463503200226210ustar00rootroot00000000000000"""Tests camera and system functions.""" from unittest import mock from unittest import IsolatedAsyncioTestCase import pytest from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkOwl from blinkpy.camera import BlinkCameraMini @mock.patch("blinkpy.auth.Auth.query") class TestBlinkSyncModule(IsolatedAsyncioTestCase): """Test BlinkSyncModule functions in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") response = { "name": "test", "id": 2, "serial": "foobar123", "enabled": True, "network_id": 1, "thumbnail": "/foo/bar", } self.blink.homescreen = {"owls": [response]} self.blink.sync["test"] = BlinkOwl(self.blink, "test", "1234", response) self.blink.sync["test"].network_info = {"network": {"armed": True}} def tearDown(self): """Clean up after test.""" self.blink = None def test_sync_attributes(self, mock_resp): """Test sync attributes.""" self.assertEqual(self.blink.sync["test"].attributes["name"], "test") self.assertEqual(self.blink.sync["test"].attributes["network_id"], "1234") @pytest.mark.asyncio async def test_owl_start(self, mock_resp): """Test owl camera instantiation.""" self.blink.last_refresh = None owl = self.blink.sync["test"] self.assertTrue(await owl.start()) self.assertTrue("test" in owl.cameras) self.assertEqual(owl.cameras["test"].__class__, BlinkCameraMini) fronzbot-blinkpy-098d43b/tests/test_sync_functions.py000066400000000000000000000211571463463503200232150ustar00rootroot00000000000000"""Tests camera and system functions.""" import json from unittest import mock from unittest import IsolatedAsyncioTestCase from random import shuffle import pytest from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkSyncModule from blinkpy.camera import BlinkCamera, BlinkCameraMini, BlinkDoorbell @mock.patch("blinkpy.auth.Auth.query") class TestBlinkSyncModule(IsolatedAsyncioTestCase): """Test BlinkSyncModule functions in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", []) self.camera = BlinkCamera(self.blink.sync) self.mock_start = [ { "syncmodule": { "id": 1234, "network_id": 5678, "serial": "12345678", "status": "foobar", } }, {"event": True}, {}, {}, None, {"devicestatus": {}}, ] self.blink.sync["test"].network_info = {"network": {"armed": True}} def tearDown(self): """Clean up after test.""" self.blink = None self.camera = None self.mock_start = None @pytest.mark.asyncio async def test_check_new_videos(self, mock_resp): """Test recent video response.""" mock_resp.return_value = { "media": [ { "device_name": "foo", "media": "/foo/bar.mp4", "created_at": "1990-01-01T00:00:00+00:00", } ] } sync_module = self.blink.sync["test"] sync_module.cameras = {"foo": None} sync_module.blink.last_refresh = 0 self.assertEqual(sync_module.motion, {}) self.assertTrue(await sync_module.check_new_videos()) self.assertEqual( sync_module.last_records["foo"], [{"clip": "/foo/bar.mp4", "time": "1990-01-01T00:00:00+00:00"}], ) self.assertEqual(sync_module.motion, {"foo": True}) mock_resp.return_value = {"media": []} self.assertTrue(await sync_module.check_new_videos()) self.assertEqual(sync_module.motion, {"foo": False}) self.assertEqual( sync_module.last_records["foo"], [{"clip": "/foo/bar.mp4", "time": "1990-01-01T00:00:00+00:00"}], ) @pytest.mark.asyncio async def test_check_new_videos_old_date(self, mock_resp): """Test videos return response with old date.""" mock_resp.return_value = { "media": [ { "device_name": "foo", "media": "/foo/bar.mp4", "created_at": "1970-01-01T00:00:00+00:00", } ] } sync_module = self.blink.sync["test"] sync_module.cameras = {"foo": None} sync_module.blink.last_refresh = 1000 self.assertTrue(await sync_module.check_new_videos()) self.assertEqual(sync_module.motion, {"foo": False}) @pytest.mark.asyncio async def test_check_no_motion_if_not_armed(self, mock_resp): """Test that motion detection is not set if module unarmed.""" mock_resp.return_value = { "media": [ { "device_name": "foo", "media": "/foo/bar.mp4", "created_at": "1990-01-01T00:00:00+00:00", } ] } sync_module = self.blink.sync["test"] sync_module.cameras = {"foo": None} sync_module.blink.last_refresh = 1000 self.assertTrue(await sync_module.check_new_videos()) self.assertEqual(sync_module.motion, {"foo": True}) sync_module.network_info = {"network": {"armed": False}} self.assertTrue(await sync_module.check_new_videos()) self.assertEqual(sync_module.motion, {"foo": False}) @pytest.mark.asyncio async def test_check_multiple_videos(self, mock_resp): """Test motion found even with multiple videos.""" mock_resp.return_value = { "media": [ { "device_name": "foo", "media": "/foo/bar.mp4", "created_at": "1970-01-01T00:00:00+00:00", }, { "device_name": "foo", "media": "/bar/foo.mp4", "created_at": "1990-01-01T00:00:00+00:00", }, { "device_name": "foo", "media": "/foobar.mp4", "created_at": "1970-01-01T00:00:01+00:00", }, ] } sync_module = self.blink.sync["test"] sync_module.cameras = {"foo": None} sync_module.blink.last_refresh = 1000 self.assertTrue(await sync_module.check_new_videos()) self.assertEqual(sync_module.motion, {"foo": True}) expected_result = { "foo": [{"clip": "/bar/foo.mp4", "time": "1990-01-01T00:00:00+00:00"}] } self.assertEqual(sync_module.last_records, expected_result) @pytest.mark.asyncio async def test_sync_start(self, mock_resp): """Test sync start function.""" mock_resp.side_effect = self.mock_start await self.blink.sync["test"].start() self.assertEqual(self.blink.sync["test"].name, "test") self.assertEqual(self.blink.sync["test"].sync_id, 1234) self.assertEqual(self.blink.sync["test"].network_id, 5678) self.assertEqual(self.blink.sync["test"].serial, "12345678") self.assertEqual(self.blink.sync["test"].status, "foobar") @pytest.mark.asyncio async def test_sync_with_mixed_cameras(self, mock_resp): """Test sync module with mixed cameras attached.""" resp_sync = { "syncmodule": { "network_id": 1234, "id": 1, "serial": 456, "status": "onboarded", } } resp_network_info = {"network": {"sync_module_error": False}} resp_videos = {"media": []} resp_empty = {} self.blink.sync["test"].camera_list = [ {"name": "foo", "id": 10, "type": "default"}, {"name": "bar", "id": 11, "type": "mini"}, {"name": "fake", "id": 12, "type": "doorbell"}, ] self.blink.homescreen = { "owls": [{"name": "bar", "id": 3}], "doorbells": [{"name": "fake", "id": 12}], } side_effect = [ resp_sync, resp_network_info, resp_videos, resp_empty, resp_empty, resp_empty, resp_empty, resp_empty, resp_empty, ] mock_resp.side_effect = side_effect test_sync = self.blink.sync["test"] self.assertTrue(await test_sync.start()) self.assertEqual(test_sync.cameras["foo"].__class__, BlinkCamera) self.assertEqual(test_sync.cameras["bar"].__class__, BlinkCameraMini) self.assertEqual(test_sync.cameras["fake"].__class__, BlinkDoorbell) # Now shuffle the cameras and see if it still works for i in range(0, 10): shuffle(test_sync.camera_list) mock_resp.side_effect = side_effect self.assertTrue(await test_sync.start()) debug_msg = f"Iteration: {i}, {test_sync.camera_list}" self.assertEqual( test_sync.cameras["foo"].__class__, BlinkCamera, msg=debug_msg ) self.assertEqual( test_sync.cameras["bar"].__class__, BlinkCameraMini, msg=debug_msg ) self.assertEqual( test_sync.cameras["fake"].__class__, BlinkDoorbell, msg=debug_msg ) @pytest.mark.asyncio async def test_init_local_storage(self, mock_resp): """Test initialization of local storage object.""" json_fragment = """{ "sync_modules": [ { "id": 123456, "name": "test", "local_storage_enabled": true, "local_storage_compatible": true, "local_storage_status": "active" } ] }""" self.blink.homescreen = json.loads(json_fragment) await self.blink.sync["test"]._init_local_storage(123456) self.assertTrue(self.blink.sync["test"].local_storage) fronzbot-blinkpy-098d43b/tests/test_sync_module.py000066400000000000000000000641611463463503200224740ustar00rootroot00000000000000"""Tests camera and system functions.""" import datetime import logging from unittest import IsolatedAsyncioTestCase from unittest import mock import aiofiles from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler, to_alphanumeric from blinkpy.sync_module import ( BlinkSyncModule, BlinkOwl, BlinkLotus, LocalStorageMediaItem, ) 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 _LOGGER = logging.getLogger(__name__) logging.basicConfig(filename="blinkpy_test.log", level=logging.DEBUG) _LOGGER.setLevel(logging.DEBUG) @mock.patch("blinkpy.auth.Auth.query", return_value={"status": 200}) class TestBlinkSyncModule(IsolatedAsyncioTestCase): """Test BlinkSyncModule functions in blinkpy.""" def setUp(self): """Set up Blink module.""" self.blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") self.blink.sync["test"]: BlinkSyncModule = BlinkSyncModule( self.blink, "test", "1234", [] ) self.blink.sync["test"].network_info = {"network": {"armed": True}} self.camera: BlinkCamera = BlinkCamera(self.blink.sync) self.mock_start = [ { "syncmodule": { "id": 1234, "network_id": 5678, "serial": "12345678", "status": "foobar", } }, {"event": True}, {}, {}, None, {"devicestatus": {}}, ] def tearDown(self): """Clean up after test.""" self.blink = None self.camera = None self.mock_start = None def test_bad_status(self, mock_resp) -> None: """Check that we mark module unavaiable on bad status.""" self.blink.sync["test"].status = None self.blink.sync["test"].available = True self.assertFalse(self.blink.sync["test"].online) self.assertFalse(self.blink.sync["test"].available) async def test_arm(self, mock_resp) -> None: """Check that we arm and disarm a module.""" self.assertTrue(await self.blink.sync["test"].async_arm(True)) self.assertTrue(await self.blink.sync["test"].async_arm(False)) def test_bad_arm(self, mock_resp) -> None: """Check that we mark module unavaiable if bad arm status.""" self.blink.sync["test"].network_info = None self.blink.sync["test"].available = True self.assertEqual(self.blink.sync["test"].arm, None) self.assertFalse(self.blink.sync["test"].available) self.blink.sync["test"].network_info = {} self.blink.sync["test"].available = True self.assertEqual(self.blink.sync["test"].arm, None) self.assertFalse(self.blink.sync["test"].available) def test_get_unique_info_valid_device(self, mock_resp) -> None: """Check that we get the correct info.""" device = { "enabled": True, "name": "doorbell1", } self.blink.homescreen = {"doorbells": [device], "owls": []} self.assertEqual(self.blink.sync["test"].get_unique_info("doorbell1"), device) def test_get_unique_info_invalid_device(self, mock_resp) -> None: """Check what happens if the devide does not exist.""" device = { "enabled": True, "name": "doorbell1", } self.blink.homescreen = {"doorbells": [device], "owls": []} self.assertEqual(self.blink.sync["test"].get_unique_info("doorbell2"), None) async def test_get_events(self, mock_resp) -> None: """Test get events function.""" mock_resp.return_value = {"event": True} self.assertEqual(await self.blink.sync["test"].get_events(), True) @mock.patch( "blinkpy.api.request_sync_events", mock.AsyncMock(return_value={"BAD_event": True}), ) async def test_get_events_malformed(self, mock_resp) -> None: """Test malformed event message.""" self.assertFalse(await self.blink.sync["test"].get_events()) @mock.patch("blinkpy.sync_module.BlinkSyncModule.get_events") async def test_get_events_fail(self, mock_get, mock_resp) -> None: """Test handling of failed get events function.""" mock_resp.return_value = None mock_get.return_value = None self.assertFalse(await self.blink.sync["test"].get_events()) mock_resp.return_value = {} mock_get.return_value = {} self.assertFalse(await self.blink.sync["test"].get_events()) async def test_get_camera_info(self, mock_resp) -> None: """Test get camera info function.""" mock_resp.return_value = {"camera": ["foobar"]} self.assertEqual( await self.blink.sync["test"].get_camera_info("1234"), "foobar" ) self.assertEqual(self.blink.sync["test"].version, None) async def test_get_camera_info_fail(self, mock_resp) -> None: """Test handling of failed get camera info function.""" mock_resp.return_value = None self.assertEqual(await self.blink.sync["test"].get_camera_info("1"), {}) mock_resp.return_value = {} self.assertEqual(await self.blink.sync["test"].get_camera_info("1"), {}) mock_resp.return_value = {"camera": None} self.assertEqual(await self.blink.sync["test"].get_camera_info("1"), {}) async def test_get_network_info(self, mock_resp) -> None: """Test network retrieval.""" mock_resp.return_value = {"network": {"sync_module_error": False}} self.assertTrue(await self.blink.sync["test"].get_network_info()) mock_resp.return_value = {"network": {"sync_module_error": True}} self.assertFalse(await self.blink.sync["test"].get_network_info()) async def test_get_network_info_failure(self, mock_resp) -> None: """Test failed network retrieval.""" 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.side_effect = None self.assertFalse(await self.blink.sync["test"].get_network_info()) self.assertFalse(self.blink.sync["test"].available) async def test_check_new_videos_startup(self, mock_resp) -> None: """Test that check_new_videos does not block startup.""" sync_module = self.blink.sync["test"] self.blink.last_refresh = None self.assertFalse(await sync_module.check_new_videos()) async def test_check_new_videos_failed(self, mock_resp) -> None: """Test method when response is unexpected.""" generic_entry = { "device_name": "foo", "deleted": True, "media": "/bar.mp4", } result = [generic_entry] mock_resp.return_value = {"media": result} sync_module = self.blink.sync["test"] # I think this should be false - should the exception return False? self.assertTrue(await sync_module.check_new_videos()) mock_resp.side_effect = [None, "just a string", {}] sync_module.cameras = {"foo": None} sync_module.motion["foo"] = True self.assertFalse(await sync_module.check_new_videos()) self.assertFalse(sync_module.motion["foo"]) sync_module.motion["foo"] = True self.assertFalse(await sync_module.check_new_videos()) self.assertFalse(sync_module.motion["foo"]) sync_module.motion["foo"] = True self.assertFalse(await sync_module.check_new_videos()) self.assertFalse(sync_module.motion["foo"]) async def test_unexpected_summary(self, mock_resp) -> None: """Test unexpected summary response.""" self.mock_start[0] = None mock_resp.side_effect = self.mock_start self.assertFalse(await self.blink.sync["test"].start()) async def test_summary_with_no_network_id(self, mock_resp) -> None: """Test handling of bad summary.""" self.mock_start[0]["syncmodule"] = None mock_resp.side_effect = self.mock_start self.assertFalse(await self.blink.sync["test"].start()) async def test_missing_key_startup(self, mock_resp) -> None: """Test for missing key at sync module startup.""" del self.mock_start[0]["syncmodule"]["serial"] mock_resp.side_effect = self.mock_start self.assertFalse(await self.blink.sync["test"].start()) async def test_summary_with_only_network_id(self, mock_resp) -> None: """Test handling of sparse summary.""" self.mock_start[0]["syncmodule"] = {"network_id": 8675309} mock_resp.side_effect = self.mock_start await self.blink.sync["test"].start() self.assertEqual(self.blink.sync["test"].network_id, 8675309) async def test_unexpected_camera_info(self, mock_resp) -> None: """Test unexpected camera info response.""" self.blink.sync["test"].cameras["foo"] = None self.mock_start[5] = None mock_resp.side_effect = self.mock_start await self.blink.sync["test"].start() self.assertEqual(self.blink.sync["test"].cameras, {"foo": None}) async def test_missing_camera_info(self, mock_resp) -> None: """Test missing key from camera info response.""" self.blink.sync["test"].cameras["foo"] = None self.mock_start[5] = {} await self.blink.sync["test"].start() self.assertEqual(self.blink.sync["test"].cameras, {"foo": None}) def test_sync_attributes(self, mock_resp) -> None: """Test sync attributes.""" self.assertEqual(self.blink.sync["test"].attributes["name"], "test") self.assertEqual(self.blink.sync["test"].attributes["network_id"], "1234") async def test_name_not_in_config(self, mock_resp) -> None: """Check that function exits when name not in camera_config.""" test_sync = self.blink.sync["test"] test_sync.camera_list = [{"foo": "bar"}] self.assertTrue(await test_sync.update_cameras()) async def test_camera_config_key_error(self, mock_resp) -> None: """Check that update returns False on KeyError.""" test_sync = self.blink.sync["test"] test_sync.camera_list = [{"name": "foobar"}] self.assertFalse(await test_sync.update_cameras()) @mock.patch( "blinkpy.sync_module.BlinkSyncModule.get_network_info", mock.AsyncMock(return_value=False), ) async def test_refresh_network_info(self, mock_resp) -> None: """Test no network info on refresh.""" self.assertFalse(await self.blink.sync["test"].refresh()) async def test_update_local_storage_manifest(self, mock_resp) -> None: """Test getting the manifest from the sync module.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 mock_resp.side_effect = [ COMMAND_RESPONSE, COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", "clips": [ { "id": "866333964", "size": "234", "camera_name": "BackDoor", "created_at": "2022-12-01T21:11:50+00:00", }, { "id": "1568781420", "size": "430", "camera_name": "FrontDoor", "created_at": "2022-12-01T21:11:22+00:00", }, { "id": "1289590916", "size": "425", "camera_name": "BackDoor", "created_at": "2022-12-01T18:12:26+00:00", }, { "id": "1893118325", "size": "186", "camera_name": "FrontDoor", "created_at": "2022-12-01T11:35:52+00:00", }, { "id": "2358747807", "size": "452", "camera_name": "Yard", "created_at": "2022-12-01T11:34:55+00:00", }, ], }, ] test_sync._names_table[to_alphanumeric("Front Door")] = "Front Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" test_sync._names_table[to_alphanumeric("Yard")] = "Yard" await test_sync.update_local_storage_manifest() self.assertEqual(len(test_sync._local_storage["manifest"]), 5) self.assertEqual( test_sync._local_storage["manifest"][0].url(), "/api/v1/accounts/10111213/networks/1234/sync_modules/1234/local_storage/" + "manifest/4321/clip/request/2358747807", ) self.assertEqual( test_sync._local_storage["manifest"][4].url(), "/api/v1/accounts/10111213/networks/1234/sync_modules/1234/local_storage/" + "manifest/4321/clip/request/866333964", ) async def test_check_new_videos_with_local_storage(self, mock_resp) -> None: """Test checking new videos in local storage.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) created_at = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=60) ).isoformat() mock_resp.side_effect = [ COMMAND_RESPONSE, COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", "clips": [ { "id": "866333964", "size": "234", "camera_name": "BackDoor", "created_at": f"{created_at}", }, { "id": "1568781420", "size": "430", "camera_name": "Front_Door", "created_at": f"{created_at}", }, ], }, {"media": []}, COMMAND_RESPONSE, COMMAND_COMPLETE, COMMAND_RESPONSE, COMMAND_COMPLETE, {"media": []}, COMMAND_RESPONSE, COMMAND_COMPLETE, COMMAND_RESPONSE, COMMAND_COMPLETE, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" await test_sync.update_local_storage_manifest() self.assertTrue(await test_sync.check_new_videos()) self.assertTrue(await test_sync.check_new_videos()) self.assertEqual( test_sync.last_records["Back Door"][0]["clip"], "/api/v1/accounts/10111213/networks/1234/sync_modules/1234/local_storage/" + "manifest/4321/clip/request/866333964", ) self.assertEqual( test_sync.last_records["Front_Door"][0]["clip"], "/api/v1/accounts/10111213/networks/1234/sync_modules/1234/local_storage/" + "manifest/4321/clip/request/1568781420", ) @mock.patch("blinkpy.sync_module.BlinkSyncModule.poll_local_storage_manifest") # Need to mock out poll_local_storage_manifest due to retries timing out test async def test_check_no_missing_id_with_update_local_storage_manifest( self, mock_poll, mock_resp ) -> None: """Test checking missing ID in local storage update.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) mock_poll.return_value = [ COMMAND_RESPONSE, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" self.assertIsNone(await test_sync.update_local_storage_manifest()) async def test_check_missing_manifest_id_with_update_local_storage_manifest( self, mock_resp ) -> None: """Test checking missing manifest in update.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) created_at = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=60) ).isoformat() mock_resp.side_effect = [ COMMAND_RESPONSE, COMMAND_COMPLETE, { "version": "1.0", "clips": [ { "id": "866333964", "size": "234", "camera_name": "BackDoor", "created_at": f"{created_at}", }, { "id": "1568781420", "size": "430", "camera_name": "Front_Door", "created_at": f"{created_at}", }, ], }, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" self.assertIsNone(await test_sync.update_local_storage_manifest()) async def test_check_malformed_clips_with_update_local_storage_manifest( self, mock_resp ) -> None: """Test checking malformed clips in update.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) created_at = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=60) ).isoformat() mock_resp.side_effect = [ COMMAND_RESPONSE, COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", "clips": [ { "id": "866333964", "camera_name": "BackDoor", "created_at": f"{created_at}", }, { "id": "1568781420", "size": "430", "camera_name": "Front_Door", "created_at": f"{created_at}", }, ], }, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" self.assertIsNone(await test_sync.update_local_storage_manifest()) async def test_check_poll_local_storage_manifest_retry(self, mock_resp) -> None: """Test checking poll local storage manifest retry logic.""" self.blink.account_id = 10111213 test_sync = self.blink.sync["test"] test_sync._local_storage["status"] = True test_sync.sync_id = 1234 test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) mock_resp.side_effect = [ {"bad": "stuff"}, # bad command response, fall back to retry logic COMMAND_RESPONSE, COMMAND_COMPLETE, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" response = await test_sync.poll_local_storage_manifest(max_retries=2) self.assertEqual( response, COMMAND_RESPONSE, ) async def test_sync_owl_init(self, mock_resp): """Test sync owl setup with no serial in response.""" self.blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") response_value = {"id": 489371591, "enabled": 123456, "serial": None} test = BlinkOwl(self.blink, "test", "1234", response=response_value) self.assertIsNotNone(test.serial) self.blink.homescreen = {"owls": {"enabled": True}} self.assertIsNone(await test.get_camera_info("test")) async def test_sync_lotus_init(self, mock_resp): """Test sync lotus setup with no serial in response.""" self.blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) self.blink.last_refresh = 0 self.blink.urls = BlinkURLHandler("test") response_value = {"id": 489371591, "enabled": 123456, "serial": None} test = BlinkLotus(self.blink, "test", "1234", response=response_value) self.assertIsNotNone(test.serial) self.blink.homescreen = {"doorbells": {"enabled": True}} self.assertIsNone(await test.get_camera_info("test")) async def test_local_storage_media_item(self, mock_resp): """Test local storage media properties.""" blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) blink.last_refresh = 0 blink.urls = BlinkURLHandler("test") item = LocalStorageMediaItem( "1234", "Backdoor", datetime.datetime.utcnow().isoformat(), "432", " manifest_id", "url", ) item2 = LocalStorageMediaItem( "1235", "Backdoor", datetime.datetime.utcnow().isoformat(), "432", " manifest_id", "url", ) self.assertEqual(item.id, 1234) self.assertEqual(item.size, "432") self.assertFalse(item == item2) mock_resp.side_effect = [ COMMAND_RESPONSE, COMMAND_COMPLETE, ] self.assertEqual( await item.prepare_download(blink, max_retries=1), COMMAND_RESPONSE, ) with mock.patch("blinkpy.api.http_post", return_value=""): self.assertIsNone(await item2.prepare_download(blink, max_retries=0)) async def test_poll_local_storage_manifest(self, mock_resp): """Test incorrect response.""" with mock.patch("blinkpy.api.request_local_storage_manifest", return_value=""): self.assertIsNone( await self.blink.sync["test"].poll_local_storage_manifest(max_retries=0) ) async def test_delete_video(self, mock_resp): """Test item delete.""" blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) blink.last_refresh = 0 blink.urls = BlinkURLHandler("test") item = LocalStorageMediaItem( "1234", "Backdoor", datetime.datetime.utcnow().isoformat(), "432", " manifest_id", "url", ) mock_resp.return_value = mresp.MockResponse({"status": 200}, 200) self.assertTrue(await item.delete_video(blink)) mock_resp.return_value = mresp.MockResponse({"status": 400}, 400) self.assertFalse(await item.delete_video(blink, 1)) async def test_download_video(self, mock_resp): """Test item download.""" blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) blink.last_refresh = 0 blink.urls = BlinkURLHandler("test") item = LocalStorageMediaItem( "1234", "Backdoor", datetime.datetime.utcnow().isoformat(), "432", " manifest_id", "url", ) mock_file = mock.MagicMock() aiofiles.threadpool.wrap.register(mock.MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( *args, **kwargs ) ) with mock.patch("aiofiles.threadpool.sync_open", return_value=mock_file): mock_resp.return_value = mresp.MockResponse({"status": 200}, 200) self.assertTrue(await item.download_video(blink, "filename.mp4")) mock_resp.return_value = mresp.MockResponse({"status": 400}, 400) self.assertFalse(await item.download_video(blink, "filename.mp4", 1)) @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.download_video") @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.delete_video") @mock.patch("blinkpy.sync_module.LocalStorageMediaItem.prepare_download") async def test_download_delete(self, mock_prepdl, mock_del, mock_dl, mock_resp): """Test download and delete.""" blink: Blink = Blink(motion_interval=0, session=mock.AsyncMock()) blink.last_refresh = 0 blink.urls = BlinkURLHandler("test") item = LocalStorageMediaItem( "1234", "Backdoor", datetime.datetime.utcnow().isoformat(), "432", " manifest_id", "url", ) self.assertTrue(await item.download_video_delete(self.blink, "filename.mp4")) mock_prepdl.return_value = False self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) mock_prepdl.return_value = mock.AsyncMock() mock_del.return_value = False self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) mock_del.return_value = mock.AsyncMock() mock_dl.return_value = False self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) fronzbot-blinkpy-098d43b/tests/test_util.py000066400000000000000000000161621463463503200211260ustar00rootroot00000000000000"""Test various api functions.""" from unittest import mock, IsolatedAsyncioTestCase import time import aiofiles from io import BufferedIOBase from blinkpy.helpers.util import ( json_load, json_save, Throttle, time_to_seconds, gen_uid, get_time, merge_dicts, backoff_seconds, BlinkException, ) from blinkpy.helpers import constants as const class TestUtil(IsolatedAsyncioTestCase): """Test the helpers/util module.""" def setUp(self): """Initialize the blink module.""" def tearDown(self): """Tear down blink module.""" async def test_throttle(self): """Test the throttle decorator.""" calls = [] @Throttle(seconds=5) async def test_throttle(force=False): calls.append(1) now = int(time.time()) # First call should fire await test_throttle() self.assertEqual(1, len(calls)) # Call again, still should fire with delay await test_throttle() self.assertEqual(2, len(calls)) assert int(time.time()) - now >= 5 # Call with force await test_throttle(force=True) self.assertEqual(3, len(calls)) # Call without throttle, fire with delay now = int(time.time()) await test_throttle() self.assertEqual(4, len(calls)) assert int(time.time()) - now >= 5 async def test_throttle_per_instance(self): """Test that throttle is done once per instance of class.""" class Tester: """A tester class for throttling.""" async def test(self): """Test the throttle.""" return True tester = Tester() throttled = Throttle(seconds=1)(tester.test) now = int(time.time()) self.assertEqual(await throttled(), True) self.assertEqual(await throttled(), True) assert int(time.time()) - now >= 1 async def test_throttle_multiple_objects(self): """Test that function is throttled even if called by multiple objects.""" @Throttle(seconds=5) async def test_throttle_method(): return True class Tester: """A tester class for throttling.""" def test(self): """Test function for throttle.""" return test_throttle_method() tester1 = Tester() tester2 = Tester() now = int(time.time()) self.assertEqual(await tester1.test(), True) self.assertEqual(await tester2.test(), True) assert int(time.time()) - now >= 5 async def test_throttle_on_two_methods(self): """Test that throttle works for multiple methods.""" class Tester: """A tester class for throttling.""" @Throttle(seconds=3) async def test1(self): """Test function for throttle.""" return True @Throttle(seconds=5) async def test2(self): """Test function for throttle.""" return True tester = Tester() now = int(time.time()) self.assertEqual(await tester.test1(), True) self.assertEqual(await tester.test2(), True) self.assertEqual(await tester.test1(), True) assert int(time.time()) - now >= 3 self.assertEqual(await tester.test2(), True) assert int(time.time()) - now >= 5 def test_time_to_seconds(self): """Test time to seconds conversion.""" correct_time = "1970-01-01T00:00:05+00:00" wrong_time = "1/1/1970 00:00:03" self.assertEqual(time_to_seconds(correct_time), 5) self.assertFalse(time_to_seconds(wrong_time)) async def test_json_save(self): """Check that the file is saved.""" mock_file = mock.MagicMock() aiofiles.threadpool.wrap.register(mock.MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( *args, **kwargs ) ) with mock.patch( "aiofiles.threadpool.sync_open", return_value=mock_file ) as mock_open: await json_save('{"test":1,"test2":2}', "face.file") mock_open.assert_called_once() async def test_json_load_data(self): """Check that bad file is handled.""" filename = "fake.file" aiofiles.threadpool.wrap.register(mock.MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( *args, **kwargs ) ) self.assertEqual(await json_load(filename), None) mock_file = mock.MagicMock(spec=BufferedIOBase) mock_file.name = filename mock_file.read.return_value = '{"some data":"more"}' with mock.patch("aiofiles.threadpool.sync_open", return_value=mock_file): self.assertNotEqual(await json_load(filename), None) async def test_json_load_bad_data(self): """Check that bad file is handled.""" self.assertEqual(await json_load("fake.file"), None) filename = "fake.file" aiofiles.threadpool.wrap.register(mock.MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase( *args, **kwargs ) ) self.assertEqual(await json_load(filename), None) mock_file = mock.MagicMock(spec=BufferedIOBase) mock_file.name = filename mock_file.read.return_value = "" with mock.patch("aiofiles.threadpool.sync_open", return_value=mock_file): self.assertEqual(await json_load("fake.file"), None) def test_gen_uid(self): """Test gen_uid formatting.""" val1 = gen_uid(8) val2 = gen_uid(8, uid_format=True) self.assertEqual(len(val1), 16) self.assertTrue(val2.startswith("BlinkCamera_")) val2_cut = val2.split("_") val2_split = val2_cut[1].split("-") self.assertEqual(len(val2_split[0]), 8) self.assertEqual(len(val2_split[1]), 4) self.assertEqual(len(val2_split[2]), 4) self.assertEqual(len(val2_split[3]), 4) self.assertEqual(len(val2_split[4]), 12) def test_get_time(self): """Test the get time util.""" self.assertEqual( get_time(), time.strftime(const.TIMESTAMP_FORMAT, time.gmtime(time.time())) ) def test_merge_dicts(self): """Test for duplicates message in merge dicts.""" dict_A = {"key1": "value1", "key2": "value2"} dict_B = {"key1": "value1"} expected_log = [ "WARNING:blinkpy.helpers.util:Duplicates found during merge: ['key1']. " "Renaming is recommended." ] with self.assertLogs(level="DEBUG") as merge_log: merge_dicts(dict_A, dict_B) self.assertListEqual(merge_log.output, expected_log) def test_backoff_seconds(self): """Test the backoff seconds function.""" self.assertNotEqual(backoff_seconds(), None) def test_blink_exception(self): """Test the Blink Exception class.""" test_exception = BlinkException([1, "No good"]) self.assertEqual(test_exception.errid, 1) self.assertEqual(test_exception.message, "No good") fronzbot-blinkpy-098d43b/tox.ini000066400000000000000000000022261463463503200167050ustar00rootroot00000000000000[tox] envlist = build, py39, py310, py311, py312, lint skip_missing_interpreters = True skipsdist = True [testenv] setenv = LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = pytest --timeout=30 --durations=10 --cov=blinkpy --cov-report term-missing {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt [testenv:cov] setenv = LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = pip install -e . pytest --timeout=30 --durations=10 --cov=blinkpy --cov-report=xml {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt [testenv:lint] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt basepython = python3 commands = ruff check blinkpy tests blinkapp black --check --color --diff blinkpy tests blinkapp rst-lint README.rst CHANGES.rst CONTRIBUTING.rst [testenv:build] recreate = True skip_install = True allowlist_externals = /bin/sh /bin/rm deps = -r{toxinidir}/requirements_test.txt commands = /bin/rm -rf build dist python -m build /bin/sh -c "pip install --upgrade dist/*.whl" py.test tests