Skip to content

Commit 63537f9

Browse files
author
Bobrock
committed
Enforce lowercase standard on all submodule files, improve type hinting throughout, complete first pass for logic issues
1 parent 17bc207 commit 63537f9

File tree

16 files changed

+456
-132
lines changed

16 files changed

+456
-132
lines changed

reolink_api/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .APIHandler import APIHandler
2-
from .Camera import Camera
1+
from .api_handler import APIHandler
2+
from .camera import Camera
33

4-
__version__ = "0.1.1"
4+
__version__ = "0.1.2"

reolink_api/alarm.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from typing import Dict
2+
3+
14
class AlarmAPIMixin:
25
"""API calls for getting device alarm information."""
36

4-
def get_alarm_motion(self) -> object:
7+
def get_alarm_motion(self) -> Dict:
58
"""
69
Gets the device alarm motion
710
See examples/response/GetAlarmMotion.json for example response data.

reolink_api/api_handler.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import requests
2+
from typing import Dict, List, Optional, Union
3+
from reolink_api.alarm import AlarmAPIMixin
4+
from reolink_api.device import DeviceAPIMixin
5+
from reolink_api.display import DisplayAPIMixin
6+
from reolink_api.download import DownloadAPIMixin
7+
from reolink_api.image import ImageAPIMixin
8+
from reolink_api.motion import MotionAPIMixin
9+
from reolink_api.network import NetworkAPIMixin
10+
from reolink_api.ptz import PtzAPIMixin
11+
from reolink_api.recording import RecordingAPIMixin
12+
from reolink_api.resthandle import Request
13+
from reolink_api.system import SystemAPIMixin
14+
from reolink_api.user import UserAPIMixin
15+
from reolink_api.zoom import ZoomAPIMixin
16+
17+
18+
class APIHandler(AlarmAPIMixin,
19+
DeviceAPIMixin,
20+
DisplayAPIMixin,
21+
DownloadAPIMixin,
22+
ImageAPIMixin,
23+
MotionAPIMixin,
24+
NetworkAPIMixin,
25+
PtzAPIMixin,
26+
RecordingAPIMixin,
27+
SystemAPIMixin,
28+
UserAPIMixin,
29+
ZoomAPIMixin):
30+
"""
31+
The APIHandler class is the backend part of the API, the actual API calls
32+
are implemented in Mixins.
33+
This handles communication directly with the camera.
34+
Current camera's tested: RLC-411WS
35+
36+
All Code will try to follow the PEP 8 standard as described here: https://www.python.org/dev/peps/pep-0008/
37+
"""
38+
39+
def __init__(self, ip: str, username: str, password: str, https: bool = False, **kwargs):
40+
"""
41+
Initialise the Camera API Handler (maps api calls into python)
42+
:param ip:
43+
:param username:
44+
:param password:
45+
:param proxy: Add a proxy dict for requests to consume.
46+
eg: {"http":"socks5://[username]:[password]@[host]:[port], "https": ...}
47+
More information on proxies in requests: https://stackoverflow.com/a/15661226/9313679
48+
"""
49+
scheme = 'https' if https else 'http'
50+
self.url = f"{scheme}://{ip}/cgi-bin/api.cgi"
51+
self.ip = ip
52+
self.token = None
53+
self.username = username
54+
self.password = password
55+
Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found
56+
57+
def login(self) -> bool:
58+
"""
59+
Get login token
60+
Must be called first, before any other operation can be performed
61+
:return: bool
62+
"""
63+
try:
64+
body = [{"cmd": "Login", "action": 0,
65+
"param": {"User": {"userName": self.username, "password": self.password}}}]
66+
param = {"cmd": "Login", "token": "null"}
67+
response = Request.post(self.url, data=body, params=param)
68+
if response is not None:
69+
data = response.json()[0]
70+
code = data["code"]
71+
if int(code) == 0:
72+
self.token = data["value"]["Token"]["name"]
73+
print("Login success")
74+
return True
75+
print(self.token)
76+
return False
77+
else:
78+
# TODO: Verify this change w/ owner. Delete old code if acceptable.
79+
# A this point, response is NoneType. There won't be a status code property.
80+
# print("Failed to login\nStatus Code:", response.status_code)
81+
print("Failed to login\nResponse was null.")
82+
return False
83+
except Exception as e:
84+
print("Error Login\n", e)
85+
raise
86+
87+
def logout(self) -> bool:
88+
"""
89+
Logout of the camera
90+
:return: bool
91+
"""
92+
try:
93+
data = [{"cmd": "Logout", "action": 0}]
94+
self._execute_command('Logout', data)
95+
# print(ret)
96+
return True
97+
except Exception as e:
98+
print("Error Logout\n", e)
99+
return False
100+
101+
def _execute_command(self, command: str, data: List[Dict], multi: bool = False) -> \
102+
Optional[Union[Dict, bool]]:
103+
"""
104+
Send a POST request to the IP camera with given data.
105+
:param command: name of the command to send
106+
:param data: object to send to the camera (send as json)
107+
:param multi: whether the given command name should be added to the
108+
url parameters of the request. Defaults to False. (Some multi-step
109+
commands seem to not have a single command name)
110+
:return: response JSON as python object
111+
"""
112+
params = {"token": self.token, 'cmd': command}
113+
if multi:
114+
del params['cmd']
115+
try:
116+
if self.token is None:
117+
raise ValueError("Login first")
118+
if command == 'Download':
119+
# Special handling for downloading an mp4
120+
# Pop the filepath from data
121+
tgt_filepath = data[0].pop('filepath')
122+
# Apply the data to the params
123+
params.update(data[0])
124+
with requests.get(self.url, params=params, stream=True) as req:
125+
if req.status_code == 200:
126+
with open(tgt_filepath, 'wb') as f:
127+
f.write(req.content)
128+
return True
129+
else:
130+
print(f'Error received: {req.status_code}')
131+
return False
132+
133+
else:
134+
response = Request.post(self.url, data=data, params=params)
135+
return response.json()
136+
except Exception as e:
137+
print(f"Command {command} failed: {e}")
138+
raise

reolink_api/camera.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from .api_handler import APIHandler
2+
3+
4+
class Camera(APIHandler):
5+
6+
def __init__(self, ip: str, username: str = "admin", password: str = "", https: bool = False):
7+
"""
8+
Initialise the Camera object by passing the ip address.
9+
The default details {"username":"admin", "password":""} will be used if nothing passed
10+
:param ip:
11+
:param username:
12+
:param password:
13+
"""
14+
# For when you need to connect to a camera behind a proxy, pass
15+
# a proxy argument: proxy={"http": "socks5://127.0.0.1:8000"}
16+
APIHandler.__init__(self, ip, username, password, https=https)
17+
18+
# Normal call without proxy:
19+
# APIHandler.__init__(self, ip, username, password)
20+
21+
self.ip = ip
22+
self.username = username
23+
self.password = password
24+
super().login()

reolink_api/config_handler.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import io
2+
import yaml
3+
from typing import Optional, Dict
4+
5+
6+
class ConfigHandler:
7+
camera_settings = {}
8+
9+
@staticmethod
10+
def load() -> Optional[Dict]:
11+
try:
12+
stream = io.open("config.yml", 'r', encoding='utf8')
13+
data = yaml.safe_load(stream)
14+
return data
15+
except Exception as e:
16+
print("Config Property Error\n", e)
17+
return None

reolink_api/display.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from typing import Dict
2+
3+
14
class DisplayAPIMixin:
25
"""API calls related to the current image (osd, on screen display)."""
36

4-
def get_osd(self) -> object:
7+
def get_osd(self) -> Dict:
58
"""
69
Get OSD information.
710
See examples/response/GetOsd.json for example response data.
@@ -10,7 +13,7 @@ def get_osd(self) -> object:
1013
body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}]
1114
return self._execute_command('GetOsd', body)
1215

