Enforce lowercase standard on all submodule files, improve type hinting throughout, complete first pass for logic issues

This commit is contained in:
Bobrock
2020-12-18 09:05:56 -06:00
parent 17bc207e3b
commit 63537f9daf
16 changed files with 456 additions and 132 deletions

View File

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

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.

138
reolink_api/api_handler.py Normal file
View File

@@ -0,0 +1,138 @@
import requests
from typing import Dict, List, Optional, Union
from reolink_api.alarm import AlarmAPIMixin
from reolink_api.device import DeviceAPIMixin
from reolink_api.display import DisplayAPIMixin
from reolink_api.download import DownloadAPIMixin
from reolink_api.image import ImageAPIMixin
from reolink_api.motion import MotionAPIMixin
from reolink_api.network import NetworkAPIMixin
from reolink_api.ptz import PtzAPIMixin
from reolink_api.recording import RecordingAPIMixin
from reolink_api.resthandle import Request
from reolink_api.system import SystemAPIMixin
from reolink_api.user import UserAPIMixin
from reolink_api.zoom import ZoomAPIMixin
class APIHandler(AlarmAPIMixin,
DeviceAPIMixin,
DisplayAPIMixin,
DownloadAPIMixin,
ImageAPIMixin,
MotionAPIMixin,
NetworkAPIMixin,
PtzAPIMixin,
RecordingAPIMixin,
SystemAPIMixin,
UserAPIMixin,
ZoomAPIMixin):
"""
The APIHandler class is the backend part of the API, the actual API calls
are implemented in Mixins.
This handles communication directly with the camera.
Current camera's tested: RLC-411WS
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: bool = False, **kwargs):
"""
Initialise the Camera API Handler (maps api calls into python)
:param ip:
:param username:
:param password:
: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
"""
scheme = 'https' if https else 'http'
self.url = f"{scheme}://{ip}/cgi-bin/api.cgi"
self.ip = ip
self.token = None
self.username = username
self.password = password
Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found
def login(self) -> bool:
"""
Get login token
Must be called first, before any other operation can be performed
:return: bool
"""
try:
body = [{"cmd": "Login", "action": 0,
"param": {"User": {"userName": self.username, "password": self.password}}}]
param = {"cmd": "Login", "token": "null"}
response = Request.post(self.url, data=body, params=param)
if response is not None:
data = response.json()[0]
code = data["code"]
if int(code) == 0:
self.token = data["value"]["Token"]["name"]
print("Login success")
return True
print(self.token)
return False
else:
# 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
except Exception as e:
print("Error Login\n", e)
raise
def logout(self) -> bool:
"""
Logout of the camera
:return: bool
"""
try:
data = [{"cmd": "Logout", "action": 0}]
self._execute_command('Logout', data)
# print(ret)
return True
except Exception as e:
print("Error Logout\n", e)
return 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.
:param command: name of the command to send
:param data: object to send to the camera (send as json)
:param multi: whether the given command name should be added to the
url parameters of the request. Defaults to False. (Some multi-step
commands seem to not have a single command name)
:return: response JSON as python object
"""
params = {"token": self.token, 'cmd': command}
if multi:
del params['cmd']
try:
if self.token is None:
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)
return response.json()
except Exception as e:
print(f"Command {command} failed: {e}")
raise

24
reolink_api/camera.py Normal file
View File

