Updated project structure and some file names.

Restored `requirements.txt`

Updated `setup.py` to include new repository url and contact details.

Moved the rtsp code from `record` to `stream`.

Updated project structure to make it more readable and developer friendly - moved mixins to the `mixins` package, moved handlers to the `handlers` package.

Moved files not belonging to anything in particular to the `util` package.

Updated `camera` class to also defer login call.

Deleted unused files like `config_handler`.
This commit is contained in:
Alano Terblanche
2020-12-19 19:55:12 +02:00
parent 4c4dd7dd69
commit 2b3e142fe5
27 changed files with 117 additions and 95 deletions

View File

@@ -2,9 +2,9 @@
<p align="center"> <p align="center">
<img alt="Reolink Approval" src="https://img.shields.io/badge/reolink-approved-blue?style=flat-square"> <img alt="Reolink Approval" src="https://img.shields.io/badge/reolink-approved-blue?style=flat-square">
<img alt="GitHub" src="https://img.shields.io/github/license/ReolinkCameraApi/reolink-python-api?style=flat-square"> <img alt="GitHub" src="https://img.shields.io/github/license/ReolinkCameraAPI/reolinkapipy?style=flat-square">
<img alt="GitHub tag (latest SemVer)" src="https://img.shields.io/github/v/tag/ReolinkCameraApi/reolink-python-api?style=flat-square"> <img alt="GitHub tag (latest SemVer)" src="https://img.shields.io/github/v/tag/ReolinkCameraAPI/reolinkapipy?style=flat-square">
<img alt="PyPI" src="https://img.shields.io/pypi/v/reolink-api?style=flat-square"> <img alt="PyPI" src="https://img.shields.io/pypi/v/reolinkapi?style=flat-square">
<img alt="Discord" src="https://img.shields.io/discord/773257004911034389?style=flat-square"> <img alt="Discord" src="https://img.shields.io/discord/773257004911034389?style=flat-square">
</p> </p>

11
examples/basic_usage.py Normal file
View File

@@ -0,0 +1,11 @@
import reolinkapi
if __name__ == "__main__":
cam = reolinkapi.Camera("192.168.0.102", defer_login=True)
# must first login since I defer have deferred the login process
cam.login()
dst = cam.get_dst()
ok = cam.add_user("foo", "bar", "admin")
alarm = cam.get_alarm_motion()

View File

@@ -1,4 +1,4 @@
from .api_handler import APIHandler from reolinkapi.handlers.api_handler import APIHandler
from .camera import Camera from .camera import Camera
__version__ = "0.1.2" __version__ = "0.1.2"

View File

