Merge pull request #37 from barretobrock/develop

Refactor package to be imported as a single package
This commit is contained in:
Alano Terblanche
2020-12-19 22:34:37 +02:00
committed by GitHub
35 changed files with 601 additions and 361 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
secrets.cfg
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@@ -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()

View File

@@ -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

View File

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

View File

@@ -1,4 +0,0 @@
from .APIHandler import APIHandler
__version__ = "0.0.5"
VERSION = __version__

View File

@@ -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
View File

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

View File

@@ -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'))

View File

@@ -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
View 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
View 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()

View File

View 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,8 +118,24 @@ class APIHandler(SystemAPIMixin,
try: try:
if self.token is None: if self.token is None:
raise ValueError("Login first") raise ValueError("Login first")
response = Request.post(self.url, data=data, params=params) if command == 'Download':
return response.json() # 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)
return response.json()
except Exception as e: except Exception as e:
print(f"Command {command} failed: {e}") print(f"Command {command} failed: {e}")
raise raise

View File

@@ -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)

View File

View 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.

View File

@@ -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:

View File

@@ -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,
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos} "channel": channel,
} "osdChannel": {
}}] "enable": osd_channel_enabled, "name": osd_channel_name,
"pos": osd_channel_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

View 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

View File

@@ -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.

View 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

View File

@@ -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.

View File

@@ -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.

View 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)

View File

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

View File

@@ -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,11 +34,11 @@ 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.
:return: response json :return: response json
""" """
body = [{"cmd": "GetTime", "action": 0, "param": {}}] body = [{"cmd": "GetTime", "action": 0, "param": {}}]
return self._execute_command('GetTime', body) return self._execute_command('GetTime', body)

View File

@@ -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

View File

@@ -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

View File

View 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

View File

@@ -8,4 +8,4 @@ def threaded(fn):
thread.start() thread.start()
return thread return thread
return wrapper return wrapper

View File

@@ -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

View File

@@ -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,32 +18,40 @@ 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.
python_requires='>=3.6.0', NAME = 'reolinkapi'
version=find_version('api', '__init__.py'), DESCRIPTION = 'Reolink Camera API client written in Python 3'
description=DESCRIPTION, URL = 'https://github.com/ReolinkCameraAPI/reolinkapipy'
long_description=long_description, AUTHOR_EMAIL = 'alanoterblanche@gmail.com'
long_description_content_type='text/markdown', AUTHOR = 'Benehiko'
author=AUTHOR, LICENSE = 'GPL-3.0'
author_email=AUTHOR_EMAIL, INSTALL_REQUIRES = [
url=URL, 'numpy==1.19.4',
license=LICENSE, 'opencv-python==4.4.0.46',
install_requires=INSTALL_REQUIRES, 'Pillow==8.0.1',
py_modules=[ 'PySocks==1.7.1',
'Camera', 'PyYaml==5.3.1',
'ConfigHandler', 'requests>=2.18.4',
'RtspClient', ]
'resthandle',
'api.APIHandler',
'api.device', here = os.path.abspath(os.path.dirname(__file__))
'api.display', # read the contents of your README file
'api.network', with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
'api.ptz', long_description = f.read()
'api.recording',
'api.system',
'api.user', setup(
'api.zoom', name=NAME,
'api.alarm', python_requires='>=3.6.0',
'api.image' version=find_version('reolinkapi', '__init__.py'),
] description=DESCRIPTION,
) long_description=long_description,
long_description_content_type='text/markdown',
author=AUTHOR,
author_email=AUTHOR_EMAIL,
url=URL,
license=LICENSE,
install_requires=INSTALL_REQUIRES,
packages=find_packages(exclude=['examples', 'tests'])
)

View File

@@ -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
View 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()