diff --git a/README.md b/README.md index 19332b7..4fcdd37 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@

Reolink Approval - GitHub - GitHub tag (latest SemVer) - PyPI + GitHub + GitHub tag (latest SemVer) + PyPI Discord

diff --git a/examples/basic_usage.py b/examples/basic_usage.py new file mode 100644 index 0000000..0ba744c --- /dev/null +++ b/examples/basic_usage.py @@ -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() diff --git a/reolinkapi/__init__.py b/reolinkapi/__init__.py index e623a67..0a886c6 100644 --- a/reolinkapi/__init__.py +++ b/reolinkapi/__init__.py @@ -1,4 +1,4 @@ -from .api_handler import APIHandler +from reolinkapi.handlers.api_handler import APIHandler from .camera import Camera __version__ = "0.1.2" diff --git a/reolinkapi/camera.py b/reolinkapi/camera.py index 47db97e..98c208b 100644 --- a/reolinkapi/camera.py +++ b/reolinkapi/camera.py @@ -1,19 +1,31 @@ -from .api_handler import APIHandler +from reolinkapi.handlers.api_handler import 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. 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 username: :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 # 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: # APIHandler.__init__(self, ip, username, password) @@ -21,4 +33,6 @@ class Camera(APIHandler): self.ip = ip self.username = username self.password = password - super().login() + + if not defer_login: + super().login() diff --git a/reolinkapi/config_handler.py b/reolinkapi/config_handler.py deleted file mode 100644 index a1c08ec..0000000 --- a/reolinkapi/config_handler.py +++ /dev/null @@ -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 diff --git a/reolinkapi/handlers/__init__.py b/reolinkapi/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reolinkapi/api_handler.py b/reolinkapi/handlers/api_handler.py similarity index 84% rename from reolinkapi/api_handler.py rename to reolinkapi/handlers/api_handler.py index 3ceae88..46d4637 100644 --- a/reolinkapi/api_handler.py +++ b/reolinkapi/handlers/api_handler.py @@ -1,18 +1,19 @@ import requests from typing import Dict, List, Optional, Union -from reolinkapi.alarm import AlarmAPIMixin -from reolinkapi.device import DeviceAPIMixin -from reolinkapi.display import DisplayAPIMixin -from reolinkapi.download import DownloadAPIMixin -from reolinkapi.image import ImageAPIMixin -from reolinkapi.motion import MotionAPIMixin -from reolinkapi.network import NetworkAPIMixin -from reolinkapi.ptz import PtzAPIMixin -from reolinkapi.recording import RecordingAPIMixin -from reolinkapi.resthandle import Request -from reolinkapi.system import SystemAPIMixin -from reolinkapi.user import UserAPIMixin -from reolinkapi.zoom import ZoomAPIMixin +from reolinkapi.mixins.alarm import AlarmAPIMixin +from reolinkapi.mixins.device import DeviceAPIMixin +from reolinkapi.mixins.display import DisplayAPIMixin +from reolinkapi.mixins.download import DownloadAPIMixin +from reolinkapi.mixins.image import ImageAPIMixin +from reolinkapi.mixins.motion import MotionAPIMixin +from reolinkapi.mixins.network import NetworkAPIMixin +from reolinkapi.mixins.ptz import PtzAPIMixin +from reolinkapi.mixins.record import RecordAPIMixin +from reolinkapi.handlers.rest_handler import Request +from reolinkapi.mixins.stream import StreamAPIMixin +from reolinkapi.mixins.system import SystemAPIMixin +from reolinkapi.mixins.user import UserAPIMixin +from reolinkapi.mixins.zoom import ZoomAPIMixin class APIHandler(AlarmAPIMixin, @@ -23,10 +24,11 @@ class APIHandler(AlarmAPIMixin, MotionAPIMixin, NetworkAPIMixin, PtzAPIMixin, - RecordingAPIMixin, + RecordAPIMixin, SystemAPIMixin, UserAPIMixin, - ZoomAPIMixin): + ZoomAPIMixin, + StreamAPIMixin): """ The APIHandler class is the backend part of the API, the actual API calls are implemented in Mixins. @@ -42,6 +44,7 @@ class APIHandler(AlarmAPIMixin, :param ip: :param username: :param password: + :param https: connect over https :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 diff --git a/reolinkapi/resthandle.py b/reolinkapi/handlers/rest_handler.py similarity index 100% rename from reolinkapi/resthandle.py rename to reolinkapi/handlers/rest_handler.py diff --git a/reolinkapi/mixins/__init__.py b/reolinkapi/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reolinkapi/alarm.py b/reolinkapi/mixins/alarm.py similarity index 100% rename from reolinkapi/alarm.py rename to reolinkapi/mixins/alarm.py diff --git a/reolinkapi/device.py b/reolinkapi/mixins/device.py similarity index 100% rename from reolinkapi/device.py rename to reolinkapi/mixins/device.py diff --git a/reolinkapi/display.py b/reolinkapi/mixins/display.py similarity index 100% rename from reolinkapi/display.py rename to reolinkapi/mixins/display.py diff --git a/reolinkapi/download.py b/reolinkapi/mixins/download.py similarity index 100% rename from reolinkapi/download.py rename to reolinkapi/mixins/download.py diff --git a/reolinkapi/image.py b/reolinkapi/mixins/image.py similarity index 100% rename from reolinkapi/image.py rename to reolinkapi/mixins/image.py diff --git a/reolinkapi/motion.py b/reolinkapi/mixins/motion.py similarity index 100% rename from reolinkapi/motion.py rename to reolinkapi/mixins/motion.py diff --git a/reolinkapi/network.py b/reolinkapi/mixins/network.py similarity index 100% rename from reolinkapi/network.py rename to reolinkapi/mixins/network.py diff --git a/reolinkapi/ptz.py b/reolinkapi/mixins/ptz.py similarity index 100% rename from reolinkapi/ptz.py rename to reolinkapi/mixins/ptz.py diff --git a/reolinkapi/recording.py b/reolinkapi/mixins/record.py similarity index 57% rename from reolinkapi/recording.py rename to reolinkapi/mixins/record.py index 41284f2..375b5ca 100644 --- a/reolinkapi/recording.py +++ b/reolinkapi/mixins/record.py @@ -1,15 +1,8 @@ -import requests -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 +from typing import Dict -class RecordingAPIMixin: - """API calls for recording/streaming image or video.""" +class RecordAPIMixin: + """API calls for the recording settings""" def get_recording_encoding(self) -> Dict: """ @@ -77,44 +70,3 @@ class RecordingAPIMixin: } ] 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 diff --git a/reolinkapi/mixins/stream.py b/reolinkapi/mixins/stream.py new file mode 100644 index 0000000..5d6e419 --- /dev/null +++ b/reolinkapi/mixins/stream.py @@ -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 diff --git a/reolinkapi/system.py b/reolinkapi/mixins/system.py similarity index 100% rename from reolinkapi/system.py rename to reolinkapi/mixins/system.py diff --git a/reolinkapi/user.py b/reolinkapi/mixins/user.py similarity index 100% rename from reolinkapi/user.py rename to reolinkapi/mixins/user.py diff --git a/reolinkapi/zoom.py b/reolinkapi/mixins/zoom.py similarity index 100% rename from reolinkapi/zoom.py rename to reolinkapi/mixins/zoom.py diff --git a/reolinkapi/utils/__init__.py b/reolinkapi/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reolinkapi/rtsp_client.py b/reolinkapi/utils/rtsp_client.py similarity index 97% rename from reolinkapi/rtsp_client.py rename to reolinkapi/utils/rtsp_client.py index 0c1db0e..e260a74 100644 --- a/reolinkapi/rtsp_client.py +++ b/reolinkapi/utils/rtsp_client.py @@ -2,11 +2,12 @@ import os from threading import ThreadError from typing import Any import cv2 -from reolinkapi.util import threaded +from reolinkapi.utils.util import threaded class RtspClient: """ + This is a wrapper of the OpenCV VideoCapture method Inspiration from: - https://benhowell.github.io/guide/2015/03/09/opencv-and-web-cam-streaming - https://stackoverflow.com/questions/19846332/python-threading-inside-a-class diff --git a/reolinkapi/util.py b/reolinkapi/utils/util.py similarity index 100% rename from reolinkapi/util.py rename to reolinkapi/utils/util.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a8aabcd --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/setup.py b/setup.py index 500a40f..3764023 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ def find_version(*file_paths): # Package meta-data. NAME = 'reolinkapi' -DESCRIPTION = 'Reolink Camera API written in Python 3.6' -URL = 'https://github.com/Benehiko/ReolinkCameraAPI' -AUTHOR_EMAIL = '' +DESCRIPTION = 'Reolink Camera API client written in Python 3' +URL = 'https://github.com/ReolinkCameraAPI/reolinkapipy' +AUTHOR_EMAIL = 'alanoterblanche@gmail.com' AUTHOR = 'Benehiko' LICENSE = 'GPL-3.0' INSTALL_REQUIRES = [