@@ -0,0 +1,24 @@
from .api_handler import APIHandler
class Camera(APIHandler):
def __init__(self, ip: str, username: str = "admin", password: str = "", https: bool = 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

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

View File

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

@@ -1,3 +1,5 @@
from typing import Dict
class ImageAPIMixin: class ImageAPIMixin:
"""API calls for image settings.""" """API calls for image settings."""
@@ -5,20 +7,20 @@ class ImageAPIMixin:
def set_adv_image_settings(self, def set_adv_image_settings(self,
anti_flicker: str = 'Outdoor', anti_flicker: str = 'Outdoor',
exposure: str = 'Auto', exposure: str = 'Auto',
gain_min: int = 1, gain_min: float = 1,
gain_max: int = 62, gain_max: float = 62,
shutter_min: int = 1, shutter_min: float = 1,
shutter_max: int = 125, shutter_max: float = 125,
blue_gain: int = 128, blue_gain: float = 128,
red_gain: int = 128, red_gain: float = 128,
white_balance: str = 'Auto', white_balance: str = 'Auto',
day_night: str = 'Auto', day_night: str = 'Auto',
back_light: str = 'DynamicRangeControl', back_light: str = 'DynamicRangeControl',
blc: int = 128, blc: float = 128,
drc: int = 128, drc: float = 128,
rotation: int = 0, rotation: float = 0,
mirroring: int = 0, mirroring: float = 0,
nr3d: int = 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: int = 128, brightness: float = 128,
contrast: int = 62, contrast: float = 62,
hue: int = 1, hue: float = 1,
saturation: int = 125, saturation: float = 125,
sharpness: int = 128) -> object: sharpness: float = 128) -> Dict:
""" """
Sets the camera image settings. Sets the camera image settings.

View File

@@ -1,3 +1,6 @@
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: int = 80, https_port: int = 443, media_port: int = 9000, def set_net_port(self, http_port: int = 80, https_port: int = 443, media_port: int = 9000,
@@ -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: str, password: str) -> 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

@@ -3,14 +3,15 @@ import random
import string import string
from urllib import parse from urllib import parse
from io import BytesIO from io import BytesIO
from PIL import Image from typing import Dict, Any, Optional
from reolink_api.RtspClient import RtspClient from PIL.Image import Image, open as open_image
from reolink_api.rtsp_client import RtspClient
class RecordingAPIMixin: class RecordingAPIMixin:
"""API calls for recording/streaming image or video.""" """API calls for recording/streaming image or video."""
def get_recording_encoding(self) -> object: def get_recording_encoding(self) -> Dict:
""" """
Get the current camera encoding settings for "Clear" and "Fluent" profiles. Get the current camera encoding settings for "Clear" and "Fluent" profiles.
See examples/response/GetEnc.json for example response data. See examples/response/GetEnc.json for example response data.
@@ -19,7 +20,7 @@ class RecordingAPIMixin:
body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}] body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}]
return self._execute_command('GetEnc', body) return self._execute_command('GetEnc', body)
def get_recording_advanced(self) -> object: def get_recording_advanced(self) -> Dict:
""" """
Get recording advanced setup data Get recording advanced setup data
See examples/response/GetRec.json for example response data. See examples/response/GetRec.json for example response data.
@@ -29,15 +30,15 @@ class RecordingAPIMixin:
return self._execute_command('GetRec', body) return self._execute_command('GetRec', body)
def set_recording_encoding(self, def set_recording_encoding(self,
audio=0, audio: float = 0,
main_bit_rate=8192, main_bit_rate: float = 8192,
main_frame_rate=8, main_frame_rate: float = 8,
main_profile='High', main_profile: str = 'High',
main_size="2560*1440", main_size: str = "2560*1440",
sub_bit_rate=160, sub_bit_rate: float = 160,
sub_frame_rate=7, sub_frame_rate: float = 7,
sub_profile='High', sub_profile: str = 'High',
sub_size='640*480') -> object: sub_size: str = '640*480') -> Dict:
""" """
Sets the current camera encoding settings for "Clear" and "Fluent" profiles. Sets the current camera encoding settings for "Clear" and "Fluent" profiles.
:param audio: int Audio on or off :param audio: int Audio on or off
@@ -51,59 +52,67 @@ class RecordingAPIMixin:
:param sub_size: string Fluent Size :param sub_size: string Fluent Size
:return: response :return: response
""" """
body = [{"cmd": "SetEnc", body = [
"action": 0, {
"param": "cmd": "SetEnc",
{"Enc": "action": 0,
{"audio": audio, "param": {
"channel": 0, "Enc": {
"mainStream": { "audio": audio,
"bitRate": main_bit_rate, "channel": 0,
"frameRate": main_frame_rate, "mainStream": {
"profile": main_profile, "bitRate": main_bit_rate,
"size": main_size}, "frameRate": main_frame_rate,
"subStream": { "profile": main_profile,
"bitRate": sub_bit_rate, "size": main_size
"frameRate": sub_frame_rate, },
"profile": sub_profile, "subStream": {
"size": sub_size}} "bitRate": sub_bit_rate,
}}] "frameRate": sub_frame_rate,
"profile": sub_profile,
"size": sub_size
}
}
}
}
]
return self._execute_command('SetEnc', body) return self._execute_command('SetEnc', body)
########### ###########
# RTSP Stream # RTSP Stream
########### ###########
def open_video_stream(self, callback=None, profile: str = "main", proxies=None): 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' '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 Blocking function creates a generator and returns the frames as it is spawned
:param profile: profile is "main" or "sub" :param callback:
:param proxies: Default is none, example: {"host": "localhost", "port": 8000} :param proxies: Default is none, example: {"host": "localhost", "port": 8000}
""" """
rtsp_client = RtspClient( rtsp_client = RtspClient(
ip=self.ip, username=self.username, password=self.password, proxies=proxies, callback=callback) ip=self.ip, username=self.username, password=self.password, proxies=proxies, callback=callback)
return rtsp_client.open_stream() return rtsp_client.open_stream()
def get_snap(self, timeout: int = 3, proxies=None) -> Image or None: 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 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 timeout: Request timeout to camera in seconds
:param proxies: http/https proxies to pass to the request object. :param proxies: http/https proxies to pass to the request object.
:return: Image or None :return: Image or None
""" """
data = {} data = {
data['cmd'] = 'Snap' 'cmd': 'Snap',
data['channel'] = 0 'channel': 0,
data['rs'] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) 'rs': ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)),
data['user'] = self.username 'user': self.username,
data['password'] = self.password 'password': self.password,
}
parms = parse.urlencode(data).encode("utf-8") parms = parse.urlencode(data).encode("utf-8")
try: try:
response = requests.get(self.url, proxies=proxies, params=parms, timeout=timeout) response = requests.get(self.url, proxies=proxies, params=parms, timeout=timeout)
if response.status_code == 200: if response.status_code == 200:
return Image.open(BytesIO(response.content)) return open_image(BytesIO(response.content))
print("Could not retrieve data from camera successfully. Status:", response.stats_code) print("Could not retrieve data from camera successfully. Status:", response.status_code)
return None return None
except Exception as e: except Exception as e:

