Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
Finish out the first iteration of the library (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
bachya authored Oct 30, 2019
1 parent 25b7906 commit 56367a9
Show file tree
Hide file tree
Showing 15 changed files with 1,270 additions and 29 deletions.
11 changes: 11 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[run]
source = eufy_security

omit =
eufy_security/__version__.py

[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
tags
Pipfile.lock
.coverage
.mypy_cache/
coverage.xml
12 changes: 6 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
clean:
pipenv --rm
coverage:
pipenv run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=aiowwlln tests
pipenv run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=eufy_security tests
init:
pip3 install --upgrade pip pipenv
pipenv lock
pipenv install --three --dev
pipenv run pre-commit install
lint:
pipenv run flake8 aiowwlln
pipenv run pydocstyle aiowwlln
pipenv run pylint aiowwlln
pipenv run flake8 eufy_security
pipenv run pydocstyle eufy_security
pipenv run pylint eufy_security
publish:
pipenv run python setup.py sdist bdist_wheel
pipenv run twine upload dist/*
rm -rf dist/ build/ .egg aiowwlln.egg-info/
rm -rf dist/ build/ .egg eufy_security.egg-info/
test:
pipenv run py.test
typing:
pipenv run mypy --ignore-missing-imports aiowwlln
pipenv run mypy --ignore-missing-imports eufy_security
85 changes: 83 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,85 @@
# python-eufy-security
Python library for Eufy Security cameras

Very much a work in progress. Will be used for integration into HomeAssistant.
This is an experimental Python library for Eufy Security devices (cameras, doorbells,
etc.).

# Python Versions

The library is currently supported on

* Python 3.6
* Python 3.7

# Installation

TBD

# Usage

Everything starts with an:
[aiohttp](https://aiohttp.readthedocs.io/en/stable/) `ClientSession`:

```python
import asyncio

from aiohttp import ClientSession


async def main() -> None:
"""Create the aiohttp session and run the example."""
async with ClientSession() as websession:
# YOUR CODE HERE


asyncio.get_event_loop().run_until_complete(main())
```

Login and get to work:

```python
import asyncio

from aiohttp import ClientSession

from eufy_security import async_login


async def main() -> None:
"""Create the aiohttp session and run the example."""
async with ClientSession() as websession:
# Create an API client:
api = await async_login(EUFY_EMAIL, EUFY_PASSWORD, websession)

# Loop through the cameras associated with the account:
for camera in api.cameras.values():
print("------------------")
print("Camera Name: %s", camera.name)
print("Serial Number: %s", camera.serial)
print("Station Serial Number: %s", camera.station_serial)
print("Last Camera Image URL: %s", camera.last_camera_image_url)

print("Starting RTSP Stream")
stream_url = await camera.async_start_stream()
print("Stream URL: %s", stream_url)

print("Stopping RTSP Stream")
stream_url = await camera.async_stop_stream()


asyncio.get_event_loop().run_until_complete(main())
```

Check out `example.py`, the tests, and the source files themselves for method
signatures and more examples.

# Contributing

1. [Check for open features/bugs](https://github.com/FuzzyMistborn/python-eufy-security/issues)
or [initiate a discussion on one](https://github.com/FuzzyMistborn/python-eufy-security/issues/new).
2. [Fork the repository](https://github.com/FuzzyMistborn/python-eufy-security/fork).
3. Install the dev environment: `make init`.
4. Enter the virtual environment: `pipenv shell`
5. Code your new feature or bug fix.
6. Write a test that covers your new functionality.
7. Run tests and ensure 100% code coverage: `make coverage`
8. Submit a pull request!
2 changes: 2 additions & 0 deletions eufy_security/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Define the package version."""
VERSION: str = "0.0.1"
28 changes: 19 additions & 9 deletions eufy_security/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from aiohttp.client_exceptions import ClientError

from .camera import Camera
from .errors import ExpiredTokenError, InvalidCredentialsError, RequestError
from .errors import RequestError, raise_error

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,13 +40,19 @@ async def async_authenticate(self) -> None:
auth_resp["data"]["token_expires_at"]
)

async def async_get_history(self) -> dict:
"""Get the camera's history."""
history_resp = await self.request("post", "event/app/get_all_history_record")

return history_resp["data"]

async def async_update_device_info(self) -> None:
"""Get the latest device info."""
devices_resp = await self.request("post", "app/get_devs_list")

for device_info in devices_resp["data"]:
if device_info["device_sn"] in self.cameras:
camera = self.cameras[device_info["device_info"]]
camera = self.cameras[device_info["device_sn"]]
camera.camera_info = device_info
continue

Expand Down Expand Up @@ -78,25 +84,29 @@ async def request(
method, url, headers=headers, json=json
) as resp:
try:
resp.raise_for_status()
data: dict = await resp.json(content_type=None)

if not data:
raise RequestError(f"No response while requesting {endpoint}")

if data["code"] != 0:
raise RequestError(
f"There was an error while requesting {endpoint}: {data['msg']}"
)
_raise_on_error(data)

return data
except ClientError as err:
if "401" in str(err):
raise InvalidCredentialsError("Invalid credentials")
raise RequestError(
f"There was an unknown error while requesting {endpoint}"
f"There was an unknown error while requesting {endpoint}: {err}"
)


def _raise_on_error(data: dict) -> None:
"""Raise appropriately when a returned data payload contains an error."""
if data["code"] == 0:
return

raise_error(data)


async def async_login(email: str, password: str, websession: ClientSession) -> API:
"""Return an authenticated API object."""
api: API = API(email, password, websession)
Expand Down
9 changes: 1 addition & 8 deletions eufy_security/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .api import API
from .api import API # pylint: disable=cyclic-import

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -36,13 +36,6 @@ def station_serial(self) -> str:
"""Return the camera's station serial number."""
return self.camera_info["station_sn"]

async def async_get_history(self) -> dict:
"""Get the camera's history."""
history_resp = await self._api.request(
"post", "event/app/get_all_history_record"
)
return history_resp["data"]

async def async_start_stream(self) -> str:
"""Start the camera stream and return the RTSP URL."""
start_resp = await self._api.request(
Expand Down
10 changes: 10 additions & 0 deletions eufy_security/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Define package errors."""
from typing import Dict, Type


class EufySecurityError(Exception):
Expand All @@ -17,3 +18,12 @@ class RequestError(EufySecurityError):
"""Define an error related to invalid requests."""

pass


ERRORS: Dict[int, Type[EufySecurityError]] = {26006: InvalidCredentialsError}


def raise_error(data: dict) -> None:
"""Raise the appropriate error based upon a response code."""
cls = ERRORS.get(data["code"], EufySecurityError)
raise cls(data["msg"])
23 changes: 19 additions & 4 deletions example.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
"""Run an example script to quickly test."""
import asyncio
import logging

from aiohttp import ClientSession

from eufy_security import async_login
from eufy_security.errors import EufySecurityError

EUFY_EMAIL = "<EMAIL>"
EUFY_PASSWORD = "<PASSWORD>"
_LOGGER: logging.Logger = logging.getLogger()

EUFY_EMAIL: str = "<EMAIL>"
EUFY_PASSWORD: str = "<PASSWORD>"


async def main() -> None:
"""Create the aiohttp session and run the example."""
logging.basicConfig(level=logging.INFO)
async with ClientSession() as websession:
try:
# Create an API client:
api = await async_login(EUFY_EMAIL, EUFY_PASSWORD, websession)

# Loop through the cameras associated with the account:
for camera in api.cameras.values():
print(camera.name)
_LOGGER.info("------------------")
_LOGGER.info("Camera Name: %s", camera.name)
_LOGGER.info("Serial Number: %s", camera.serial)
_LOGGER.info("Station Serial Number: %s", camera.station_serial)
_LOGGER.info("Last Camera Image URL: %s", camera.last_camera_image_url)

_LOGGER.info("Starting RTSP Stream")
stream_url = await camera.async_start_stream()
_LOGGER.info("Stream URL: %s", stream_url)

_LOGGER.info("Stopping RTSP Stream")
stream_url = await camera.async_stop_stream()
except EufySecurityError as err:
print(f"There was an error: {err}")
print(f"There was a/an {type(err)} error: {err}")


asyncio.get_event_loop().run_until_complete(main())
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Define the test suite."""
4 changes: 4 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Define constants to use in tests."""
TEST_ACCESS_TOKEN = "abcde12345"
TEST_EMAIL = "user@host.com"
TEST_PASSWORD = "password12345"
Loading

0 comments on commit 56367a9

Please sign in to comment.