Merge pull request #37 from barretobrock/develop
Refactor package to be imported as a single package
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
secrets.cfg
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
24
Camera.py
24
Camera.py
@@ -1,24 +0,0 @@
|
|||||||
from api import APIHandler
|
|
||||||
|
|
||||||
|
|
||||||
class Camera(APIHandler):
|
|
||||||
|
|
||||||
def __init__(self, ip, username="admin", password="", https=False):
|
|
||||||
"""
|
|
||||||
Initialise the Camera object by passing the ip address.
|
|
||||||
The default details {"username":"admin", "password":""} will be used if nothing passed
|
|
||||||
:param ip:
|
|
||||||
:param username:
|
|
||||||
:param password:
|
|
||||||
"""
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Normal call without proxy:
|
|
||||||
# APIHandler.__init__(self, ip, username, password)
|
|
||||||
|
|
||||||
self.ip = ip
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
super().login()
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import io
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigHandler:
|
|
||||||
camera_settings = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load() -> yaml or None:
|
|
||||||
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
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .APIHandler import APIHandler
|
|
||||||
|
|
||||||
__version__ = "0.0.5"
|
|
||||||
VERSION = __version__
|
|
||||||
111
api/recording.py
111
api/recording.py
@@ -1,111 +0,0 @@
|
|||||||
import requests
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from urllib import parse
|
|
||||||
from io import BytesIO
|
|
||||||
from PIL import Image
|
|
||||||
from RtspClient import RtspClient
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingAPIMixin:
|
|
||||||
"""API calls for recording/streaming image or video."""
|
|
||||||
|
|
||||||
def get_recording_encoding(self) -> object:
|
|
||||||
"""
|
|
||||||
Get the current camera encoding settings for "Clear" and "Fluent" profiles.
|
|
||||||
See examples/response/GetEnc.json for example response data.
|
|
||||||
:return: response json
|
|
||||||
"""
|
|
||||||
body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}]
|
|
||||||
return self._execute_command('GetEnc', body)
|
|
||||||
|
|
||||||
def get_recording_advanced(self) -> object:
|
|
||||||
"""
|
|
||||||
Get recording advanced setup data
|
|
||||||
See examples/response/GetRec.json for example response data.
|
|
||||||
:return: response json
|
|
||||||
"""
|
|
||||||
body = [{"cmd": "GetRec", "action": 1, "param": {"channel": 0}}]
|
|
||||||
return self._execute_command('GetRec', body)
|
|
||||||
|
|
||||||
def set_recording_encoding(self,
|
|
||||||
audio=0,
|
|
||||||
main_bit_rate=8192,
|
|
||||||
main_frame_rate=8,
|
|
||||||
main_profile='High',
|
|
||||||
main_size="2560*1440",
|
|
||||||
sub_bit_rate=160,
|
|
||||||
sub_frame_rate=7,
|
|
||||||
sub_profile='High',
|
|
||||||
sub_size='640*480') -> object:
|
|
||||||
"""
|
|
||||||
Sets the current camera encoding settings for "Clear" and "Fluent" profiles.
|
|
||||||
:param audio: int Audio on or off
|
|
||||||
:param main_bit_rate: int Clear Bit Rate
|
|
||||||
:param main_frame_rate: int Clear Frame Rate
|
|
||||||
:param main_profile: string Clear Profile
|
|
||||||
:param main_size: string Clear Size
|
|
||||||
:param sub_bit_rate: int Fluent Bit Rate
|
|
||||||
:param sub_frame_rate: int Fluent Frame Rate
|
|
||||||
:param sub_profile: string Fluent Profile
|
|
||||||
:param sub_size: string Fluent Size
|
|
||||||
:return: response
|
|
||||||
"""
|
|
||||||
body = [{"cmd": "SetEnc",
|
|
||||||
"action": 0,
|
|
||||||
"param":
|
|
||||||
{"Enc":
|
|
||||||
{"audio": audio,
|
|
||||||
"channel": 0,
|
|
||||||
"mainStream": {
|
|
||||||
"bitRate": main_bit_rate,
|
|
||||||
"frameRate": main_frame_rate,
|
|
||||||
"profile": main_profile,
|
|
||||||
"size": main_size},
|
|
||||||
"subStream": {
|
|
||||||
"bitRate": sub_bit_rate,
|
|
||||||
"frameRate": sub_frame_rate,
|
|
||||||
"profile": sub_profile,
|
|
||||||
"size": sub_size}}
|
|
||||||
}}]
|
|
||||||
return self._execute_command('SetEnc', body)
|
|
||||||
|
|
||||||
###########
|
|
||||||
# RTSP Stream
|
|
||||||
###########
|
|
||||||
def open_video_stream(self, callback=None, profile: str = "main", proxies=None):
|
|
||||||
"""
|
|
||||||
'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 profile: profile is "main" or "sub"
|
|
||||||
: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: int = 3, proxies=None) -> Image or None:
|
|
||||||
"""
|
|
||||||
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 = {}
|
|
||||||
data['cmd'] = 'Snap'
|
|
||||||
data['channel'] = 0
|
|
||||||
data['rs'] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
|
||||||
data['user'] = self.username
|
|
||||||
data['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 Image.open(BytesIO(response.content))
|
|
||||||
print("Could not retrieve data from camera successfully. Status:", response.stats_code)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print("Could not get Image data\n", e)
|
|
||||||
raise
|
|
||||||
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()
|
||||||
44
examples/download_motions.py
Normal file
44
examples/download_motions.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Downloads all motion events from camera from the past hour."""
|
||||||
|
import os
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
from datetime import datetime as dt, timedelta
|
||||||
|
from reolinkapi import Camera
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(props_path: str) -> dict:
|
||||||
|
"""Reads in a properties file into variables.
|
||||||
|
|
||||||
|
NB! this config file is kept out of commits with .gitignore. The structure of this file is such:
|
||||||
|
# secrets.cfg
|
||||||
|
[camera]
|
||||||
|
ip={ip_address}
|
||||||
|
username={username}
|
||||||
|
password={password}
|
||||||
|
"""
|
||||||
|
config = RawConfigParser()
|
||||||
|
assert os.path.exists(props_path), f"Path does not exist: {props_path}"
|
||||||
|
config.read(props_path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
# Read in your ip, username, & password
|
||||||
|
# (NB! you'll likely have to create this file. See tests/test_camera.py for details on structure)
|
||||||
|
config = read_config('../secrets.cfg')
|
||||||
|
|
||||||
|
ip = config.get('camera', 'ip')
|
||||||
|
un = config.get('camera', 'username')
|
||||||
|
pw = config.get('camera', 'password')
|
||||||
|
|
||||||
|
# Connect to camera
|
||||||
|
cam = Camera(ip, un, pw)
|
||||||
|
|
||||||
|
start = (dt.now() - timedelta(hours=1))
|
||||||
|
end = dt.now()
|
||||||
|
# Collect motion events between these timestamps for substream
|
||||||
|
processed_motions = cam.get_motion_files(start=start, end=end, streamtype='sub')
|
||||||
|
|
||||||
|
dl_dir = os.path.join(os.path.expanduser('~'), 'Downloads')
|
||||||
|
for i, motion in enumerate(processed_motions):
|
||||||
|
fname = motion['filename']
|
||||||
|
# Download the mp4
|
||||||
|
resp = cam.get_file(fname, output_path=os.path.join(dl_dir, f'motion_event_{i}.mp4'))
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import cv2
|
import cv2
|
||||||
|
from reolinkapi import Camera
|
||||||
from Camera import Camera
|
|
||||||
|
|
||||||
|
|
||||||
def non_blocking():
|
def non_blocking():
|
||||||
|
|||||||
4
reolinkapi/__init__.py
Normal file
4
reolinkapi/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from reolinkapi.handlers.api_handler import APIHandler
|
||||||
|
from .camera import Camera
|
||||||
|
|
||||||
|
__version__ = "0.1.2"
|
||||||
38
reolinkapi/camera.py
Normal file
38
reolinkapi/camera.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from reolinkapi.handlers.api_handler import APIHandler
|
||||||
|
|
||||||
|
|
||||||
|
class Camera(APIHandler):
|
||||||
|
|
||||||
|
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, **kwargs)
|
||||||
|
|
||||||
|
# Normal call without proxy:
|
||||||
|
# APIHandler.__init__(self, ip, username, password)
|
||||||
|
|
||||||
|
self.ip = ip
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
if not defer_login:
|
||||||
|
super().login()
|
||||||
0
reolinkapi/handlers/__init__.py
Normal file
0
reolinkapi/handlers/__init__.py
Normal file
@@ -1,26 +1,34 @@
|
|||||||
from .recording import RecordingAPIMixin
|
import requests
|
||||||
from .zoom import ZoomAPIMixin
|
from typing import Dict, List, Optional, Union
|
||||||
from .device import DeviceAPIMixin
|
from reolinkapi.mixins.alarm import AlarmAPIMixin
|
||||||
from .display import DisplayAPIMixin
|
from reolinkapi.mixins.device import DeviceAPIMixin
|
||||||
from .network import NetworkAPIMixin
|
from reolinkapi.mixins.display import DisplayAPIMixin
|
||||||
from .system import SystemAPIMixin
|
from reolinkapi.mixins.download import DownloadAPIMixin
|
||||||
from .user import UserAPIMixin
|
from reolinkapi.mixins.image import ImageAPIMixin
|
||||||
from .ptz import PtzAPIMixin
|
from reolinkapi.mixins.motion import MotionAPIMixin
|
||||||
from .alarm import AlarmAPIMixin
|
from reolinkapi.mixins.network import NetworkAPIMixin
|
||||||
from .image import ImageAPIMixin
|
from reolinkapi.mixins.ptz import PtzAPIMixin
|
||||||
from resthandle import Request
|
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(SystemAPIMixin,
|
class APIHandler(AlarmAPIMixin,
|
||||||
NetworkAPIMixin,
|
|
||||||
UserAPIMixin,
|
|
||||||
DeviceAPIMixin,
|
DeviceAPIMixin,
|
||||||
DisplayAPIMixin,
|
DisplayAPIMixin,
|
||||||
RecordingAPIMixin,
|
DownloadAPIMixin,
|
||||||
ZoomAPIMixin,
|
ImageAPIMixin,
|
||||||
|
MotionAPIMixin,
|
||||||
|
NetworkAPIMixin,
|
||||||
PtzAPIMixin,
|
PtzAPIMixin,
|
||||||
AlarmAPIMixin,
|
RecordAPIMixin,
|
||||||
ImageAPIMixin):
|
SystemAPIMixin,
|
||||||
|
UserAPIMixin,
|
||||||
|
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.
|
||||||
@@ -30,12 +38,13 @@ class APIHandler(SystemAPIMixin,
|
|||||||
All Code will try to follow the PEP 8 standard as described here: https://www.python.org/dev/peps/pep-0008/
|
All Code will try to follow the PEP 8 standard as described here: https://www.python.org/dev/peps/pep-0008/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip: str, username: str, password: str, https=False, **kwargs):
|
def __init__(self, ip: str, username: str, password: str, https: bool = False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialise the Camera API Handler (maps api calls into python)
|
Initialise the Camera API Handler (maps api calls into python)
|
||||||
: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
|
||||||
@@ -69,7 +78,10 @@ class APIHandler(SystemAPIMixin,
|
|||||||
print(self.token)
|
print(self.token)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print("Failed to login\nStatus Code:", response.status_code)
|
# TODO: Verify this change w/ owner. Delete old code if acceptable.
|
||||||
|
# A this point, response is NoneType. There won't be a status code property.
|
||||||
|
# print("Failed to login\nStatus Code:", response.status_code)
|
||||||
|
print("Failed to login\nResponse was null.")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error Login\n", e)
|
print("Error Login\n", e)
|
||||||
@@ -89,7 +101,8 @@ class APIHandler(SystemAPIMixin,
|
|||||||
print("Error Logout\n", e)
|
print("Error Logout\n", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _execute_command(self, command, data, multi=False):
|
def _execute_command(self, command: str, data: List[Dict], multi: bool = False) -> \
|
||||||
|
Optional[Union[Dict, bool]]:
|
||||||
"""
|
"""
|
||||||
Send a POST request to the IP camera with given data.
|
Send a POST request to the IP camera with given data.
|
||||||
:param command: name of the command to send
|
:param command: name of the command to send
|
||||||
@@ -105,6 +118,22 @@ class APIHandler(SystemAPIMixin,
|
|||||||
try:
|
try:
|
||||||
if self.token is None:
|
if self.token is None:
|
||||||
raise ValueError("Login first")
|
raise ValueError("Login first")
|
||||||
|
if command == 'Download':
|
||||||
|
# Special handling for downloading an mp4
|
||||||
|
# Pop the filepath from data
|
||||||
|
tgt_filepath = data[0].pop('filepath')
|
||||||
|
# Apply the data to the params
|
||||||
|
params.update(data[0])
|
||||||
|
with requests.get(self.url, params=params, stream=True) as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
with open(tgt_filepath, 'wb') as f:
|
||||||
|
f.write(req.content)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f'Error received: {req.status_code}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
response = Request.post(self.url, data=data, params=params)
|
response = Request.post(self.url, data=data, params=params)
|
||||||
return response.json()
|
return response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from typing import List, Dict, Union, Optional
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
proxies = None
|
proxies = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post(url: str, data, params=None) -> requests.Response or None:
|
def post(url: str, data: List[Dict], params: Dict[str, Union[str, float]] = None) -> \
|
||||||
|
Optional[requests.Response]:
|
||||||
"""
|
"""
|
||||||
Post request
|
Post request
|
||||||
:param params:
|
:param params:
|
||||||
@@ -17,11 +17,8 @@ class Request:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
headers = {'content-type': 'application/json'}
|
headers = {'content-type': 'application/json'}
|
||||||
r = requests.post(url, verify=False, params=params, json=data, headers=headers, proxies=Request.proxies)
|
r = requests.post(url, verify=False, params=params, json=data, headers=headers,
|
||||||
# if params is not None:
|
proxies=Request.proxies)
|
||||||
# r = requests.post(url, params=params, json=data, headers=headers, proxies=proxies)
|
|
||||||
# else:
|
|
||||||
# r = requests.post(url, json=data)
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
return r
|
return r
|
||||||
else:
|
else:
|
||||||
@@ -31,7 +28,7 @@ class Request:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(url, params, timeout=1) -> json or None:
|
def get(url: str, params: Dict[str, Union[str, float]], timeout: float = 1) -> Optional[requests.Response]:
|
||||||
"""
|
"""
|
||||||
Get request
|
Get request
|
||||||
:param url:
|
:param url:
|
||||||
@@ -41,7 +38,6 @@ class Request:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = requests.get(url=url, verify=False, params=params, timeout=timeout, proxies=Request.proxies)
|
data = requests.get(url=url, verify=False, params=params, timeout=timeout, proxies=Request.proxies)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Get Error\n", e)
|
print("Get Error\n", e)
|
||||||
0
reolinkapi/mixins/__init__.py
Normal file
0
reolinkapi/mixins/__init__.py
Normal file
@@ -1,7 +1,10 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class AlarmAPIMixin:
|
class AlarmAPIMixin:
|
||||||
"""API calls for getting device alarm information."""
|
"""API calls for getting device alarm information."""
|
||||||
|
|
||||||
def get_alarm_motion(self) -> object:
|
def get_alarm_motion(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Gets the device alarm motion
|
Gets the device alarm motion
|
||||||
See examples/response/GetAlarmMotion.json for example response data.
|
See examples/response/GetAlarmMotion.json for example response data.
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
|
||||||
class DeviceAPIMixin:
|
class DeviceAPIMixin:
|
||||||
"""API calls for getting device information."""
|
"""API calls for getting device information."""
|
||||||
def get_hdd_info(self) -> object:
|
DEFAULT_HDD_ID = [0]
|
||||||
|
|
||||||
|
def get_hdd_info(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Gets all HDD and SD card information from Camera
|
Gets all HDD and SD card information from Camera
|
||||||
See examples/response/GetHddInfo.json for example response data.
|
See examples/response/GetHddInfo.json for example response data.
|
||||||
@@ -9,12 +14,14 @@ class DeviceAPIMixin:
|
|||||||
body = [{"cmd": "GetHddInfo", "action": 0, "param": {}}]
|
body = [{"cmd": "GetHddInfo", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetHddInfo', body)
|
return self._execute_command('GetHddInfo', body)
|
||||||
|
|
||||||
def format_hdd(self, hdd_id: [int] = [0]) -> bool:
|
def format_hdd(self, hdd_id: List[float] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Format specified HDD/SD cards with their id's
|
Format specified HDD/SD cards with their id's
|
||||||
:param hdd_id: List of id's specified by the camera with get_hdd_info api. Default is 0 (SD card)
|
:param hdd_id: List of id's specified by the camera with get_hdd_info api. Default is 0 (SD card)
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
|
if hdd_id is None:
|
||||||
|
hdd_id = self.DEFAULT_HDD_ID
|
||||||
body = [{"cmd": "Format", "action": 0, "param": {"HddInfo": {"id": hdd_id}}}]
|
body = [{"cmd": "Format", "action": 0, "param": {"HddInfo": {"id": hdd_id}}}]
|
||||||
r_data = self._execute_command('Format', body)[0]
|
r_data = self._execute_command('Format', body)[0]
|
||||||
if r_data["value"]["rspCode"] == 200:
|
if r_data["value"]["rspCode"] == 200:
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class DisplayAPIMixin:
|
class DisplayAPIMixin:
|
||||||
"""API calls related to the current image (osd, on screen display)."""
|
"""API calls related to the current image (osd, on screen display)."""
|
||||||
|
|
||||||
def get_osd(self) -> object:
|
def get_osd(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get OSD information.
|
Get OSD information.
|
||||||
See examples/response/GetOsd.json for example response data.
|
See examples/response/GetOsd.json for example response data.
|
||||||
@@ -10,7 +13,7 @@ class DisplayAPIMixin:
|
|||||||
body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}]
|
body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}]
|
||||||
return self._execute_command('GetOsd', body)
|
return self._execute_command('GetOsd', body)
|
||||||
|
|
||||||
def get_mask(self) -> object:
|
def get_mask(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera mask information.
|
Get the camera mask information.
|
||||||
See examples/response/GetMask.json for example response data.
|
See examples/response/GetMask.json for example response data.
|
||||||
@@ -19,8 +22,8 @@ class DisplayAPIMixin:
|
|||||||
body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}]
|
body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}]
|
||||||
return self._execute_command('GetMask', body)
|
return self._execute_command('GetMask', body)
|
||||||
|
|
||||||
def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_enabled: bool = 0, osd_channel_name: str = "",
|
def set_osd(self, bg_color: bool = 0, channel: float = 0, osd_channel_enabled: bool = 0,
|
||||||
osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0,
|
osd_channel_name: str = "", osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0,
|
||||||
osd_time_pos: str = "Lower Right") -> bool:
|
osd_time_pos: str = "Lower Right") -> bool:
|
||||||
"""
|
"""
|
||||||
Set OSD
|
Set OSD
|
||||||
@@ -28,18 +31,24 @@ class DisplayAPIMixin:
|
|||||||
:param channel: int channel id
|
:param channel: int channel id
|
||||||
:param osd_channel_enabled: bool
|
:param osd_channel_enabled: bool
|
||||||
:param osd_channel_name: string channel name
|
:param osd_channel_name: string channel name
|
||||||
:param osd_channel_pos: string channel position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
|
:param osd_channel_pos: string channel position
|
||||||
|
["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
|
||||||
:param osd_time_enabled: bool
|
:param osd_time_enabled: bool
|
||||||
:param osd_time_pos: string time position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
|
:param osd_time_pos: string time position
|
||||||
|
["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
|
||||||
:return: whether the action was successful
|
:return: whether the action was successful
|
||||||
"""
|
"""
|
||||||
body = [{"cmd": "SetOsd", "action": 1, "param": {
|
body = [{"cmd": "SetOsd", "action": 1,
|
||||||
"Osd": {"bgcolor": bg_color, "channel": channel,
|
"param": {
|
||||||
"osdChannel": {"enable": osd_channel_enabled, "name": osd_channel_name,
|
"Osd": {
|
||||||
"pos": osd_channel_pos},
|
"bgcolor": bg_color,
|
||||||
|
"channel": channel,
|
||||||
|
"osdChannel": {
|
||||||
|
"enable": osd_channel_enabled, "name": osd_channel_name,
|
||||||
|
"pos": osd_channel_pos
|
||||||
|
},
|
||||||
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
|
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
|
||||||
}
|
}}}]
|
||||||
}}]
|
|
||||||
r_data = self._execute_command('SetOsd', body)[0]
|
r_data = self._execute_command('SetOsd', body)[0]
|
||||||
if r_data["value"]["rspCode"] == 200:
|
if r_data["value"]["rspCode"] == 200:
|
||||||
return True
|
return True
|
||||||
18
reolinkapi/mixins/download.py
Normal file
18
reolinkapi/mixins/download.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class DownloadAPIMixin:
|
||||||
|
"""API calls for downloading video files."""
|
||||||
|
def get_file(self, filename: str, output_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Download the selected video file
|
||||||
|
:return: response json
|
||||||
|
"""
|
||||||
|
body = [
|
||||||
|
{
|
||||||
|
"cmd": "Download",
|
||||||
|
"source": filename,
|
||||||
|
"output": filename,
|
||||||
|
"filepath": output_path
|
||||||
|
}
|
||||||
|
]
|
||||||
|
resp = self._execute_command('Download', body)
|
||||||
|
|
||||||
|
return resp
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class ImageAPIMixin:
|
class ImageAPIMixin:
|
||||||
"""API calls for image settings."""
|
"""API calls for image settings."""
|
||||||
|
|
||||||
def set_adv_image_settings(self,
|
def set_adv_image_settings(self,
|
||||||
anti_flicker='Outdoor',
|
anti_flicker: str = 'Outdoor',
|
||||||
exposure='Auto',
|
exposure: str = 'Auto',
|
||||||
gain_min=1,
|
gain_min: float = 1,
|
||||||
gain_max=62,
|
gain_max: float = 62,
|
||||||
shutter_min=1,
|
shutter_min: float = 1,
|
||||||
shutter_max=125,
|
shutter_max: float = 125,
|
||||||
blue_gain=128,
|
blue_gain: float = 128,
|
||||||
red_gain=128,
|
red_gain: float = 128,
|
||||||
white_balance='Auto',
|
white_balance: str = 'Auto',
|
||||||
day_night='Auto',
|
day_night: str = 'Auto',
|
||||||
back_light='DynamicRangeControl',
|
back_light: str = 'DynamicRangeControl',
|
||||||
blc=128,
|
blc: float = 128,
|
||||||
drc=128,
|
drc: float = 128,
|
||||||
rotation=0,
|
rotation: float = 0,
|
||||||
mirroring=0,
|
mirroring: float = 0,
|
||||||
nr3d=1) -> object:
|
nr3d: float = 1) -> Dict:
|
||||||
"""
|
"""
|
||||||
Sets the advanced camera settings.
|
Sets the advanced camera settings.
|
||||||
|
|
||||||
@@ -66,11 +68,11 @@ class ImageAPIMixin:
|
|||||||
return self._execute_command('SetIsp', body)
|
return self._execute_command('SetIsp', body)
|
||||||
|
|
||||||
def set_image_settings(self,
|
def set_image_settings(self,
|
||||||
brightness=128,
|
brightness: float = 128,
|
||||||
contrast=62,
|
contrast: float = 62,
|
||||||
hue=1,
|
hue: float = 1,
|
||||||
saturation=125,
|
saturation: float = 125,
|
||||||
sharpness=128) -> object:
|
sharpness: float = 128) -> Dict:
|
||||||
"""
|
"""
|
||||||
Sets the camera image settings.
|
Sets the camera image settings.
|
||||||
|
|
||||||
80
reolinkapi/mixins/motion.py
Normal file
80
reolinkapi/mixins/motion.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from typing import Union, List, Dict
|
||||||
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
|
||||||
|
# Type hints for input and output of the motion api response
|
||||||
|
RAW_MOTION_LIST_TYPE = List[Dict[str, Union[str, float, Dict[str, str]]]]
|
||||||
|
PROCESSED_MOTION_LIST_TYPE = List[Dict[str, Union[str, dt]]]
|
||||||
|
|
||||||
|
|
||||||
|
class MotionAPIMixin:
|
||||||
|
"""API calls for past motion alerts."""
|
||||||
|
def get_motion_files(self, start: dt, end: dt = dt.now(),
|
||||||
|
streamtype: str = 'sub') -> PROCESSED_MOTION_LIST_TYPE:
|
||||||
|
"""
|
||||||
|
Get the timestamps and filenames of motion detection events for the time range provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: the starting time range to examine
|
||||||
|
end: the end time of the time range to examine
|
||||||
|
streamtype: 'main' or 'sub' - the stream to examine
|
||||||
|
:return: response json
|
||||||
|
"""
|
||||||
|
search_params = {
|
||||||
|
'Search': {
|
||||||
|
'channel': 0,
|
||||||
|
'streamType': streamtype,
|
||||||
|
'onlyStatus': 0,
|
||||||
|
'StartTime': {
|
||||||
|
'year': start.year,
|
||||||
|
'mon': start.month,
|
||||||
|
'day': start.day,
|
||||||
|
'hour': start.hour,
|
||||||
|
'min': start.minute,
|
||||||
|
'sec': start.second
|
||||||
|
},
|
||||||
|
'EndTime': {
|
||||||
|
'year': end.year,
|
||||||
|
'mon': end.month,
|
||||||
|
'day': end.day,
|
||||||
|
'hour': end.hour,
|
||||||
|
'min': end.minute,
|
||||||
|
'sec': end.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = [{"cmd": "Search", "action": 1, "param": search_params}]
|
||||||
|
|
||||||
|
resp = self._execute_command('Search', body)[0]
|
||||||
|
result = resp['value']['SearchResult']
|
||||||
|
files = result.get('File', [])
|
||||||
|
if len(files) > 0:
|
||||||
|
# Begin processing files
|
||||||
|
processed_files = self._process_motion_files(files)
|
||||||
|
return processed_files
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_motion_files(motion_files: RAW_MOTION_LIST_TYPE) -> PROCESSED_MOTION_LIST_TYPE:
|
||||||
|
"""Processes raw list of dicts containing motion timestamps
|
||||||
|
and the filename associated with them"""
|
||||||
|
# Process files
|
||||||
|
processed_motions = []
|
||||||
|
replace_fields = {'mon': 'month', 'sec': 'second', 'min': 'minute'}
|
||||||
|
for file in motion_files:
|
||||||
|
time_range = {}
|
||||||
|
for x in ['Start', 'End']:
|
||||||
|
# Get raw dict
|
||||||
|
raw = file[f'{x}Time']
|
||||||
|
# Replace certain keys
|
||||||
|
for k, v in replace_fields.items():
|
||||||
|
if k in raw.keys():
|
||||||
|
raw[v] = raw.pop(k)
|
||||||
|
time_range[x.lower()] = dt(**raw)
|
||||||
|
start, end = time_range.values()
|
||||||
|
processed_motions.append({
|
||||||
|
'start': start,
|
||||||
|
'end': end,
|
||||||
|
'filename': file['name']
|
||||||
|
})
|
||||||
|
return processed_motions
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class NetworkAPIMixin:
|
class NetworkAPIMixin:
|
||||||
"""API calls for network settings."""
|
"""API calls for network settings."""
|
||||||
def set_net_port(self, http_port=80, https_port=443, media_port=9000, onvif_port=8000, rtmp_port=1935,
|
def set_net_port(self, http_port: float = 80, https_port: float = 443, media_port: float = 9000,
|
||||||
rtsp_port=554) -> bool:
|
onvif_port: float = 8000, rtmp_port: float = 1935, rtsp_port: float = 554) -> bool:
|
||||||
"""
|
"""
|
||||||
Set network ports
|
Set network ports
|
||||||
If nothing is specified, the default values will be used
|
If nothing is specified, the default values will be used
|
||||||
@@ -25,7 +28,7 @@ class NetworkAPIMixin:
|
|||||||
print("Successfully Set Network Ports")
|
print("Successfully Set Network Ports")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_wifi(self, ssid, password) -> object:
|
def set_wifi(self, ssid: str, password: str) -> Dict:
|
||||||
body = [{"cmd": "SetWifi", "action": 0, "param": {
|
body = [{"cmd": "SetWifi", "action": 0, "param": {
|
||||||
"Wifi": {
|
"Wifi": {
|
||||||
"ssid": ssid,
|
"ssid": ssid,
|
||||||
@@ -33,7 +36,7 @@ class NetworkAPIMixin:
|
|||||||
}}}]
|
}}}]
|
||||||
return self._execute_command('SetWifi', body)
|
return self._execute_command('SetWifi', body)
|
||||||
|
|
||||||
def get_net_ports(self) -> object:
|
def get_net_ports(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get network ports
|
Get network ports
|
||||||
See examples/response/GetNetworkAdvanced.json for example response data.
|
See examples/response/GetNetworkAdvanced.json for example response data.
|
||||||
@@ -44,15 +47,15 @@ class NetworkAPIMixin:
|
|||||||
{"cmd": "GetP2p", "action": 0, "param": {}}]
|
{"cmd": "GetP2p", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetNetPort', body, multi=True)
|
return self._execute_command('GetNetPort', body, multi=True)
|
||||||
|
|
||||||
def get_wifi(self):
|
def get_wifi(self) -> Dict:
|
||||||
body = [{"cmd": "GetWifi", "action": 1, "param": {}}]
|
body = [{"cmd": "GetWifi", "action": 1, "param": {}}]
|
||||||
return self._execute_command('GetWifi', body)
|
return self._execute_command('GetWifi', body)
|
||||||
|
|
||||||
def scan_wifi(self):
|
def scan_wifi(self) -> Dict:
|
||||||
body = [{"cmd": "ScanWifi", "action": 1, "param": {}}]
|
body = [{"cmd": "ScanWifi", "action": 1, "param": {}}]
|
||||||
return self._execute_command('ScanWifi', body)
|
return self._execute_command('ScanWifi', body)
|
||||||
|
|
||||||
def get_network_general(self) -> object:
|
def get_network_general(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera information
|
Get the camera information
|
||||||
See examples/response/GetNetworkGeneral.json for example response data.
|
See examples/response/GetNetworkGeneral.json for example response data.
|
||||||
@@ -61,7 +64,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetLocalLink", "action": 0, "param": {}}]
|
body = [{"cmd": "GetLocalLink", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetLocalLink', body)
|
return self._execute_command('GetLocalLink', body)
|
||||||
|
|
||||||
def get_network_ddns(self) -> object:
|
def get_network_ddns(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera DDNS network information
|
Get the camera DDNS network information
|
||||||
See examples/response/GetNetworkDDNS.json for example response data.
|
See examples/response/GetNetworkDDNS.json for example response data.
|
||||||
@@ -70,7 +73,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetDdns", "action": 0, "param": {}}]
|
body = [{"cmd": "GetDdns", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetDdns', body)
|
return self._execute_command('GetDdns', body)
|
||||||
|
|
||||||
def get_network_ntp(self) -> object:
|
def get_network_ntp(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera NTP network information
|
Get the camera NTP network information
|
||||||
See examples/response/GetNetworkNTP.json for example response data.
|
See examples/response/GetNetworkNTP.json for example response data.
|
||||||
@@ -79,7 +82,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetNtp", "action": 0, "param": {}}]
|
body = [{"cmd": "GetNtp", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetNtp', body)
|
return self._execute_command('GetNtp', body)
|
||||||
|
|
||||||
def get_network_email(self) -> object:
|
def get_network_email(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera email network information
|
Get the camera email network information
|
||||||
See examples/response/GetNetworkEmail.json for example response data.
|
See examples/response/GetNetworkEmail.json for example response data.
|
||||||
@@ -88,7 +91,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetEmail", "action": 0, "param": {}}]
|
body = [{"cmd": "GetEmail", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetEmail', body)
|
return self._execute_command('GetEmail', body)
|
||||||
|
|
||||||
def get_network_ftp(self) -> object:
|
def get_network_ftp(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera FTP network information
|
Get the camera FTP network information
|
||||||
See examples/response/GetNetworkFtp.json for example response data.
|
See examples/response/GetNetworkFtp.json for example response data.
|
||||||
@@ -97,7 +100,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetFtp", "action": 0, "param": {}}]
|
body = [{"cmd": "GetFtp", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetFtp', body)
|
return self._execute_command('GetFtp', body)
|
||||||
|
|
||||||
def get_network_push(self) -> object:
|
def get_network_push(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera push network information
|
Get the camera push network information
|
||||||
See examples/response/GetNetworkPush.json for example response data.
|
See examples/response/GetNetworkPush.json for example response data.
|
||||||
@@ -106,7 +109,7 @@ class NetworkAPIMixin:
|
|||||||
body = [{"cmd": "GetPush", "action": 0, "param": {}}]
|
body = [{"cmd": "GetPush", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetPush', body)
|
return self._execute_command('GetPush', body)
|
||||||
|
|
||||||
def get_network_status(self) -> object:
|
def get_network_status(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera status network information
|
Get the camera status network information
|
||||||
See examples/response/GetNetworkGeneral.json for example response data.
|
See examples/response/GetNetworkGeneral.json for example response data.
|
||||||
@@ -1,46 +1,49 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class PtzAPIMixin:
|
class PtzAPIMixin:
|
||||||
"""
|
"""
|
||||||
API for PTZ functions.
|
API for PTZ functions.
|
||||||
"""
|
"""
|
||||||
def _send_operation(self, operation, speed, index=None):
|
def _send_operation(self, operation: str, speed: float, index: float = None) -> Dict:
|
||||||
if index is None:
|
# Refactored to reduce redundancy
|
||||||
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": operation, "speed": speed}}]
|
param = {"channel": 0, "op": operation, "speed": speed}
|
||||||
else:
|
if index is not None:
|
||||||
data = [{"cmd": "PtzCtrl", "action": 0, "param": {
|
param['id'] = index
|
||||||
"channel": 0, "op": operation, "speed": speed, "id": index}}]
|
data = [{"cmd": "PtzCtrl", "action": 0, "param": param}]
|
||||||
return self._execute_command('PtzCtrl', data)
|
return self._execute_command('PtzCtrl', data)
|
||||||
|
|
||||||
def _send_noparm_operation(self, operation):
|
def _send_noparm_operation(self, operation: str) -> Dict:
|
||||||
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": operation}}]
|
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": operation}}]
|
||||||
return self._execute_command('PtzCtrl', data)
|
return self._execute_command('PtzCtrl', data)
|
||||||
|
|
||||||
def _send_set_preset(self, operation, enable, preset=1, name='pos1'):
|
def _send_set_preset(self, enable: float, preset: float = 1, name: str = 'pos1') -> Dict:
|
||||||
data = [{"cmd": "SetPtzPreset", "action": 0, "param": {
|
data = [{"cmd": "SetPtzPreset", "action": 0, "param": {
|
||||||
"channel": 0, "enable": enable, "id": preset, "name": name}}]
|
"channel": 0, "enable": enable, "id": preset, "name": name}}]
|
||||||
return self._execute_command('PtzCtrl', data)
|
return self._execute_command('PtzCtrl', data)
|
||||||
|
|
||||||
def go_to_preset(self, speed=60, index=1):
|
def go_to_preset(self, speed: float = 60, index: float = 1) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to a preset location
|
Move the camera to a preset location
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._send_operation('ToPos', speed=speed, index=index)
|
return self._send_operation('ToPos', speed=speed, index=index)
|
||||||
|
|
||||||
def add_preset(self, preset=1, name='pos1'):
|
def add_preset(self, preset: float = 1, name: str = 'pos1') -> Dict:
|
||||||
"""
|
"""
|
||||||
Adds the current camera position to the specified preset.
|
Adds the current camera position to the specified preset.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._send_set_preset('PtzPreset', enable=1, preset=preset, name=name)
|
return self._send_set_preset(enable=1, preset=preset, name=name)
|
||||||
|
|
||||||
def remove_preset(self, preset=1, name='pos1'):
|
def remove_preset(self, preset: float = 1, name: str = 'pos1') -> Dict:
|
||||||
"""
|
"""
|
||||||
Removes the specified preset
|
Removes the specified preset
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._send_set_preset('PtzPreset', enable=0, preset=preset, name=name)
|
return self._send_set_preset(enable=0, preset=preset, name=name)
|
||||||
|
|
||||||
def move_right(self, speed=25):
|
def move_right(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the right
|
Move the camera to the right
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -48,7 +51,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('Right', speed=speed)
|
return self._send_operation('Right', speed=speed)
|
||||||
|
|
||||||
def move_right_up(self, speed=25):
|
def move_right_up(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the right and up
|
Move the camera to the right and up
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -56,7 +59,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('RightUp', speed=speed)
|
return self._send_operation('RightUp', speed=speed)
|
||||||
|
|
||||||
def move_right_down(self, speed=25):
|
def move_right_down(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the right and down
|
Move the camera to the right and down
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -64,7 +67,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('RightDown', speed=speed)
|
return self._send_operation('RightDown', speed=speed)
|
||||||
|
|
||||||
def move_left(self, speed=25):
|
def move_left(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the left
|
Move the camera to the left
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -72,7 +75,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('Left', speed=speed)
|
return self._send_operation('Left', speed=speed)
|
||||||
|
|
||||||
def move_left_up(self, speed=25):
|
def move_left_up(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the left and up
|
Move the camera to the left and up
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -80,7 +83,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('LeftUp', speed=speed)
|
return self._send_operation('LeftUp', speed=speed)
|
||||||
|
|
||||||
def move_left_down(self, speed=25):
|
def move_left_down(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera to the left and down
|
Move the camera to the left and down
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -88,7 +91,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('LeftDown', speed=speed)
|
return self._send_operation('LeftDown', speed=speed)
|
||||||
|
|
||||||
def move_up(self, speed=25):
|
def move_up(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera up.
|
Move the camera up.
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -96,7 +99,7 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('Up', speed=speed)
|
return self._send_operation('Up', speed=speed)
|
||||||
|
|
||||||
def move_down(self, speed=25):
|
def move_down(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera down.
|
Move the camera down.
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
@@ -104,14 +107,14 @@ class PtzAPIMixin:
|
|||||||
"""
|
"""
|
||||||
return self._send_operation('Down', speed=speed)
|
return self._send_operation('Down', speed=speed)
|
||||||
|
|
||||||
def stop_ptz(self):
|
def stop_ptz(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Stops the cameras current action.
|
Stops the cameras current action.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._send_noparm_operation('Stop')
|
return self._send_noparm_operation('Stop')
|
||||||
|
|
||||||
def auto_movement(self, speed=25):
|
def auto_movement(self, speed: float = 25) -> Dict:
|
||||||
"""
|
"""
|
||||||
Move the camera in a clockwise rotation.
|
Move the camera in a clockwise rotation.
|
||||||
The camera moves self.stop_ptz() is called.
|
The camera moves self.stop_ptz() is called.
|
||||||
72
reolinkapi/mixins/record.py
Normal file
72
reolinkapi/mixins/record.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class RecordAPIMixin:
|
||||||
|
"""API calls for the recording settings"""
|
||||||
|
|
||||||
|
def get_recording_encoding(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Get the current camera encoding settings for "Clear" and "Fluent" profiles.
|
||||||
|
See examples/response/GetEnc.json for example response data.
|
||||||
|
:return: response json
|
||||||
|
"""
|
||||||
|
body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}]
|
||||||
|
return self._execute_command('GetEnc', body)
|
||||||
|
|
||||||
|
def get_recording_advanced(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Get recording advanced setup data
|
||||||
|
See examples/response/GetRec.json for example response data.
|
||||||
|
:return: response json
|
||||||
|
"""
|
||||||
|
body = [{"cmd": "GetRec", "action": 1, "param": {"channel": 0}}]
|
||||||
|
return self._execute_command('GetRec', body)
|
||||||
|
|
||||||
|
def set_recording_encoding(self,
|
||||||
|
audio: float = 0,
|
||||||
|
main_bit_rate: float = 8192,
|
||||||
|
main_frame_rate: float = 8,
|
||||||
|
main_profile: str = 'High',
|
||||||
|
main_size: str = "2560*1440",
|
||||||
|
sub_bit_rate: float = 160,
|
||||||
|
sub_frame_rate: float = 7,
|
||||||
|
sub_profile: str = 'High',
|
||||||
|
sub_size: str = '640*480') -> Dict:
|
||||||
|
"""
|
||||||
|
Sets the current camera encoding settings for "Clear" and "Fluent" profiles.
|
||||||
|
:param audio: int Audio on or off
|
||||||
|
:param main_bit_rate: int Clear Bit Rate
|
||||||
|
:param main_frame_rate: int Clear Frame Rate
|
||||||
|
:param main_profile: string Clear Profile
|
||||||
|
:param main_size: string Clear Size
|
||||||
|
:param sub_bit_rate: int Fluent Bit Rate
|
||||||
|
:param sub_frame_rate: int Fluent Frame Rate
|
||||||
|
:param sub_profile: string Fluent Profile
|
||||||
|
:param sub_size: string Fluent Size
|
||||||
|
:return: response
|
||||||
|
"""
|
||||||
|
body = [
|
||||||
|
{
|
||||||
|
"cmd": "SetEnc",
|
||||||
|
"action": 0,
|
||||||
|
"param": {
|
||||||
|
"Enc": {
|
||||||
|
"audio": audio,
|
||||||
|
"channel": 0,
|
||||||
|
"mainStream": {
|
||||||
|
"bitRate": main_bit_rate,
|
||||||
|
"frameRate": main_frame_rate,
|
||||||
|
"profile": main_profile,
|
||||||
|
"size": main_size
|
||||||
|
},
|
||||||
|
"subStream": {
|
||||||
|
"bitRate": sub_bit_rate,
|
||||||
|
"frameRate": sub_frame_rate,
|
||||||
|
"profile": sub_profile,
|
||||||
|
"size": sub_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return self._execute_command('SetEnc', body)
|
||||||
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
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class SystemAPIMixin:
|
class SystemAPIMixin:
|
||||||
"""API for accessing general system information of the camera."""
|
"""API for accessing general system information of the camera."""
|
||||||
def get_general_system(self) -> object:
|
def get_general_system(self) -> Dict:
|
||||||
""":return: response json"""
|
""":return: response json"""
|
||||||
body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}]
|
body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}]
|
||||||
return self._execute_command('get_general_system', body, multi=True)
|
return self._execute_command('get_general_system', body, multi=True)
|
||||||
|
|
||||||
def get_performance(self) -> object:
|
def get_performance(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get a snapshot of the current performance of the camera.
|
Get a snapshot of the current performance of the camera.
|
||||||
See examples/response/GetPerformance.json for example response data.
|
See examples/response/GetPerformance.json for example response data.
|
||||||
@@ -14,7 +17,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "GetPerformance", "action": 0, "param": {}}]
|
body = [{"cmd": "GetPerformance", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetPerformance', body)
|
return self._execute_command('GetPerformance', body)
|
||||||
|
|
||||||
def get_information(self) -> object:
|
def get_information(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera information
|
Get the camera information
|
||||||
See examples/response/GetDevInfo.json for example response data.
|
See examples/response/GetDevInfo.json for example response data.
|
||||||
@@ -23,7 +26,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
|
body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetDevInfo', body)
|
return self._execute_command('GetDevInfo', body)
|
||||||
|
|
||||||
def reboot_camera(self) -> object:
|
def reboot_camera(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Reboots the camera
|
Reboots the camera
|
||||||
:return: response json
|
:return: response json
|
||||||
@@ -31,7 +34,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "Reboot", "action": 0, "param": {}}]
|
body = [{"cmd": "Reboot", "action": 0, "param": {}}]
|
||||||
return self._execute_command('Reboot', body)
|
return self._execute_command('Reboot', body)
|
||||||
|
|
||||||
def get_dst(self) -> object:
|
def get_dst(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera DST information
|
Get the camera DST information
|
||||||
See examples/response/GetDSTInfo.json for example response data.
|
See examples/response/GetDSTInfo.json for example response data.
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class UserAPIMixin:
|
class UserAPIMixin:
|
||||||
"""User-related API calls."""
|
"""User-related API calls."""
|
||||||
def get_online_user(self) -> object:
|
def get_online_user(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Return a list of current logged-in users in json format
|
Return a list of current logged-in users in json format
|
||||||
See examples/response/GetOnline.json for example response data.
|
See examples/response/GetOnline.json for example response data.
|
||||||
@@ -9,7 +12,7 @@ class UserAPIMixin:
|
|||||||
body = [{"cmd": "GetOnline", "action": 1, "param": {}}]
|
body = [{"cmd": "GetOnline", "action": 1, "param": {}}]
|
||||||
return self._execute_command('GetOnline', body)
|
return self._execute_command('GetOnline', body)
|
||||||
|
|
||||||
def get_users(self) -> object:
|
def get_users(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Return a list of user accounts from the camera in json format.
|
Return a list of user accounts from the camera in json format.
|
||||||
See examples/response/GetUser.json for example response data.
|
See examples/response/GetUser.json for example response data.
|
||||||
@@ -45,7 +48,7 @@ class UserAPIMixin:
|
|||||||
r_data = self._execute_command('ModifyUser', body)[0]
|
r_data = self._execute_command('ModifyUser', body)[0]
|
||||||
if r_data["value"]["rspCode"] == 200:
|
if r_data["value"]["rspCode"] == 200:
|
||||||
return True
|
return True
|
||||||
print("Could not modify user:", username, "\nCamera responded with:", r_data["value"])
|
print(f"Could not modify user: {username}\nCamera responded with: {r_data['value']}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_user(self, username: str) -> bool:
|
def delete_user(self, username: str) -> bool:
|
||||||
@@ -58,5 +61,5 @@ class UserAPIMixin:
|
|||||||
r_data = self._execute_command('DelUser', body)[0]
|
r_data = self._execute_command('DelUser', body)[0]
|
||||||
if r_data["value"]["rspCode"] == 200:
|
if r_data["value"]["rspCode"] == 200:
|
||||||
return True
|
return True
|
||||||
print("Could not delete user:", username, "\nCamera responded with:", r_data["value"])
|
print(f"Could not delete user: {username}\nCamera responded with: {r_data['value']}")
|
||||||
return False
|
return False
|
||||||
@@ -1,54 +1,57 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class ZoomAPIMixin:
|
class ZoomAPIMixin:
|
||||||
"""
|
"""
|
||||||
API for zooming and changing focus.
|
API for zooming and changing focus.
|
||||||
Note that the API does not allow zooming/focusing by absolute
|
Note that the API does not allow zooming/focusing by absolute
|
||||||
values rather that changing focus/zoom for a given time.
|
values rather that changing focus/zoom for a given time.
|
||||||
"""
|
"""
|
||||||
def _start_operation(self, operation, speed):
|
def _start_operation(self, operation: str, speed: float) -> Dict:
|
||||||
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": operation, "speed": speed}}]
|
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": operation, "speed": speed}}]
|
||||||
return self._execute_command('PtzCtrl', data)
|
return self._execute_command('PtzCtrl', data)
|
||||||
|
|
||||||
def _stop_zooming_or_focusing(self):
|
def _stop_zooming_or_focusing(self) -> Dict:
|
||||||
"""This command stops any ongoing zooming or focusing actions."""
|
"""This command stops any ongoing zooming or focusing actions."""
|
||||||
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": "Stop"}}]
|
data = [{"cmd": "PtzCtrl", "action": 0, "param": {"channel": 0, "op": "Stop"}}]
|
||||||
return self._execute_command('PtzCtrl', data)
|
return self._execute_command('PtzCtrl', data)
|
||||||
|
|
||||||
def start_zooming_in(self, speed=60):
|
def start_zooming_in(self, speed: float = 60) -> Dict:
|
||||||
"""
|
"""
|
||||||
The camera zooms in until self.stop_zooming() is called.
|
The camera zooms in until self.stop_zooming() is called.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._start_operation('ZoomInc', speed=speed)
|
return self._start_operation('ZoomInc', speed=speed)
|
||||||
|
|
||||||
def start_zooming_out(self, speed=60):
|
def start_zooming_out(self, speed: float = 60) -> Dict:
|
||||||
"""
|
"""
|
||||||
The camera zooms out until self.stop_zooming() is called.
|
The camera zooms out until self.stop_zooming() is called.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._start_operation('ZoomDec', speed=speed)
|
return self._start_operation('ZoomDec', speed=speed)
|
||||||
|
|
||||||
def stop_zooming(self):
|
def stop_zooming(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Stop zooming.
|
Stop zooming.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._stop_zooming_or_focusing()
|
return self._stop_zooming_or_focusing()
|
||||||
|
|
||||||
def start_focusing_in(self, speed=32):
|
def start_focusing_in(self, speed: float = 32) -> Dict:
|
||||||
"""
|
"""
|
||||||
The camera focuses in until self.stop_focusing() is called.
|
The camera focuses in until self.stop_focusing() is called.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._start_operation('FocusInc', speed=speed)
|
return self._start_operation('FocusInc', speed=speed)
|
||||||
|
|
||||||
def start_focusing_out(self, speed=32):
|
def start_focusing_out(self, speed: float = 32) -> Dict:
|
||||||
"""
|
"""
|
||||||
The camera focuses out until self.stop_focusing() is called.
|
The camera focuses out until self.stop_focusing() is called.
|
||||||
:return: response json
|
:return: response json
|
||||||
"""
|
"""
|
||||||
return self._start_operation('FocusDec', speed=speed)
|
return self._start_operation('FocusDec', speed=speed)
|
||||||
|
|
||||||
def stop_focusing(self):
|
def stop_focusing(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Stop focusing.
|
Stop focusing.
|
||||||
:return: response json
|
:return: response json
|
||||||
0
reolinkapi/utils/__init__.py
Normal file
0
reolinkapi/utils/__init__.py
Normal file
@@ -1,20 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
from threading import ThreadError
|
from threading import ThreadError
|
||||||
|
from typing import Any
|
||||||
import cv2
|
import cv2
|
||||||
|
from reolinkapi.utils.util import threaded
|
||||||
from 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
|
||||||
- https://stackoverflow.com/questions/55828451/video-streaming-from-ip-camera-in-python-using-opencv-cv2-videocapture
|
- https://stackoverflow.com/questions/55828451/video-streaming-from-ip-camera-in-python-using-opencv-cv2-videocapture
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip, username, password, port=554, profile="main", use_udp=True, callback=None, **kwargs):
|
def __init__(self, ip: str, username: str, password: str, port: float = 554, profile: str = "main",
|
||||||
|
use_udp: bool = True, callback: Any = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
RTSP client is used to retrieve frames from the camera in a stream
|
RTSP client is used to retrieve frames from the camera in a stream
|
||||||
|
|
||||||
@@ -36,12 +37,8 @@ class RtspClient:
|
|||||||
self.password = password
|
self.password = password
|
||||||
self.port = port
|
self.port = port
|
||||||
self.proxy = kwargs.get("proxies")
|
self.proxy = kwargs.get("proxies")
|
||||||
self.url = "rtsp://" + self.username + ":" + self.password + "@" + \
|
self.url = f'rtsp://{self.username}:{self.password}@{self.ip}:{self.port}//h264Preview_01_{profile}'
|
||||||
self.ip + ":" + str(self.port) + "//h264Preview_01_" + profile
|
capture_options += 'udp' if use_udp else 'tcp'
|
||||||
if use_udp:
|
|
||||||
capture_options = capture_options + 'udp'
|
|
||||||
else:
|
|
||||||
capture_options = capture_options + 'tcp'
|
|
||||||
|
|
||||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = capture_options
|
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = capture_options
|
||||||
|
|
||||||
@@ -91,9 +88,6 @@ class RtspClient:
|
|||||||
"""
|
"""
|
||||||
Opens OpenCV Video stream and returns the result according to the OpenCV documentation
|
Opens OpenCV Video stream and returns the result according to the OpenCV documentation
|
||||||
https://docs.opencv.org/3.4/d8/dfe/classcv_1_1VideoCapture.html#a473055e77dd7faa4d26d686226b292c1
|
https://docs.opencv.org/3.4/d8/dfe/classcv_1_1VideoCapture.html#a473055e77dd7faa4d26d686226b292c1
|
||||||
|
|
||||||
:param callback: The function to callback the cv::mat frame to if required to be non-blocking. If this is left
|
|
||||||
as None, then the function returns a generator which is blocking.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Reset the capture object
|
# Reset the capture object
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
requests
|
numpy==1.19.4
|
||||||
opencv-python
|
opencv-python==4.4.0.46
|
||||||
numpy
|
Pillow==8.0.1
|
||||||
socks
|
PySocks==1.7.1
|
||||||
|
PyYaml==5.3.1
|
||||||
|
requests>=2.18.4
|
||||||
71
setup.py
71
setup.py
@@ -1,31 +1,8 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import codecs
|
import codecs
|
||||||
from setuptools import setup
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
# Package meta-data.
|
|
||||||
NAME = 'reolink-api'
|
|
||||||
DESCRIPTION = 'Reolink Camera API written in Python 3.6'
|
|
||||||
URL = 'https://github.com/Benehiko/ReolinkCameraAPI'
|
|
||||||
AUTHOR_EMAIL = ''
|
|
||||||
AUTHOR = 'Benehiko'
|
|
||||||
LICENSE = 'GPL-3.0'
|
|
||||||
INSTALL_REQUIRES = [
|
|
||||||
'pillow',
|
|
||||||
'pyyaml',
|
|
||||||
'requests>=2.18.4',
|
|
||||||
'numpy',
|
|
||||||
'opencv-python',
|
|
||||||
'pysocks'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
# read the contents of your README file
|
|
||||||
with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
|
|
||||||
long_description = f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def read(*parts):
|
def read(*parts):
|
||||||
@@ -41,9 +18,33 @@ def find_version(*file_paths):
|
|||||||
raise RuntimeError("Unable to find version string.")
|
raise RuntimeError("Unable to find version string.")
|
||||||
|
|
||||||
|
|
||||||
setup(name=NAME,
|
# Package meta-data.
|
||||||
|
NAME = 'reolinkapi'
|
||||||
|
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 = [
|
||||||
|
'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',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
# read the contents of your README file
|
||||||
|
with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name=NAME,
|
||||||
python_requires='>=3.6.0',
|
python_requires='>=3.6.0',
|
||||||
version=find_version('api', '__init__.py'),
|
version=find_version('reolinkapi', '__init__.py'),
|
||||||
description=DESCRIPTION,
|
description=DESCRIPTION,
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
@@ -52,21 +53,5 @@ setup(name=NAME,
|
|||||||
url=URL,
|
url=URL,
|
||||||
license=LICENSE,
|
license=LICENSE,
|
||||||
install_requires=INSTALL_REQUIRES,
|
install_requires=INSTALL_REQUIRES,
|
||||||
py_modules=[
|
packages=find_packages(exclude=['examples', 'tests'])
|
||||||
'Camera',
|
|
||||||
'ConfigHandler',
|
|
||||||
'RtspClient',
|
|
||||||
'resthandle',
|
|
||||||
'api.APIHandler',
|
|
||||||
'api.device',
|
|
||||||
'api.display',
|
|
||||||
'api.network',
|
|
||||||
'api.ptz',
|
|
||||||
'api.recording',
|
|
||||||
'api.system',
|
|
||||||
'api.user',
|
|
||||||
'api.zoom',
|
|
||||||
'api.alarm',
|
|
||||||
'api.image'
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|||||||
5
test.py
5
test.py
@@ -1,5 +0,0 @@
|
|||||||
from Camera import Camera
|
|
||||||
|
|
||||||
c = Camera("192.168.1.112", "admin", "jUa2kUzi")
|
|
||||||
# print("Getting information", c.get_information())
|
|
||||||
c.open_video_stream()
|
|
||||||
40
tests/test_camera.py
Normal file
40
tests/test_camera.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
import unittest
|
||||||
|
from reolinkapi import Camera
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(props_path: str) -> dict:
|
||||||
|
"""Reads in a properties file into variables.
|
||||||
|
|
||||||
|
NB! this config file is kept out of commits with .gitignore. The structure of this file is such:
|
||||||
|
# secrets.cfg
|
||||||
|
[camera]
|
||||||
|
ip={ip_address}
|
||||||
|
username={username}
|
||||||
|
password={password}
|
||||||
|
"""
|
||||||
|
config = RawConfigParser()
|
||||||
|
assert os.path.exists(props_path), f"Path does not exist: {props_path}"
|
||||||
|
config.read(props_path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class TestCamera(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
cls.config = read_config('../secrets.cfg')
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.cam = Camera(self.config.get('camera', 'ip'), self.config.get('camera', 'username'),
|
||||||
|
self.config.get('camera', 'password'))
|
||||||
|
|
||||||
|
def test_camera(self):
|
||||||
|
"""Test that camera connects and gets a token"""
|
||||||
|
self.assertTrue(self.cam.ip == self.config.get('camera', 'ip'))
|
||||||
|
self.assertTrue(self.cam.token != '')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user