diff --git a/examples/get_info.py b/examples/get_info.py new file mode 100644 index 0000000..420f0f7 --- /dev/null +++ b/examples/get_info.py @@ -0,0 +1,34 @@ +import reolinkapi +import json + +if __name__ == "__main__": + hostname = "watchdog1" + + try: + cam = reolinkapi.Camera(hostname, username="userid", password="passwd") + except: + print(f"Failed to open camera {hostname}") + exit(1) + + if not cam.is_logged_in(): + print(f"Login failed for {hostname}") + exit(1) + + + print( json.dumps( cam.get_information(), indent=4 ) ) + print( json.dumps( cam.get_network_general(), indent=4 ) ) + print( json.dumps( cam.get_network_ddns(), indent=4 ) ) + print( json.dumps( cam.get_network_ntp(), indent=4 ) ) + print( json.dumps( cam.get_network_email(), indent=4 ) ) + print( json.dumps( cam.get_network_ftp(), indent=4 ) ) + print( json.dumps( cam.get_network_push(), indent=4 ) ) + print( json.dumps( cam.get_network_status(), indent=4 ) ) + print( json.dumps( cam.get_recording_encoding(), indent=4 ) ) + print( json.dumps( cam.get_recording_advanced(), indent=4 ) ) + print( json.dumps( cam.get_general_system(), indent=4 ) ) + print( json.dumps( cam.get_performance(), indent=4 ) ) + print( json.dumps( cam.get_dst(), indent=4 ) ) + print( json.dumps( cam.get_online_user(), indent=4 ) ) + print( json.dumps( cam.get_users(), indent=4 ) ) + + diff --git a/examples/set_ftp.py b/examples/set_ftp.py new file mode 100644 index 0000000..8c53dd3 --- /dev/null +++ b/examples/set_ftp.py @@ -0,0 +1,44 @@ +#!/opt/homebrew/bin/python3 + +import reolinkapi +import os +import json +from sys import argv + +def ftp_cam(which): + hostname = 'watchdog%d' % which + cam_name = 'Watchdog%d' % which + + try: + cam = reolinkapi.Camera(hostname, username="userid", password="passwd") + except: + print(f"Failed to open camera {cam_name}") + return 1 + + if not cam.is_logged_in(): + print(f"Login failed for {cam_name}") + return 2 + + try: + print(f"Setting FTP params for {cam_name}") + response = cam.set_network_ftp("hocus", "pocus", "directory", "192.168.1.2", 0) + print( json.dumps( response, indent=4 ) ) + print( json.dumps( cam.get_network_ftp(), indent=4 ) ) + if response[0]["value"]["rspCode"] == 200: + print("Success!") + except Exception as e: + print(f"{argv[0]} for {cam_name} failed.") + print(e) + return 3 + + return 0 + + +if __name__ == "__main__": + if len(argv) != 2: + print("Usage: %s ]" % argv[0]) + exit(1) + + exit( ftp_cam(int(argv[1])) ) + + diff --git a/examples/snap.py b/examples/snap.py new file mode 100755 index 0000000..c2c945b --- /dev/null +++ b/examples/snap.py @@ -0,0 +1,41 @@ +#!/opt/homebrew/bin/python3 + +import reolinkapi +import os +from sys import argv + +def snap_cam(which, dirname): + hostname = 'watchdog%d' % which + cam_name = 'Watchdog%d' % which + + try: + cam = reolinkapi.Camera(hostname, username="rtsp", password="darknet") + except: + print(f"Failed to open camera {cam_name}") + return 1 + + if not cam.is_logged_in(): + print(f"Login failed for {cam_name}") + return 2 + + image_file = '%s/%s.jpg' % (dirname, hostname) + if os.path.exists(image_file): + os.unlink(image_file) + + print("Snapping", cam_name) + image = cam.get_snap() + image.save(image_file) + return 0 + + +if __name__ == "__main__": + dirname = '.' + if len(argv) == 3: + dirname = argv[2] + elif len(argv) != 2: + print("Usage: %s [output-directory]" % argv[0]) + exit(1) + + exit( snap_cam(int(argv[1]), dirname) ) + + diff --git a/examples/streaming_video.py b/examples/streaming_video.py old mode 100644 new mode 100755 index 9049ed8..9a449e0 --- a/examples/streaming_video.py +++ b/examples/streaming_video.py @@ -1,54 +1,65 @@ +#!/opt/homebrew/bin/python3 + import cv2 +from queue import Queue +from sys import argv from reolinkapi import Camera +_resize = 1024 + +def display_frame(cam, frame, count): + if frame is not None: + print("Frame %5d" % count, end='\r') + cv2.imshow(cam.ip, frame) + + key = cv2.waitKey(1) + if key == ord('q') or key == ord('Q') or key == 27: + cam.stop_stream() + cv2.destroyAllWindows() + return False + + return True -def non_blocking(): - print("calling non-blocking") +def non_blocking(cam): + frames = Queue(maxsize=30) - def inner_callback(img): - cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600)) - print("got the image non-blocking") - key = cv2.waitKey(1) - if key == ord('q'): - cv2.destroyAllWindows() - exit(1) + def inner_callback(frame): + if _resize > 10: + frames.put(maintain_aspect_ratio_resize(frame, width=_resize)) + else: + frames.put(frame.copy()) - c = Camera("192.168.1.112", "admin", "jUa2kUzi") # t in this case is a thread - t = c.open_video_stream(callback=inner_callback) + t = cam.open_video_stream(callback=inner_callback) - print(t.is_alive()) - while True: - if not t.is_alive(): - print("continuing") + counter = 0 + + while t.is_alive(): + frame = frames.get() + counter = counter+1 + + if not display_frame(cam, frame, counter): break - # stop the stream - # client.stop_stream() + + if __debug__ and frames.qsize() > 1: + # Can't consume frames fast enough?? + print("\nQueue: %d" % frames.qsize()) + -def blocking(): - c = Camera("192.168.1.112", "admin", "jUa2kUzi") +def blocking(cam): # stream in this case is a generator returning an image (in mat format) - stream = c.open_video_stream() - - # using next() - # while True: - # img = next(stream) - # cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600)) - # print("got the image blocking") - # key = cv2.waitKey(1) - # if key == ord('q'): - # cv2.destroyAllWindows() - # exit(1) - - # or using a for loop - for img in stream: - cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600)) - print("got the image blocking") - key = cv2.waitKey(1) - if key == ord('q'): - cv2.destroyAllWindows() - exit(1) + stream = cam.open_video_stream() + counter = 0 + + for frame in stream: + if _resize > 10: + frame = maintain_aspect_ratio_resize(frame, width=_resize) + + counter = counter + 1 + + if not display_frame(cam, frame, counter): + break; # Resizes a image and maintains aspect ratio @@ -76,6 +87,25 @@ def maintain_aspect_ratio_resize(image, width=None, height=None, inter=cv2.INTER return cv2.resize(image, dim, interpolation=inter) -# Call the methods. Either Blocking (using generator) or Non-Blocking using threads -# non_blocking() -blocking() + +if __name__ == "__main__": + if len(argv) != 2: + print(f"Usage: {argv[0]} ") + exit(1) + + try: + host = f"watchdog{argv[1]}" + cam = Camera(host, username="rtsp", password="darknet") + except: + print(f"Failed to open camera: {host}") + exit(1) + + if not cam.is_logged_in(): + print(f"Login failed for {host}") + exit(1) + + # Call the methods. Either Blocking (using generator) or Non-Blocking using threads + non_blocking(cam) + # blocking(cam) + + print("\nDone.") diff --git a/reolinkapi/handlers/api_handler.py b/reolinkapi/handlers/api_handler.py index 501ceb7..bd75a0e 100644 --- a/reolinkapi/handlers/api_handler.py +++ b/reolinkapi/handlers/api_handler.py @@ -1,4 +1,6 @@ import requests +from urllib3.exceptions import InsecureRequestWarning + from typing import Dict, List, Optional, Union from reolinkapi.mixins.alarm import AlarmAPIMixin from reolinkapi.mixins.device import DeviceAPIMixin @@ -53,6 +55,8 @@ def __init__(self, ip: str, username: str, password: str, https: bool = False, * self.url = f"{scheme}://{ip}/cgi-bin/api.cgi" self.ip = ip self.token = None + self.ability = None + self.scheduleVersion = 0 self.username = username self.password = password Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found @@ -64,19 +68,24 @@ def login(self) -> bool: :return: bool """ try: + # Suppress only the single warning from urllib3 needed. + requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) + 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: + # print("LOGIN GOT: ", response.text) 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 + # print("Login success") + else: + # print(self.token) + print("ERROR: LOGIN RESPONSE: ", response.text) + 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. @@ -84,8 +93,22 @@ def login(self) -> bool: print("Failed to login\nResponse was null.") return False except Exception as e: - print("Error Login\n", e) - raise + print(f"ERROR Login Failed, exception: {e}") + return False + + try: + ability = self.get_ability() + self.ability = ability[0]["value"]["Ability"] + self.scheduleVersion = self.ability["scheduleVersion"]["ver"] + print("API VERSION: ", self.scheduleVersion) + except Exception as e: + self.logout() + return False + + return True + + def is_logged_in(self) -> bool: + return self.token is not None def logout(self) -> bool: """ @@ -98,7 +121,7 @@ def logout(self) -> bool: # print(ret) return True except Exception as e: - print("Error Logout\n", e) + print(f"ERROR Logout Failed, exception: {e}") return False def _execute_command(self, command: str, data: List[Dict], multi: bool = False) -> \ @@ -137,5 +160,5 @@ def _execute_command(self, command: str, data: List[Dict], multi: bool = False) response = Request.post(self.url, data=data, params=params) return response.json() except Exception as e: - print(f"Command {command} failed: {e}") + print(f"ERROR Command {command} failed, exception: {e}") raise diff --git a/reolinkapi/mixins/alarm.py b/reolinkapi/mixins/alarm.py index 53bc6ee..0d04624 100644 --- a/reolinkapi/mixins/alarm.py +++ b/reolinkapi/mixins/alarm.py @@ -4,11 +4,22 @@ class AlarmAPIMixin: """API calls for getting device alarm information.""" + def get_alarm(self) -> Dict: + """ + Gets the device alarm motion + See examples/response/GetAlarmMotion.json for example response data. + :return: response json + """ + cmd = "GetAlarm" + body = [{"cmd": cmd, "action": 1, "param": {"Alarm": {"channel": 0, "type": "md"}}}] + return self._execute_command(cmd, body) + def get_alarm_motion(self) -> Dict: """ Gets the device alarm motion See examples/response/GetAlarmMotion.json for example response data. :return: response json """ - body = [{"cmd": "GetAlarm", "action": 1, "param": {"Alarm": {"channel": 0, "type": "md"}}}] - return self._execute_command('GetAlarm', body) + cmd = "GetMdAlarm" + body = [{"cmd": cmd, "action": 1, "param": {"channel": 0}}] + return self._execute_command(cmd, body) diff --git a/reolinkapi/mixins/display.py b/reolinkapi/mixins/display.py index c44c04b..6652557 100644 --- a/reolinkapi/mixins/display.py +++ b/reolinkapi/mixins/display.py @@ -1,4 +1,5 @@ from typing import Dict +import json class DisplayAPIMixin: @@ -22,8 +23,8 @@ def get_mask(self) -> Dict: body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}] return self._execute_command('GetMask', body) - def set_osd(self, bg_color: bool = 0, channel: float = 0, osd_channel_enabled: bool = 0, - osd_channel_name: str = "", osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0, + def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_enabled: bool = 1, + osd_channel_name: str = "Camera", osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 1, osd_time_pos: str = "Lower Right", osd_watermark_enabled: bool = 0) -> bool: """ Set OSD @@ -38,18 +39,21 @@ def set_osd(self, bg_color: bool = 0, channel: float = 0, osd_channel_enabled: b ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"] :return: whether the action was successful """ - body = [{"cmd": "SetOsd", "action": 1, + body = [{"cmd": "SetOsd", # "action": 1, "param": { "Osd": { - "bgcolor": bg_color, + # "bgcolor": bg_color, "channel": channel, "osdChannel": { - "enable": osd_channel_enabled, "name": osd_channel_name, + "enable": int(osd_channel_enabled), + "name": osd_channel_name, "pos": osd_channel_pos }, - "osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}, - "watermark": osd_watermark_enabled, + "osdTime": {"enable": int(osd_time_enabled), + "pos": osd_time_pos}, + "watermark": int(osd_watermark_enabled), }}}] + # print("SetOsd:", json.dumps(body[0], indent=4)) r_data = self._execute_command('SetOsd', body)[0] if 'value' in r_data and r_data["value"]["rspCode"] == 200: return True diff --git a/reolinkapi/mixins/network.py b/reolinkapi/mixins/network.py index f4fe4a6..5ca39d6 100644 --- a/reolinkapi/mixins/network.py +++ b/reolinkapi/mixins/network.py @@ -25,9 +25,127 @@ def set_net_port(self, http_port: float = 80, https_port: float = 443, media_por "rtspPort": rtsp_port }}}] self._execute_command('SetNetPort', body, multi=True) - print("Successfully Set Network Ports") + # print("Successfully Set Network Ports") return True + def set_network_ftp(self, username, password, directory, server_ip, enable) -> Dict: + """ + Set the camera FTP network information + { + "cmd": "GetFtp", + "code": 0, + "value": { + "Ftp": { + "anonymous": 0, + "interval": 15, + "maxSize": 100, + "mode": 0, + "password": "***********", + "port": 21, + "remoteDir": "incoming1", + "schedule": { + "enable": 1, + "table": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + }, + "server": "192.168.1.2", + "streamType": 0, + "userName": "ftpuser" + } + } + } + + For Version 2.0 API: + [{ + "cmd": "SetFtpV20", "param": { + "Ftp": { "anonymous": 0, + "autoDir": 1, + "bpicSingle": 0, "bvideoSingle": 0, + "enable": 1, + "interval": 30, + "maxSize": 100, + "mode": 0, + "onlyFtps": 1, + "password": "***********", + "picCaptureMode": 3, + "picHeight": 1920, + "picInterval": 60, + "picName": "", + "picWidth": 2304, + "port": 21, + "remoteDir": "hello", + "schedule": { + "channel": 0, "table": { + "AI_DOG_CA T": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "AI_PEOPLE": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "AI_VEHICLE": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "MD": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "TIMING": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } }, + "server": "192.168.1.236", + "streamType": 6, + "userName": "ft***er", + "videoName": "sdfs" + } } + }] + :return: response + """ + """ + body = [{ + "action": 0, + "param": { + "Ftp": { + "password": password, + "remoteDir": directory, + "server": server_ip, + "userName": username, + } + } + }] + """ + + cmd = "SetFtp" + if self.scheduleVersion == 1: + cmd = "SetFtpV20" + + body = [ + { + "cmd": cmd, + # "action": 0, + "param": { + "Ftp": { + "password": password, + "remoteDir": directory, + "server": server_ip, + "userName": username + } + } + } + ] + if self.scheduleVersion == 1: + body[0]["param"]["Ftp"]["enable"] = int(enable) + else: + body[0]["param"]["Ftp"]["schedule"] = { "enable": int(enable) } + + # print(f"Sending {cmd} command: ", body) + return self._execute_command(cmd, body) + + def set_network_ntp(self, enable: bool, server: str, port: int = 123, interval: int = 1440) -> Dict: + body = [ + { + "cmd": "SetNtp", + # "action": 0, + "param": { + "Ntp": { + "enable": int(enable), + "server": server, + "port": port, + "interval": interval + } + } + } + ] + return self._execute_command('SetNtp', body) + def set_wifi(self, ssid: str, password: str) -> Dict: body = [{"cmd": "SetWifi", "action": 0, "param": { "Wifi": { @@ -88,8 +206,30 @@ def get_network_email(self) -> Dict: See examples/response/GetNetworkEmail.json for example response data. :return: response json """ - body = [{"cmd": "GetEmail", "action": 0, "param": {}}] - return self._execute_command('GetEmail', body) + cmd = "GetEmail" + if self.scheduleVersion == 1: + cmd = "GetEmailV20" + body = [{"cmd": cmd, "action": 1, "param": { "channel": 0 }}] + return self._execute_command(cmd, body) + + def set_network_email(self, enable: bool) -> Dict: + cmd = "SetEmail" + if self.scheduleVersion == 1: + cmd = "SetEmailV20" + + body = [ { "cmd": cmd, + "param": { + "Email": { + } + } + } + ] + + if self.scheduleVersion == 1: + body[0]["param"]["Email"]["enable"] = int(enable) + else: + body[0]["param"]["Email"]["schedule"] = { "enable": int(enable) } + return self._execute_command('SetEmail', body) def get_network_ftp(self) -> Dict: """ @@ -97,8 +237,11 @@ def get_network_ftp(self) -> Dict: See examples/response/GetNetworkFtp.json for example response data. :return: response json """ - body = [{"cmd": "GetFtp", "action": 0, "param": {}}] - return self._execute_command('GetFtp', body) + cmd = "GetFtp" + if self.scheduleVersion == 1: + cmd = "GetFtpV20" + body = [{"cmd": cmd, "action": 0, "param": {}}] + return self._execute_command(cmd, body) def get_network_push(self) -> Dict: """ @@ -106,8 +249,11 @@ def get_network_push(self) -> Dict: See examples/response/GetNetworkPush.json for example response data. :return: response json """ - body = [{"cmd": "GetPush", "action": 0, "param": {}}] - return self._execute_command('GetPush', body) + cmd = "GetPush" + if self.scheduleVersion == 1: + cmd = "GetPushV20" + body = [{"cmd": cmd, "action": 0, "param": {}}] + return self._execute_command(cmd, body) def get_network_status(self) -> Dict: """ diff --git a/reolinkapi/mixins/record.py b/reolinkapi/mixins/record.py index 375b5ca..70d222d 100644 --- a/reolinkapi/mixins/record.py +++ b/reolinkapi/mixins/record.py @@ -19,17 +19,22 @@ def get_recording_advanced(self) -> Dict: See examples/response/GetRec.json for example response data. :return: response json """ - body = [{"cmd": "GetRec", "action": 1, "param": {"channel": 0}}] - return self._execute_command('GetRec', body) + cmd = "GetRec" + if self.scheduleVersion == 1: + cmd = "GetRecV20" + body = [{"cmd": cmd, "action": 1, "param": {"channel": 0}}] + return self._execute_command(cmd, body) + # NOTE: Changing the camera encoding values apparently makes some (all?) cameras do a full restart. + # Subsequent API calls will fail. Clients must re-connect after the camera resets. def set_recording_encoding(self, - audio: float = 0, - main_bit_rate: float = 8192, - main_frame_rate: float = 8, + audio: bool = True, + main_bit_rate: int = 8192, + main_frame_rate: int = 8, main_profile: str = 'High', - main_size: str = "2560*1440", - sub_bit_rate: float = 160, - sub_frame_rate: float = 7, + main_size: str = "2560*1920", + sub_bit_rate: int = 160, + sub_frame_rate: int = 7, sub_profile: str = 'High', sub_size: str = '640*480') -> Dict: """ @@ -45,13 +50,14 @@ def set_recording_encoding(self, :param sub_size: string Fluent Size :return: response """ + cmd = "SetEnc" body = [ { - "cmd": "SetEnc", + "cmd": cmd, "action": 0, "param": { "Enc": { - "audio": audio, + "audio": int(audio), "channel": 0, "mainStream": { "bitRate": main_bit_rate, @@ -69,4 +75,4 @@ def set_recording_encoding(self, } } ] - return self._execute_command('SetEnc', body) + return self._execute_command(cmd, body) diff --git a/reolinkapi/mixins/stream.py b/reolinkapi/mixins/stream.py index 6798a42..b0d7ed6 100644 --- a/reolinkapi/mixins/stream.py +++ b/reolinkapi/mixins/stream.py @@ -10,6 +10,8 @@ from PIL.Image import Image, open as open_image from reolinkapi.utils.rtsp_client import RtspClient + def __init__(self): + self.rtsp_client = None class StreamAPIMixin: @@ -22,9 +24,13 @@ def open_video_stream(self, callback: Any = None, proxies: Any = None) -> Any: :param callback: :param proxies: Default is none, example: {"host": "localhost", "port": 8000} """ - rtsp_client = RtspClient( + self.rtsp_client = RtspClient( ip=self.ip, username=self.username, password=self.password, profile=self.profile, proxies=proxies, callback=callback) - return rtsp_client.open_stream() + return self.rtsp_client.open_stream() + + def stop_stream(self): + if self.rtsp_client: + self.rtsp_client.stop_stream() def get_snap(self, timeout: float = 3, proxies: Any = None) -> Optional[Image]: """ diff --git a/reolinkapi/mixins/system.py b/reolinkapi/mixins/system.py index dcb590a..577f486 100644 --- a/reolinkapi/mixins/system.py +++ b/reolinkapi/mixins/system.py @@ -8,6 +8,14 @@ def get_general_system(self) -> Dict: body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}] return self._execute_command('get_general_system', body, multi=True) + def get_ability(self) -> Dict: + """ + Get a the users capability set. We need this to know which API calls to use. + :return: response json + """ + body = [{"cmd": "GetAbility", "action": 0, "param": {"User": {"userName": ""}}}] + return self._execute_command('GetAbility', body) + def get_performance(self) -> Dict: """ Get a snapshot of the current performance of the camera. diff --git a/reolinkapi/mixins/user.py b/reolinkapi/mixins/user.py index c382c2d..f873fc0 100644 --- a/reolinkapi/mixins/user.py +++ b/reolinkapi/mixins/user.py @@ -1,4 +1,5 @@ from typing import Dict +import json class UserAPIMixin: @@ -32,23 +33,37 @@ def add_user(self, username: str, password: str, level: str = "guest") -> bool: body = [{"cmd": "AddUser", "action": 0, "param": {"User": {"userName": username, "password": password, "level": level}}}] r_data = self._execute_command('AddUser', body)[0] - if r_data["value"]["rspCode"] == 200: + + if r_data["code"] == 0 and r_data["value"]["rspCode"] == 200: return True - print("Could not add user. Camera responded with:", r_data["value"]) + + print("Could not add user. Camera responded with:", json.dumps(r_data, indent=4)) return False - def modify_user(self, username: str, password: str) -> bool: + def modify_user(self, username: str, oldpassword: 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: whether the user was modified successfully """ - body = [{"cmd": "ModifyUser", "action": 0, "param": {"User": {"userName": username, "password": password}}}] + body = [{"cmd": "ModifyUser", + "action": 0, + "param": {"User": + {"userName": username, + "oldPassword": oldpassword, + "newPassword": password + } + } + } + ] r_data = self._execute_command('ModifyUser', body)[0] - if r_data["value"]["rspCode"] == 200: + # print(f"modify user: {username}\nCamera responded with: {json.dumps(r_data, indent=4)}") + + if r_data["code"] == 0 and r_data["value"]["rspCode"] == 200: return True - print(f"Could not modify user: {username}\nCamera responded with: {r_data['value']}") + + print(f"Could not modify user: {username}\nCamera responded with: {json.dumps(r_data, indent=4)}") return False def delete_user(self, username: str) -> bool: diff --git a/setup.py b/setup.py index 8166180..13649de 100644 --- a/setup.py +++ b/setup.py @@ -32,9 +32,9 @@ def find_version(*file_paths): ] EXTRAS_REQUIRE = { 'streaming': [ - 'numpy==1.19.4', - 'opencv-python==4.4.0.46', - 'Pillow==8.0.1', + 'numpy>=1.19.4', + 'opencv-python>=4.4.0.46', + 'Pillow>=8.0.1', ], }