View File

@@ -1,12 +1,14 @@
import json 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:
@@ -18,10 +20,6 @@ class Request:
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
r = requests.post(url, verify=False, params=params, json=data, headers=headers, r = requests.post(url, verify=False, params=params, json=data, headers=headers,
proxies=Request.proxies) proxies=Request.proxies)
# if params is not None:
# 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 +29,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 +39,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)

110
reolink_api/rtsp_client.py Normal file
View File

@@ -0,0 +1,110 @@
import os
from threading import ThreadError
from typing import Any
import cv2
from reolink_api.util import threaded
class RtspClient:
"""
Inspiration from:
- https://benhowell.github.io/guide/2015/03/09/opencv-and-web-cam-streaming
- https://stackoverflow.com/questions/19846332/python-threading-inside-a-class
- https://stackoverflow.com/questions/55828451/video-streaming-from-ip-camera-in-python-using-opencv-cv2-videocapture
"""
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
:param ip: Camera IP
:param username: Camera Username
:param password: Camera User Password
:param port: RTSP port
:param profile: "main" or "sub"
:param use_upd: True to use UDP, False to use TCP
:param proxies: {"host": "localhost", "port": 8000}
"""
self.capture = None
self.thread_cancelled = False
self.callback = callback
capture_options = 'rtsp_transport;'
self.ip = ip
self.username = username
self.password = password
self.port = port
self.proxy = kwargs.get("proxies")
self.url = f'rtsp://{self.username}:{self.password}@{self.ip}:{self.port}//h264Preview_01_{profile}'
if use_udp:
capture_options = capture_options + 'udp'
else:
capture_options = capture_options + 'tcp'
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = capture_options
# opens the stream capture, but does not retrieve any frames yet.
self._open_video_capture()
def _open_video_capture(self):
# To CAP_FFMPEG or not To ?
self.capture = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG)
def _stream_blocking(self):
while True:
try:
if self.capture.isOpened():
ret, frame = self.capture.read()
if ret:
yield frame
else:
print("stream closed")
self.capture.release()
return
except Exception as e:
print(e)
self.capture.release()
return
@threaded
def _stream_non_blocking(self):
while not self.thread_cancelled:
try:
if self.capture.isOpened():
ret, frame = self.capture.read()
if ret:
self.callback(frame)
else:
print("stream is closed")
self.stop_stream()
except ThreadError as e:
print(e)
self.stop_stream()
def stop_stream(self):
self.capture.release()
self.thread_cancelled = True
def open_stream(self):
"""
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
: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
if self.capture is None or not self.capture.isOpened():
self._open_video_capture()
print("opening stream")
if self.callback is None:
return self._stream_blocking()
else:
# reset the thread status if the object was not re-created
if not self.thread_cancelled:
self.thread_cancelled = False
return self._stream_non_blocking()

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

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.

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