Enforce lowercase standard on all submodule files, improve type hinting throughout, complete first pass for logic issues
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
138
reolink_api/api_handler.py
Normal 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
24
reolink_api/camera.py
Normal 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()
|
||||||
17
reolink_api/config_handler.py
Normal file
17
reolink_api/config_handler.py
Normal 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
|
||||||
@@ -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,
|
||||||
|
"channel": channel,
|
||||||
|
"osdChannel": {
|
||||||
|
"enable": osd_channel_enabled, "name": osd_channel_name,
|
||||||
|
"pos": osd_channel_pos
|
||||||
|
},
|
||||||
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
|
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
|
||||||
}
|
}}}]
|
||||||
}}]
|
|
||||||
r_data = self._execute_command('SetOsd', body)[0]
|
r_data = self._execute_command('SetOsd', body)[0]
|
||||||
if r_data["value"]["rspCode"] == 200:
|
if r_data["value"]["rspCode"] == 200:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
{
|
||||||
|
"cmd": "SetEnc",
|
||||||
"action": 0,
|
"action": 0,
|
||||||
"param":
|
"param": {
|
||||||
{"Enc":
|
"Enc": {
|
||||||
{"audio": audio,
|
"audio": audio,
|
||||||
"channel": 0,
|
"channel": 0,
|
||||||
"mainStream": {
|
"mainStream": {
|
||||||
"bitRate": main_bit_rate,
|
"bitRate": main_bit_rate,
|
||||||
"frameRate": main_frame_rate,
|
"frameRate": main_frame_rate,
|
||||||
"profile": main_profile,
|
"profile": main_profile,
|
||||||
"size": main_size},
|
"size": main_size
|
||||||
|
},
|
||||||
"subStream": {
|
"subStream": {
|
||||||
"bitRate": sub_bit_rate,
|
"bitRate": sub_bit_rate,
|
||||||
"frameRate": sub_frame_rate,
|
"frameRate": sub_frame_rate,
|
||||||
"profile": sub_profile,
|
"profile": sub_profile,
|
||||||
"size": sub_size}}
|
"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:
|
||||||
|
|||||||
@@ -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
110
reolink_api/rtsp_client.py
Normal 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()
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class SystemAPIMixin:
|
class SystemAPIMixin:
|
||||||
"""API for accessing general system information of the camera."""
|
"""API for accessing general system information of the camera."""
|
||||||
def get_general_system(self) -> object:
|
def get_general_system(self) -> Dict:
|
||||||
""":return: response json"""
|
""":return: response json"""
|
||||||
body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}]
|
body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}]
|
||||||
return self._execute_command('get_general_system', body, multi=True)
|
return self._execute_command('get_general_system', body, multi=True)
|
||||||
|
|
||||||
def get_performance(self) -> object:
|
def get_performance(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get a snapshot of the current performance of the camera.
|
Get a snapshot of the current performance of the camera.
|
||||||
See examples/response/GetPerformance.json for example response data.
|
See examples/response/GetPerformance.json for example response data.
|
||||||
@@ -14,7 +17,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "GetPerformance", "action": 0, "param": {}}]
|
body = [{"cmd": "GetPerformance", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetPerformance', body)
|
return self._execute_command('GetPerformance', body)
|
||||||
|
|
||||||
def get_information(self) -> object:
|
def get_information(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera information
|
Get the camera information
|
||||||
See examples/response/GetDevInfo.json for example response data.
|
See examples/response/GetDevInfo.json for example response data.
|
||||||
@@ -23,7 +26,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
|
body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
|
||||||
return self._execute_command('GetDevInfo', body)
|
return self._execute_command('GetDevInfo', body)
|
||||||
|
|
||||||
def reboot_camera(self) -> object:
|
def reboot_camera(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Reboots the camera
|
Reboots the camera
|
||||||
:return: response json
|
:return: response json
|
||||||
@@ -31,7 +34,7 @@ class SystemAPIMixin:
|
|||||||
body = [{"cmd": "Reboot", "action": 0, "param": {}}]
|
body = [{"cmd": "Reboot", "action": 0, "param": {}}]
|
||||||
return self._execute_command('Reboot', body)
|
return self._execute_command('Reboot', body)
|
||||||
|
|
||||||
def get_dst(self) -> object:
|
def get_dst(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get the camera DST information
|
Get the camera DST information
|
||||||
See examples/response/GetDSTInfo.json for example response data.
|
See examples/response/GetDSTInfo.json for example response data.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class UserAPIMixin:
|
class UserAPIMixin:
|
||||||
"""User-related API calls."""
|
"""User-related API calls."""
|
||||||
def get_online_user(self) -> object:
|
def get_online_user(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Return a list of current logged-in users in json format
|
Return a list of current logged-in users in json format
|
||||||
See examples/response/GetOnline.json for example response data.
|
See examples/response/GetOnline.json for example response data.
|
||||||
@@ -9,7 +12,7 @@ class UserAPIMixin:
|
|||||||
body = [{"cmd": "GetOnline", "action": 1, "param": {}}]
|
body = [{"cmd": "GetOnline", "action": 1, "param": {}}]
|
||||||
return self._execute_command('GetOnline', body)
|
return self._execute_command('GetOnline', body)
|
||||||
|
|
||||||
def get_users(self) -> object:
|
def get_users(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Return a list of user accounts from the camera in json format.
|
Return a list of user accounts from the camera in json format.
|
||||||
See examples/response/GetUser.json for example response data.
|
See examples/response/GetUser.json for example response data.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user