From e7cdd19dfef1934899b0312d602737af0124cbea Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sat, 10 Aug 2019 21:47:57 +0200 Subject: [PATCH 1/7] Small changes and Snap Frame addition. Snap allows for the retrieval of a single frame from the current Camera video stream. PEP8 standard implemented. --- .idea/.gitignore | 2 + .idea/ReolinkCameraAPI.iml | 11 ++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 7 +++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ APIHandler.py | 57 +++++++++++++++++-- 8 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/ReolinkCameraAPI.iml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/ReolinkCameraAPI.iml b/.idea/ReolinkCameraAPI.iml new file mode 100644 index 0000000..6711606 --- /dev/null +++ b/.idea/ReolinkCameraAPI.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3999087 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8125795 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/APIHandler.py b/APIHandler.py index 2653081..33f5472 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -1,26 +1,46 @@ +import io import json +import random +import string +from urllib.request import urlopen + +from PIL import Image from resthandle import Request class APIHandler: + """ + The APIHandler class is the backend part of the API. + This handles communication directly with the camera. - def __init__(self, ip): + 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): + """ + Initialise the Camera API Handler (maps api calls into python) + :param ip: + :param username: + :param password: + """ self.url = "http://" + ip + "/cgi-bin/api.cgi" self.token = None + self.username = username + self.password = password # Token - def login(self, username: str, password: str): + def login(self): """ Get login token Must be called first, before any other operation can be performed - :param username: - :param password: :return: """ try: - body = [{"cmd": "Login", "action": 0, "param": {"User": {"userName": username, "password": password}}}] + 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: @@ -176,3 +196,30 @@ class APIHandler: except Exception as e: print("Could not get General System settings\n", e) raise + + ########## + # Image Data + ########## + def get_snap(self, timeout=3) -> Image or None: + """ + Gets a "snap" of the current camera video data and returns a Pillow Image or None + :param timeout: + :return: + """ + try: + randomstr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + snap = "?cmd=Snap&channel=0&rs=" \ + + randomstr \ + + "&user=" + self.username \ + + "&password=" + self.password + reader = urlopen(self.url + snap, timeout) + if reader.status == 200: + b = bytearray(reader.read()) + return Image.open(io.BytesIO(b)) + print("Could not retrieve data from camera successfully. Status:", reader.status) + return None + + except Exception as e: + print("Could not get Image data\n", e) + raise + From 696b3ac58f652394b3f36a92432a1b4847522e85 Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sat, 10 Aug 2019 21:48:47 +0200 Subject: [PATCH 2/7] Camera class updated --- Camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Camera.py b/Camera.py index 1e82361..317840d 100644 --- a/Camera.py +++ b/Camera.py @@ -4,8 +4,8 @@ from APIHandler import APIHandler class Camera(APIHandler): def __init__(self, ip, username="admin", password=""): - APIHandler.__init__(self, ip) + APIHandler.__init__(self, ip, username, password) self.ip = ip self.username = username self.password = password - super().login(self.username, self.password) + super().login() From f23c5e8c29c8f485ef78513d50ac76271b8e1508 Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 11 Aug 2019 13:24:35 +0200 Subject: [PATCH 3/7] Added User management and osd --- APIHandler.py | 148 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 20 deletions(-) diff --git a/APIHandler.py b/APIHandler.py index 33f5472..15551be 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -32,11 +32,11 @@ class APIHandler: # Token - def login(self): + def login(self) -> bool: """ Get login token Must be called first, before any other operation can be performed - :return: + :return: bool """ try: body = [{"cmd": "Login", "action": 0, @@ -49,9 +49,12 @@ class APIHandler: if int(code) == 0: self.token = data["value"]["Token"]["name"] print("Login success") + return True print(self.token) + return False else: print("Failed to login\nStatus Code:", response.status_code) + return False except Exception as e: print("Error Login\n", e) raise @@ -63,43 +66,48 @@ class APIHandler: ########### # SET Network ########### - def set_net_port(self, httpPort=80, httpsPort=443, mediaPort=9000, onvifPort=8000, rtmpPort=1935, rtspPort=554): + def set_net_port(self, http_port=80, https_port=443, media_port=9000, onvif_port=8000, rtmp_port=1935, + rtsp_port=554) -> bool: """ Set network ports If nothing is specified, the default values will be used - :param httpPort: - :param httpsPort: - :param mediaPort: - :param onvifPort: - :param rtmpPort: - :param rtspPort: - :return: + :param rtsp_port: int + :param rtmp_port: int + :param onvif_port: int + :param media_port: int + :param https_port: int + :type http_port: int + :return: bool """ try: if self.token is None: raise ValueError("Login first") body = [{"cmd": "SetNetPort", "action": 0, "param": {"NetPort": { - "httpPort": httpPort, - "httpsPort": httpsPort, - "mediaPort": mediaPort, - "onvifPort": onvifPort, - "rtmpPort": rtmpPort, - "rtspPort": rtspPort + "httpPort": http_port, + "httpsPort": https_port, + "mediaPort": media_port, + "onvifPort": onvif_port, + "rtmpPort": rtmp_port, + "rtspPort": rtsp_port }}}] param = {"token": self.token} response = Request.post(self.url, data=body, params=param) if response is not None: if response.status_code == 200: print("Successfully Set Network Ports") + return True else: print("Something went wront\nStatus Code:", response.status_code) + return False + + return False except Exception as e: print("Setting Network Port Error\n", e) raise - def set_wifi(self, ssid, password): + def set_wifi(self, ssid, password) -> json or None: try: if self.token is None: raise ValueError("Login first") @@ -185,18 +193,119 @@ class APIHandler: ########### # GET ########### - def get_general_system(self): + def get_general_system(self) -> json or None: try: if self.token is None: raise ValueError("Login first") body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}] param = {"token": self.token} response = Request.post(self.url, data=body, params=param) - return json.loads(response.text) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve general information from camera successfully. Status:", response.status_code) + return None except Exception as e: print("Could not get General System settings\n", e) raise + def get_osd(self) -> json or None: + try: + param = {"cmd": "GetOsd", "token": self.token} + body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve OSD from camera successfully. Status:", response.status_code) + return None + except Exception as e: + print("Could not get OSD", e) + raise + + ########## + # User + ########## + + ########## + # GET + ########## + def get_online_user(self) -> json or None: + try: + param = {"cmd": "GetOnline", "token": self.token} + body = [{"cmd": "GetOnline", "action": 1, "param": {}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve online user from camera. Status:", response.status_code) + return None + except Exception as e: + print("Could not get online user", e) + raise + + def get_users(self) -> json or None: + try: + param = {"cmd": "GetUser", "token": self.token} + body = [{"cmd": "GetUser", "action": 1, "param": {}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve users from camera. Status:", response.status_code) + return None + except Exception as e: + print("Could not get users", e) + raise + + ########## + # SET + ########## + def add_user(self, username: str, password: str, level: str = "guest") -> bool: + try: + param = {"cmd": "AddUser", "token": self.token} + body = [{"cmd": "AddUser", "action": 0, + "param": {"User": {"userName": username, "password": password, "level": level}}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + r_data = json.loads(response.text) + if r_data["value"]["rspCode"] == "200": + return True + print("Could not add user. Camera responded with:", r_data["value"]) + return False + print("Something went wrong. Could not add user. Status:", response.status_code) + return False + except Exception as e: + print("Could not add user", e) + raise + + def modify_user(self, username: str, password: str) -> bool: + try: + param = {"cmd": "ModifyUser", "token": self.token} + body = [{"cmd": "ModifyUser", "action": 0, "param": {"User": {"userName": username, "password": password}}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + r_data = json.loads(response.text) + if r_data["value"]["rspCode"] == "200": + return True + print("Could not modify user:", username, "\nCamera responded with:", r_data["value"]) + print("Something went wrong. Could not modify user. Status:", response.status_code) + return False + except Exception as e: + print("Could not modify user", e) + raise + + def delete_user(self, username: str) -> bool: + try: + param = {"cmd": "DelUser", "token": self.token} + body = [{"cmd": "DelUser", "action": 0, "param": {"User": {"userName": username}}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + r_data = json.loads(response.text) + if r_data["value"]["rspCode"] == "200": + return True + print("Could not delete user:", username, "\nCamera responded with:", r_data["value"]) + return False + except Exception as e: + print("Could not delete user", e) + raise + ########## # Image Data ########## @@ -222,4 +331,3 @@ class APIHandler: except Exception as e: print("Could not get Image data\n", e) raise - From 2722a80922bad912f645cfbb0ab9a7cac1c91353 Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 11 Aug 2019 13:45:51 +0200 Subject: [PATCH 4/7] Added some doc strings to User management and HDD api --- APIHandler.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/APIHandler.py b/APIHandler.py index 15551be..75c51e5 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -229,6 +229,10 @@ class APIHandler: # GET ########## def get_online_user(self) -> json or None: + """ + Return a list of current logged-in users in json format + :return: json or None + """ try: param = {"cmd": "GetOnline", "token": self.token} body = [{"cmd": "GetOnline", "action": 1, "param": {}}] @@ -242,6 +246,10 @@ class APIHandler: raise def get_users(self) -> json or None: + """ + Return a list of user accounts from the camera in json format + :return: json or None + """ try: param = {"cmd": "GetUser", "token": self.token} body = [{"cmd": "GetUser", "action": 1, "param": {}}] @@ -258,6 +266,13 @@ class APIHandler: # SET ########## def add_user(self, username: str, password: str, level: str = "guest") -> bool: + """ + Add a new user account to the camera + :param username: The user's username + :param password: The user's password + :param level: The privilege level 'guest' or 'admin'. Default is 'guest' + :return: bool + """ try: param = {"cmd": "AddUser", "token": self.token} body = [{"cmd": "AddUser", "action": 0, @@ -276,6 +291,12 @@ class APIHandler: raise def modify_user(self, username: str, password: str) -> bool: + """ + Modify the user's password by specifying their username + :param username: The user which would want to be modified + :param password: The new password + :return: bool + """ try: param = {"cmd": "ModifyUser", "token": self.token} body = [{"cmd": "ModifyUser", "action": 0, "param": {"User": {"userName": username, "password": password}}}] @@ -292,6 +313,11 @@ class APIHandler: raise def delete_user(self, username: str) -> bool: + """ + Delete a user by specifying their username + :param username: The user which would want to be deleted + :return: bool + """ try: param = {"cmd": "DelUser", "token": self.token} body = [{"cmd": "DelUser", "action": 0, "param": {"User": {"userName": username}}}] @@ -309,11 +335,11 @@ class APIHandler: ########## # Image Data ########## - def get_snap(self, timeout=3) -> Image or None: + def get_snap(self, timeout: int = 3) -> Image or None: """ Gets a "snap" of the current camera video data and returns a Pillow Image or None - :param timeout: - :return: + :param timeout: Request timeout to camera in seconds + :return: Image or None """ try: randomstr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) @@ -331,3 +357,59 @@ class APIHandler: except Exception as e: print("Could not get Image data\n", e) raise + + ######### + # Device + ######### + def get_hdd_info(self) -> json or None: + """ + Gets all HDD and SD card information from Camera + Format is as follows: + [{"cmd" : "GetHddInfo", + "code" : 0, + "value" : { + "HddInfo" : [{ + "capacity" : 15181, + "format" : 1, + "id" : 0, + "mount" : 1, + "size" : 15181 + }] + } + }] + + :return: json or None + """ + try: + param = {"cmd": "GetHddInfo", "token": self.token} + body = [{"cmd": "GetHddInfo", "action": 0, "param": {}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve HDD/SD info from camera successfully. Status:", response.status_code) + return None + except Exception as e: + print("Could not get HDD/SD card information", e) + raise + + def format_hdd(self, hdd_id: [int] = [0]) -> bool: + """ + Format specified HDD/SD cards with their id's + :param hdd_id: List of id's specified by the camera with get_hdd_info api. Default is 0 (SD card) + :return: bool + """ + try: + param = {"cmd": "Format", "token": self.token} + body = [{"cmd": "Format", "action": 0, "param": {"HddInfo": {"id": hdd_id}}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + r_data = json.loads(response.text) + if r_data["value"]["rspCode"] == "200": + return True + print("Could not format HDD/SD. Camera responded with:", r_data["value"]) + return False + print("Could not format HDD/SD. Status:", response.status_code) + return False + except Exception as e: + print("Could not format HDD/SD", e) + raise From 5bc90f7c607ca24a2840ce6abfd84c8d573d2ddf Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 11 Aug 2019 15:35:52 +0200 Subject: [PATCH 5/7] Added some API's and Doc strings OSD -> set and get System -> get_performance, get_information Recording -> get --- APIHandler.py | 469 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 458 insertions(+), 11 deletions(-) diff --git a/APIHandler.py b/APIHandler.py index 75c51e5..bd35b67 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -13,6 +13,7 @@ class APIHandler: """ The APIHandler class is the backend part of the API. 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/ @@ -30,7 +31,9 @@ class APIHandler: self.username = username self.password = password + ########### # Token + ########### def login(self) -> bool: """ @@ -100,9 +103,7 @@ class APIHandler: else: print("Something went wront\nStatus Code:", response.status_code) return False - return False - except Exception as e: print("Setting Network Port Error\n", e) raise @@ -126,7 +127,7 @@ class APIHandler: ########### # GET ########### - def get_net_ports(self): + def get_net_ports(self) -> json or None: """ Get network ports :return: @@ -140,7 +141,10 @@ class APIHandler: {"cmd": "GetP2p", "action": 0, "param": {}}] param = {"token": self.token} response = Request.post(self.url, data=body, params=param) - return json.loads(response.text) + if response.status_code == 200: + return json.loads(response.text) + print("Could not get network ports data. Status:", response.status_code) + return None except Exception as e: print("Get Network Ports", e) @@ -157,7 +161,9 @@ class APIHandler: body = [{"cmd": "GetLocalLink", "action": 1, "param": {}}] param = {"cmd": "GetLocalLink", "token": self.token} request = Request.post(self.url, data=body, params=param) - return json.loads(request.text) + if request.status_code == 200: + return json.loads(request.text) + print("Could not get ") except Exception as e: print("Could not get Link Local", e) raise @@ -186,6 +192,113 @@ class APIHandler: print("Could not Scan wifi\n", e) raise + ########### + # Display + ########### + + ########### + # GET + ########### + def get_osd(self) -> json or None: + """ + Get OSD information. + Response data format is as follows: + [{"cmd" : "GetOsd","code" : 0, "initial" : { + "Osd" : {"bgcolor" : 0,"channel" : 0,"osdChannel" : {"enable" : 1,"name" : "Camera1","pos" : "Lower Right"}, + "osdTime" : {"enable" : 1,"pos" : "Top Center"} + }},"range" : {"Osd" : {"bgcolor" : "boolean","channel" : 0,"osdChannel" : {"enable" : "boolean","name" : {"maxLen" : 31}, + "pos" : ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"] + }, + "osdTime" : {"enable" : "boolean","pos" : ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"] + } + }},"value" : {"Osd" : {"bgcolor" : 0,"channel" : 0,"osdChannel" : {"enable" : 0,"name" : "FarRight","pos" : "Lower Right"}, + "osdTime" : {"enable" : 0,"pos" : "Top Center"} + }}}] + :return: json or None + """ + try: + param = {"cmd": "GetOsd", "token": self.token} + body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve OSD from camera successfully. Status:", response.status_code) + return None + except Exception as e: + print("Could not get OSD", e) + raise + + def get_mask(self) -> json or None: + """ + Get the camera mask information + Response data format is as follows: + [{"cmd" : "GetMask","code" : 0,"initial" : { + "Mask" : { + "area" : [{"block" : {"height" : 0,"width" : 0,"x" : 0,"y" : 0},"screen" : {"height" : 0,"width" : 0}}], + "channel" : 0, + "enable" : 0 + } + },"range" : {"Mask" : {"channel" : 0,"enable" : "boolean","maxAreas" : 4}},"value" : { + "Mask" : { + "area" : null, + "channel" : 0, + "enable" : 0} + } + }] + :return: json or None + """ + try: + param = {"cmd": "GetMask", "token": self.token} + body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not get Mask from camera successfully. Status:", response.status_code) + return None + except Exception as e: + print("Could not get mask", e) + raise + + ########### + # SET + ########### + def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_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: + """ + Set OSD + :param bg_color: bool + :param channel: int channel id + :param osd_channel_enabled: bool + :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_time_enabled: bool + :param osd_time_pos: string time position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"] + :return: + """ + try: + param = {"cmd": "setOsd", "token": self.token} + body = [{"cmd": "SetOsd", "action": 1, "param": + {"Osd": {"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} + } + } + }] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + r_data = json.loads(response.text) + if r_data["value"]["rspCode"] == "200": + return True + print("Could not set OSD. Camera responded with status:", r_data["value"]) + return False + print("Could not set OSD. Status:", response.status_code) + return False + except Exception as e: + print("Could not set OSD", e) + raise + ########### # SYSTEM ########### @@ -208,17 +321,71 @@ class APIHandler: print("Could not get General System settings\n", e) raise - def get_osd(self) -> json or None: + def get_performance(self) -> json or None: + """ + Get a snapshot of the current performance of the camera. + Response data format is as follows: + [{"cmd" : "GetPerformance", + "code" : 0, + "value" : { + "Performance" : { + "codecRate" : 2154, + "cpuUsed" : 14, + "netThroughput" : 0 + } + } + }] + :return: json or None + """ try: - param = {"cmd": "GetOsd", "token": self.token} - body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}] + param = {"cmd": "GetPerformance", "token": self.token} + body = [{"cmd": "GetPerformance", "action": 0, "param": {}}] response = Request.post(self.url, data=body, params=param) if response.status_code == 200: return json.loads(response.text) - print("Could not retrieve OSD from camera successfully. Status:", response.status_code) + print("Cound not retrieve performance information from camera successfully. Status:", response.status_code) return None except Exception as e: - print("Could not get OSD", e) + print("Could not get performance", e) + raise + + def get_information(self) -> json or None: + """ + Get the camera information + Response data format is as follows: + [{"cmd" : "GetDevInfo","code" : 0,"value" : { + "DevInfo" : { + "B485" : 0, + "IOInputNum" : 0, + "IOOutputNum" : 0, + "audioNum" : 0, + "buildDay" : "build 18081408", + "cfgVer" : "v2.0.0.0", + "channelNum" : 1, + "detail" : "IPC_3816M100000000100000", + "diskNum" : 1, + "firmVer" : "v2.0.0.1389_18081408", + "hardVer" : "IPC_3816M", + "model" : "RLC-411WS", + "name" : "Camera1_withpersonality", + "serial" : "00000000000000", + "type" : "IPC", + "wifi" : 1 + } + } + }] + :return: json or None + """ + try: + param = {"cmd": "GetDevInfo", "token": self.token} + body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}] + response = Request.post(self.url, data=body, params=param) + if response == 200: + return json.loads(response.text) + print("Could not retrieve camera information. Status:", response.status_code) + return None + except Exception as e: + print("Could not get device information", e) raise ########## @@ -231,6 +398,17 @@ class APIHandler: def get_online_user(self) -> json or None: """ Return a list of current logged-in users in json format + Response data format is as follows: + [{"cmd" : "GetOnline","code" : 0,"value" : { + "User" : [{ + "canbeDisconn" : 0, + "ip" : "192.168.1.100", + "level" : "admin", + "sessionId" : 1000, + "userName" : "admin" + }] + } + }] :return: json or None """ try: @@ -248,6 +426,29 @@ class APIHandler: def get_users(self) -> json or None: """ Return a list of user accounts from the camera in json format + Response data format is as follows: + [{"cmd" : "GetUser","code" : 0,"initial" : { + "User" : { + "level" : "guest" + }}, + "range" : {"User" : { + "level" : [ "guest", "admin" ], + "password" : { + "maxLen" : 31, + "minLen" : 6 + }, + "userName" : { + "maxLen" : 31, + "minLen" : 1 + }} + },"value" : { + "User" : [ + { + "level" : "admin", + "userName" : "admin" + }] + } + }] :return: json or None """ try: @@ -364,7 +565,7 @@ class APIHandler: def get_hdd_info(self) -> json or None: """ Gets all HDD and SD card information from Camera - Format is as follows: + Response data format is as follows: [{"cmd" : "GetHddInfo", "code" : 0, "value" : { @@ -413,3 +614,249 @@ class APIHandler: except Exception as e: print("Could not format HDD/SD", e) raise + + ########### + # Recording + ########### + + ########### + # SET + ########### + + ########### + # GET + ########### + def get_recording_encoding(self) -> json or None: + """ + Get the current camera encoding settings for "Clear" and "Fluent" profiles. + Response data format is as follows: + [{ + "cmd" : "GetEnc", + "code" : 0, + "initial" : { + "Enc" : { + "audio" : 0, + "channel" : 0, + "mainStream" : { + "bitRate" : 4096, + "frameRate" : 15, + "profile" : "High", + "size" : "3072*1728" + }, + "subStream" : { + "bitRate" : 160, + "frameRate" : 7, + "profile" : "High", + "size" : "640*360" + } + } + }, + "range" : { + "Enc" : [ + { + "audio" : "boolean", + "mainStream" : { + "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ], + "default" : { + "bitRate" : 4096, + "frameRate" : 15 + }, + "frameRate" : [ 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "3072*1728" + }, + "subStream" : { + "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ], + "default" : { + "bitRate" : 160, + "frameRate" : 7 + }, + "frameRate" : [ 15, 10, 7, 4 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "640*360" + } + }, + { + "audio" : "boolean", + "mainStream" : { + "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ], + "default" : { + "bitRate" : 4096, + "frameRate" : 15 + }, + "frameRate" : [ 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "2592*1944" + }, + "subStream" : { + "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ], + "default" : { + "bitRate" : 160, + "frameRate" : 7 + }, + "frameRate" : [ 15, 10, 7, 4 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "640*360" + } + }, + { + "audio" : "boolean", + "mainStream" : { + "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ], + "default" : { + "bitRate" : 3072, + "frameRate" : 15 + }, + "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "2560*1440" + }, + "subStream" : { + "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ], + "default" : { + "bitRate" : 160, + "frameRate" : 7 + }, + "frameRate" : [ 15, 10, 7, 4 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "640*360" + } + }, + { + "audio" : "boolean", + "mainStream" : { + "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ], + "default" : { + "bitRate" : 3072, + "frameRate" : 15 + }, + "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "2048*1536" + }, + "subStream" : { + "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ], + "default" : { + "bitRate" : 160, + "frameRate" : 7 + }, + "frameRate" : [ 15, 10, 7, 4 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "640*360" + } + }, + { + "audio" : "boolean", + "mainStream" : { + "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ], + "default" : { + "bitRate" : 3072, + "frameRate" : 15 + }, + "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "2304*1296" + }, + "subStream" : { + "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ], + "default" : { + "bitRate" : 160, + "frameRate" : 7 + }, + "frameRate" : [ 15, 10, 7, 4 ], + "profile" : [ "Base", "Main", "High" ], + "size" : "640*360" + } + } + ] + }, + "value" : { + "Enc" : { + "audio" : 0, + "channel" : 0, + "mainStream" : { + "bitRate" : 2048, + "frameRate" : 20, + "profile" : "Main", + "size" : "3072*1728" + }, + "subStream" : { + "bitRate" : 64, + "frameRate" : 4, + "profile" : "High", + "size" : "640*360" + } + } + } + }] + + :return: json or None + """ + try: + param = {"cmd": "GetEnc", "token": self.token} + body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve recording encoding data. Status:", response.status_code) + return None + except Exception as e: + print("Could not get recording encoding", e) + raise + + def get_recording_advanced(self) -> json or None: + """ + Get recording advanced setup data + Response data format is as follows: + [{ + "cmd" : "GetRec", + "code" : 0, + "initial" : { + "Rec" : { + "channel" : 0, + "overwrite" : 1, + "postRec" : "15 Seconds", + "preRec" : 1, + "schedule" : { + "enable" : 1, + "table" : "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + } + } + }, + "range" : { + "Rec" : { + "channel" : 0, + "overwrite" : "boolean", + "postRec" : [ "15 Seconds", "30 Seconds", "1 Minute" ], + "preRec" : "boolean", + "schedule" : { + "enable" : "boolean" + } + } + }, + "value" : { + "Rec" : { + "channel" : 0, + "overwrite" : 1, + "postRec" : "15 Seconds", + "preRec" : 1, + "schedule" : { + "enable" : 1, + "table" : "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + } + } + }] + + :return: json or None + """ + try: + param = {"cmd": "GetRec", "token": self.token} + body = [{"cmd": "GetRec", "action": 1, "param": {"channel": 0}}] + response = Request.post(self.url, data=body, params=param) + if response.status_code == 200: + return json.loads(response.text) + print("Could not retrieve advanced recording. Status:", response.status_code) + return None + except Exception as e: + print("Could not get advanced recoding", e) From 475c72b241ea5160879b5b468b1538010988e746 Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 11 Aug 2019 21:06:21 +0200 Subject: [PATCH 6/7] Added proxy support. Bug fixes in APIHandler. RTSP support (adding) Proxy support allows contacting the camera behind a proxy (GET and POST requests). Adding RTSP support - still in progress. --- APIHandler.py | 45 ++++++++++++++++++++++++--- Camera.py | 14 ++++++++- RtspClient.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ resthandle.py | 16 +++++++--- test.py | 6 ++-- 5 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 RtspClient.py diff --git a/APIHandler.py b/APIHandler.py index bd35b67..c52b624 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -2,10 +2,14 @@ import io import json import random import string -from urllib.request import urlopen +import sys +from urllib import request +import numpy +import rtsp from PIL import Image +from RtspClient import RtspClient from resthandle import Request @@ -19,17 +23,23 @@ class APIHandler: """ - def __init__(self, ip: str, username: str, password: str): + def __init__(self, ip: str, username: str, password: str, **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 + """ + self.ip = ip self.url = "http://" + ip + "/cgi-bin/api.cgi" self.token = None self.username = username self.password = password + Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found ########### # Token @@ -380,7 +390,7 @@ class APIHandler: param = {"cmd": "GetDevInfo", "token": self.token} body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}] response = Request.post(self.url, data=body, params=param) - if response == 200: + if response.status_code == 200: return json.loads(response.text) print("Could not retrieve camera information. Status:", response.status_code) return None @@ -543,12 +553,15 @@ class APIHandler: :return: Image or None """ try: + randomstr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) - snap = "?cmd=Snap&channel=0&rs=" \ + snap = self.url + "?cmd=Snap&channel=0&rs=" \ + randomstr \ + "&user=" + self.username \ + "&password=" + self.password - reader = urlopen(self.url + snap, timeout) + req = request.Request(snap) + req.set_proxy(Request.proxies, 'http') + reader = request.urlopen(req, timeout) if reader.status == 200: b = bytearray(reader.read()) return Image.open(io.BytesIO(b)) @@ -860,3 +873,25 @@ class APIHandler: return None except Exception as e: print("Could not get advanced recoding", e) + + ########### + # RTSP Stream + ########### + def open_video_stream(self, profile: str = "main") -> Image: + """ + profile is "main" or "sub" + https://support.reolink.com/hc/en-us/articles/360007010473-How-to-Live-View-Reolink-Cameras-via-VLC-Media-Player + :param profile: + :return: + """ + with RtspClient(ip=self.ip, username=self.username, password=self.password, + proxies={"host": "127.0.0.1", "port": 8000}) as rtsp_client: + rtsp_client.preview() + # with rtsp.Client( + # rtsp_server_uri="rtsp://" + # + self.username + ":" + # + self.password + "@" + # + self.ip + # + ":554//h264Preview_01_" + # + profile) as client: + # return client diff --git a/Camera.py b/Camera.py index 317840d..4a4ee79 100644 --- a/Camera.py +++ b/Camera.py @@ -4,7 +4,19 @@ from APIHandler import APIHandler class Camera(APIHandler): def __init__(self, ip, username="admin", password=""): - APIHandler.__init__(self, ip, username, password) + """ + 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 + APIHandler.__init__(self, ip, username, password, proxy={"http": "socks5://127.0.0.1:8000"}) + + # Normal call without proxy: + # APIHandler.__init__(self, ip, username, password) + self.ip = ip self.username = username self.password = password diff --git a/RtspClient.py b/RtspClient.py new file mode 100644 index 0000000..b3cb3fb --- /dev/null +++ b/RtspClient.py @@ -0,0 +1,85 @@ +import socket + +import cv2 +import numpy +import socks + + +class RtspClient: + + def __init__(self, ip, username, password, port=554, profile="main", **kwargs): + """ + + :param ip: + :param username: + :param password: + :param port: rtsp port + :param profile: "main" or "sub" + :param proxies: {"host": "localhost", "port": 8000} + """ + self.ip = ip + self.username = username + self.password = password + self.port = port + self.sockt = None + self.url = "rtsp://" + self.username + ":" + self.password + "@" + self.ip + ":" + str( + self.port) + "//h264Preview_01_" + profile + self.proxy = kwargs.get("proxies") + + def __enter__(self): + self.sockt = self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.sockt.close() + + def connect(self) -> socket: + try: + sockt = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) + sockt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.proxy is not None: + sockt.set_proxy(socks.SOCKS5, self.proxy["host"], self.proxy["port"]) + sockt.connect((self.ip, self.port)) + return sockt + except Exception as e: + print(e) + + def get_frame(self) -> bytearray: + try: + self.sockt.send(str.encode(self.url)) + data = b'' + while True: + try: + r = self.sockt.recv(90456) + if len(r) == 0: + break + a = r.find(b'END!') + if a != -1: + data += r[:a] + break + data += r + except Exception as e: + print(e) + continue + nparr = numpy.fromstring(data, numpy.uint8) + frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + return frame + except Exception as e: + print(e) + + def preview(self): + """ Blocking function. Opens OpenCV window to display stream. """ + self.connect() + win_name = 'RTSP' + cv2.namedWindow(win_name, cv2.WINDOW_AUTOSIZE) + cv2.moveWindow(win_name, 20, 20) + + while True: + cv2.imshow(win_name, self.get_frame()) + # if self._latest is not None: + # cv2.imshow(win_name,self._latest) + if cv2.waitKey(25) & 0xFF == ord('q'): + break + cv2.waitKey() + cv2.destroyAllWindows() + cv2.waitKey() diff --git a/resthandle.py b/resthandle.py index e632686..f68f406 100644 --- a/resthandle.py +++ b/resthandle.py @@ -1,9 +1,13 @@ import json import requests +import socket + +import socks class Request: + proxies = None @staticmethod def post(url: str, data, params=None) -> requests.Response or None: @@ -16,10 +20,11 @@ class Request: """ try: headers = {'content-type': 'application/json'} - if params is not None: - r = requests.post(url, params=params, json=data, headers=headers) - else: - r = requests.post(url, json=data) + r = requests.post(url, params=params, json=data, headers=headers, 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: return r else: @@ -38,7 +43,8 @@ class Request: :return: """ try: - data = requests.get(url=url, params=params, timeout=timeout) + data = requests.get(url=url, params=params, timeout=timeout, proxies=Request.proxies) + return data except Exception as e: print("Get Error\n", e) diff --git a/test.py b/test.py index 158ad3f..796f5a2 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,5 @@ from Camera import Camera -c = Camera("192.168.1.100", "admin", "jUa2kUzi") -c.get_wifi() -c.scan_wifi() +c = Camera("192.168.1.112", "admin", "jUa2kUzi") +# print("Getting information", c.get_information()) +c.open_video_stream() From c4600134b5ff44624c63a7337b7ed3d24e3309eb Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 15 Sep 2019 22:29:42 +0200 Subject: [PATCH 7/7] Updating dev-1.0. --- APIHandler.py | 5 +++-- Camera.py | 6 +++--- resthandle.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/APIHandler.py b/APIHandler.py index c52b624..e03379a 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -23,7 +23,7 @@ class APIHandler: """ - def __init__(self, ip: str, username: str, password: str, **kwargs): + def __init__(self, ip: str, username: str, password: str, https = False, **kwargs): """ Initialise the Camera API Handler (maps api calls into python) :param ip: @@ -34,8 +34,9 @@ class APIHandler: 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.url = "http://" + ip + "/cgi-bin/api.cgi" self.token = None self.username = username self.password = password diff --git a/Camera.py b/Camera.py index 4a4ee79..1e8a52b 100644 --- a/Camera.py +++ b/Camera.py @@ -3,7 +3,7 @@ from APIHandler import APIHandler class Camera(APIHandler): - def __init__(self, ip, username="admin", password=""): + def __init__(self, ip, username="admin", password="", https=False): """ Initialise the Camera object by passing the ip address. The default details {"username":"admin", "password":""} will be used if nothing passed @@ -12,11 +12,11 @@ class Camera(APIHandler): :param password: """ # For when you need to connect to a camera behind a proxy - APIHandler.__init__(self, ip, username, password, proxy={"http": "socks5://127.0.0.1:8000"}) + APIHandler.__init__(self, ip, username, password, proxy={"http": "socks5://127.0.0.1:8000"}, https=https) # Normal call without proxy: # APIHandler.__init__(self, ip, username, password) - + self.ip = ip self.username = username self.password = password diff --git a/resthandle.py b/resthandle.py index f68f406..6921c20 100644 --- a/resthandle.py +++ b/resthandle.py @@ -20,7 +20,7 @@ class Request: """ try: headers = {'content-type': 'application/json'} - r = requests.post(url, params=params, json=data, headers=headers, proxies=Request.proxies) + r = requests.post(url, verify=False, params=params, json=data, headers=headers, proxies=Request.proxies) # if params is not None: # r = requests.post(url, params=params, json=data, headers=headers, proxies=proxies) # else: @@ -43,7 +43,7 @@ class Request: :return: """ try: - data = requests.get(url=url, params=params, timeout=timeout, proxies=Request.proxies) + data = requests.get(url=url, verify=False, params=params, timeout=timeout, proxies=Request.proxies) return data except Exception as e: