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:
@@ -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
11
examples/basic_usage.py
Normal 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()
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
if not defer_login:
|
||||||
super().login()
|
super().login()
|
||||||
|
|||||||
@@ -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
|
|
||||||
0
reolinkapi/handlers/__init__.py
Normal file
0
reolinkapi/handlers/__init__.py
Normal 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
|
||||||
0
reolinkapi/mixins/__init__.py
Normal file
0
reolinkapi/mixins/__init__.py
Normal 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
|
|
||||||
52
reolinkapi/mixins/stream.py
Normal file
52
reolinkapi/mixins/stream.py
Normal 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
|
||||||
0
reolinkapi/utils/__init__.py
Normal file
0
reolinkapi/utils/__init__.py
Normal 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
6
requirements.txt
Normal 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
|
||||||
6
setup.py
6
setup.py
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user