@@ -1,19 +1,31 @@
from .api_handler import APIHandler from reolinkapi.handlers.api_handler import APIHandler
class Camera(APIHandler): class Camera(APIHandler):
def __init__(self, ip: str, username: str = "admin", password: str = "", https: bool = False): def __init__(self, ip: str,
username: str = "admin",
password: str = "",
https: bool = False,
defer_login: bool = False,
**kwargs):
""" """
Initialise the Camera object by passing the ip address. Initialise the Camera object by passing the ip address.
The default details {"username":"admin", "password":""} will be used if nothing passed The default details {"username":"admin", "password":""} will be used if nothing passed
For deferring the login to the camera, just pass defer_login = True.
For connecting to the camera behind a proxy pass a proxy argument: proxy={"http": "socks5://127.0.0.1:8000"}
:param ip: :param ip:
:param username: :param username:
:param password: :param password:
:param https: connect to the camera over https
:param defer_login: defer the login process
:param proxy: Add a proxy dict for requests to consume.
eg: {"http":"socks5://[username]:[password]@[host]:[port], "https": ...}
More information on proxies in requests: https://stackoverflow.com/a/15661226/9313679
""" """
# For when you need to connect to a camera behind a proxy, pass # For when you need to connect to a camera behind a proxy, pass
# a proxy argument: proxy={"http": "socks5://127.0.0.1:8000"} # a proxy argument: proxy={"http": "socks5://127.0.0.1:8000"}
APIHandler.__init__(self, ip, username, password, https=https) APIHandler.__init__(self, ip, username, password, https=https, **kwargs)
# Normal call without proxy: # Normal call without proxy:
# APIHandler.__init__(self, ip, username, password) # APIHandler.__init__(self, ip, username, password)
@@ -21,4 +33,6 @@ class Camera(APIHandler):
self.ip = ip self.ip = ip
self.username = username self.username = username
self.password = password self.password = password
super().login()
if not defer_login:
super().login()

View File

@@ -1,17 +0,0 @@
import io
import yaml
from typing import Optional, Dict
class ConfigHandler:
camera_settings = {}
@staticmethod
def load() -> Optional[Dict]:
try:
stream = io.open("config.yml", 'r', encoding='utf8')
data = yaml.safe_load(stream)
return data
except Exception as e:
print("Config Property Error\n", e)
return None

View File

View File

@@ -1,18 +1,19 @@
import requests import requests
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from reolinkapi.alarm import AlarmAPIMixin from reolinkapi.mixins.alarm import AlarmAPIMixin
from reolinkapi.device import DeviceAPIMixin from reolinkapi.mixins.device import DeviceAPIMixin
from reolinkapi.display import DisplayAPIMixin from reolinkapi.mixins.display import DisplayAPIMixin
from reolinkapi.download import DownloadAPIMixin from reolinkapi.mixins.download import DownloadAPIMixin
from reolinkapi.image import ImageAPIMixin from reolinkapi.mixins.image import ImageAPIMixin
from reolinkapi.motion import MotionAPIMixin from reolinkapi.mixins.motion import MotionAPIMixin
from reolinkapi.network import NetworkAPIMixin from reolinkapi.mixins.network import NetworkAPIMixin
from reolinkapi.ptz import PtzAPIMixin from reolinkapi.mixins.ptz import PtzAPIMixin
from reolinkapi.recording import RecordingAPIMixin from reolinkapi.mixins.record import RecordAPIMixin
from reolinkapi.resthandle import Request from reolinkapi.handlers.rest_handler import Request
from reolinkapi.system import SystemAPIMixin from reolinkapi.mixins.stream import StreamAPIMixin
from reolinkapi.user import UserAPIMixin from reolinkapi.mixins.system import SystemAPIMixin
from reolinkapi.zoom import ZoomAPIMixin from reolinkapi.mixins.user import UserAPIMixin
from reolinkapi.mixins.zoom import ZoomAPIMixin
class APIHandler(AlarmAPIMixin, class APIHandler(AlarmAPIMixin,
@@ -23,10 +24,11 @@ class APIHandler(AlarmAPIMixin,
MotionAPIMixin, MotionAPIMixin,
NetworkAPIMixin, NetworkAPIMixin,
PtzAPIMixin, PtzAPIMixin,
RecordingAPIMixin, RecordAPIMixin,
SystemAPIMixin, SystemAPIMixin,
UserAPIMixin, UserAPIMixin,
ZoomAPIMixin): ZoomAPIMixin,
StreamAPIMixin):
""" """
The APIHandler class is the backend part of the API, the actual API calls The APIHandler class is the backend part of the API, the actual API calls
are implemented in Mixins. are implemented in Mixins.
@@ -42,6 +44,7 @@ class APIHandler(AlarmAPIMixin,
:param ip: :param ip:
:param username: :param username:
:param password: :param password:
:param https: connect over https
:param proxy: Add a proxy dict for requests to consume. :param proxy: Add a proxy dict for requests to consume.
eg: {"http":"socks5://[username]:[password]@[host]:[port], "https": ...} eg: {"http":"socks5://[username]:[password]@[host]:[port], "https": ...}
More information on proxies in requests: https://stackoverflow.com/a/15661226/9313679 More information on proxies in requests: https://stackoverflow.com/a/15661226/9313679

View File

View File

@@ -1,15 +1,8 @@
import requests from typing import Dict
import random
import string
from urllib import parse
from io import BytesIO
from typing import Dict, Any, Optional
from PIL.Image import Image, open as open_image
from reolinkapi.rtsp_client import RtspClient
class RecordingAPIMixin: class RecordAPIMixin:
"""API calls for recording/streaming image or video.""" """API calls for the recording settings"""
def get_recording_encoding(self) -> Dict: def get_recording_encoding(self) -> Dict:
""" """
@@ -77,44 +70,3 @@ class RecordingAPIMixin:
} }
] ]
return self._execute_command('SetEnc', body) return self._execute_command('SetEnc', body)
###########
# RTSP Stream
###########
def open_video_stream(self, callback: Any = None, proxies: Any = None) -> Any:
"""
'https://support.reolink.com/hc/en-us/articles/360007010473-How-to-Live-View-Reolink-Cameras-via-VLC-Media-Player'
Blocking function creates a generator and returns the frames as it is spawned
:param callback:
:param proxies: Default is none, example: {"host": "localhost", "port": 8000}
"""
rtsp_client = RtspClient(
ip=self.ip, username=self.username, password=self.password, proxies=proxies, callback=callback)
return rtsp_client.open_stream()
def get_snap(self, timeout: float = 3, proxies: Any = None) -> Optional[Image]:
"""
Gets a "snap" of the current camera video data and returns a Pillow Image or None
:param timeout: Request timeout to camera in seconds
:param proxies: http/https proxies to pass to the request object.
:return: Image or None
"""
data = {
'cmd': 'Snap',
'channel': 0,
'rs': ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)),
'user': self.username,
'password': self.password,
}
parms = parse.urlencode(data).encode("utf-8")
try:
response = requests.get(self.url, proxies=proxies, params=parms, timeout=timeout)
if response.status_code == 200:
return open_image(BytesIO(response.content))
print("Could not retrieve data from camera successfully. Status:", response.status_code)
return None
except Exception as e:
print("Could not get Image data\n", e)
raise