13-
def get_mask(self) -> object:
16+
def get_mask(self) -> Dict:
1417
"""
1518
Get the camera mask information.
1619
See examples/response/GetMask.json for example response data.
@@ -19,27 +22,33 @@ def get_mask(self) -> object:
1922
body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}]
2023
return self._execute_command('GetMask', body)
2124

22-
def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_enabled: bool = 0, osd_channel_name: str = "",
23-
osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0,
25+
def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_enabled: bool = 0,
26+
osd_channel_name: str = "", osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0,
2427
osd_time_pos: str = "Lower Right") -> bool:
2528
"""
2629
Set OSD
2730
:param bg_color: bool
2831
:param channel: int channel id
2932
:param osd_channel_enabled: bool
3033
:param osd_channel_name: string channel name
31-
:param osd_channel_pos: string channel position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
34+
:param osd_channel_pos: string channel position
35+
["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
3236
:param osd_time_enabled: bool
33-
:param osd_time_pos: string time position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
37+
:param osd_time_pos: string time position
38+
["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
3439
:return: whether the action was successful
3540
"""
36-
body = [{"cmd": "SetOsd", "action": 1, "param": {
37-
"Osd": {"bgcolor": bg_color, "channel": channel,
38-
"osdChannel": {"enable": osd_channel_enabled, "name": osd_channel_name,
39-
"pos": osd_channel_pos},
40-
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
41-
}
42-
}}]
41+
body = [{"cmd": "SetOsd", "action": 1,
42+
"param": {
43+
"Osd": {
44+
"bgcolor": bg_color,
45+
"channel": channel,
46+
"osdChannel": {
47+
"enable": osd_channel_enabled, "name": osd_channel_name,
48+
"pos": osd_channel_pos
49+
},
50+
"osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
51+
}}}]
4352
r_data = self._execute_command('SetOsd', body)[0]
4453
if r_data["value"]["rspCode"] == 200:
4554
return True

reolink_api/image.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1+
from typing import Dict
2+
13

24
class ImageAPIMixin:
35
"""API calls for image settings."""
46

57
def set_adv_image_settings(self,
68
anti_flicker: str = 'Outdoor',
79
exposure: str = 'Auto',
8-
gain_min: int = 1,
9-
gain_max: int = 62,
10-
shutter_min: int = 1,
11-
shutter_max: int = 125,
12-
blue_gain: int = 128,
13-
red_gain: int = 128,
10+
gain_min: float = 1,
11+
gain_max: float = 62,
12+
shutter_min: float = 1,
13+
shutter_max: float = 125,
14+
blue_gain: float = 128,
15+
red_gain: float = 128,
1416
white_balance: str = 'Auto',
1517
day_night: str = 'Auto',
1618
back_light: str = 'DynamicRangeControl',
17-
blc: int = 128,
18-
drc: int = 128,
19-
rotation: int = 0,
20-
mirroring: int = 0,
21-
nr3d: int = 1) -> object:
19+
blc: float = 128,
20+
drc: float = 128,
21+
rotation: float = 0,
22+
mirroring: float = 0,
23+
nr3d: float = 1) -> Dict:
2224
"""
2325
Sets the advanced camera settings.
2426
@@ -66,11 +68,11 @@ def set_adv_image_settings(self,
6668
return self._execute_command('SetIsp', body)
6769

6870
def set_image_settings(self,
69-
brightness: int = 128,
70-
contrast: int = 62,
71-
hue: int = 1,
72-
saturation: int = 125,
73-
sharpness: int = 128) -> object:
71+
brightness: float = 128,
72+
contrast: float = 62,
73+
hue: float = 1,
74+
saturation: float = 125,
75+
sharpness: float = 128) -> Dict:
7476
"""
7577
Sets the camera image settings.
7678

0 commit comments

Comments
 (0)