View File

@@ -0,0 +1,52 @@
import string
from random import random
from typing import Any, Optional
from urllib import parse
from io import BytesIO
import requests
from PIL.Image import Image, open as open_image
from reolinkapi.utils.rtsp_client import RtspClient
class StreamAPIMixin:
""" API calls for opening a video stream or capturing an image from the camera."""
def open_video_stream(self, callback: Any = None, proxies: Any = None) -> Any:
"""
'https://support.reolink.com/hc/en-us/articles/360007010473-How-to-Live-View-Reolink-Cameras-via-VLC-Media-Player'
Blocking function creates a generator and returns the frames as it is spawned
:param callback:
:param proxies: Default is none, example: {"host": "localhost", "port": 8000}
"""
rtsp_client = RtspClient(
ip=self.ip, username=self.username, password=self.password, proxies=proxies, callback=callback)
return rtsp_client.open_stream()
def get_snap(self, timeout: float = 3, proxies: Any = None) -> Optional[Image]:
"""
Gets a "snap" of the current camera video data and returns a Pillow Image or None
:param timeout: Request timeout to camera in seconds
:param proxies: http/https proxies to pass to the request object.
:return: Image or None
"""
data = {
'cmd': 'Snap',
'channel': 0,
'rs': ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)),
'user': self.username,
'password': self.password,
}
parms = parse.urlencode(data).encode("utf-8")
try:
response = requests.get(self.url, proxies=proxies, params=parms, timeout=timeout)
if response.status_code == 200:
return open_image(BytesIO(response.content))
print("Could not retrieve data from camera successfully. Status:", response.status_code)
return None
except Exception as e:
print("Could not get Image data\n", e)
raise

View File

View File

@@ -2,11 +2,12 @@ import os
from threading import ThreadError from threading import ThreadError
from typing import Any from typing import Any
import cv2 import cv2
from reolinkapi.util import threaded from reolinkapi.utils.util import threaded
class RtspClient: class RtspClient:
""" """
This is a wrapper of the OpenCV VideoCapture method
Inspiration from: Inspiration from:
- https://benhowell.github.io/guide/2015/03/09/opencv-and-web-cam-streaming - https://benhowell.github.io/guide/2015/03/09/opencv-and-web-cam-streaming
- https://stackoverflow.com/questions/19846332/python-threading-inside-a-class - https://stackoverflow.com/questions/19846332/python-threading-inside-a-class

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
numpy==1.19.4
opencv-python==4.4.0.46
Pillow==8.0.1
PySocks==1.7.1
PyYaml==5.3.1
requests>=2.18.4

View File

@@ -20,9 +20,9 @@ def find_version(*file_paths):
# Package meta-data. # Package meta-data.
NAME = 'reolinkapi' NAME = 'reolinkapi'
DESCRIPTION = 'Reolink Camera API written in Python 3.6' DESCRIPTION = 'Reolink Camera API client written in Python 3'
URL = 'https://github.com/Benehiko/ReolinkCameraAPI' URL = 'https://github.com/ReolinkCameraAPI/reolinkapipy'
AUTHOR_EMAIL = '' AUTHOR_EMAIL = 'alanoterblanche@gmail.com'
AUTHOR = 'Benehiko' AUTHOR = 'Benehiko'
LICENSE = 'GPL-3.0' LICENSE = 'GPL-3.0'
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [