├── requirements.txt ├── pyezviz ├── __init__.py ├── camera.py ├── __main__.py └── client.py ├── setup.py ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==2.8 2 | pandas==0.25.3 -------------------------------------------------------------------------------- /pyezviz/__init__.py: -------------------------------------------------------------------------------- 1 | from pyezviz.client import EzvizClient 2 | from pyezviz.camera import EzvizCamera 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | # with open("README.md", "r") as fh: 4 | # long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='pyEzviz', 8 | version="0.1.5.5", 9 | license='Apache Software License', 10 | author='Pierre Ourdouille', 11 | author_email='baqs@users.github.com', 12 | description='Pilot your Ezviz cameras', 13 | long_description="Pilot your Ezviz cameras with this module. Please view readme on github", 14 | url='http://github.com/baqs/pyEzviz/', 15 | packages=setuptools.find_packages(include=['pyezviz']), 16 | setup_requires=[ 17 | 'requests', 18 | 'setuptools' 19 | ], 20 | install_requires=[ 21 | 'requests', 22 | 'fake_useragent', 23 | 'uuid', 24 | 'pandas' 25 | ], 26 | entry_points={ 27 | 'console_scripts': [ 28 | 'pyezviz = pyezviz.__main__:main' 29 | ] 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ezviz PyPi 2 | 3 | [![Build Status](https://travis-ci.org/BaQs/pyEzviz.svg?branch=master)](https://travis-ci.org/BaQs/pyEzviz) 4 | 5 | Pilot your Ezviz cameras with this module. 6 | 7 | ### Installing 8 | 9 | 10 | ``` 11 | pip install pyezviz 12 | ``` 13 | 14 | ## Playing with it 15 | 16 | ``` 17 | pyezviz -u em@il -p PASS device -h 18 | ... 19 | pyezviz -u em@il -p PASS --debug devices status 20 | serial name status privacy audio ir_led state_led follow_move alarm_notify alarm_sound_mod encrypted local_ip detection_sensibility 21 | 0 D733333333 C6N(D73333333) 1 False True True True True False Software True 192.168.2.10 3 22 | 1 D733333333 C6N(D73333333) 1 False True True True True False Software True 192.168.2.13 4 23 | 2 D833333333 C6N(D83333333) 1 False True True True True False Disabled True 192.168.2.12 3 24 | 3 D833333333 C6N(D83333333) 1 False True True True False False Software True 192.168.2.11 3 25 | 4 D933333333 C6N(D93333333) 1 False True True True False False Software True 192.168.2.14 3 26 | 27 | 28 | ``` 29 | 30 | 31 | ## Running the tests 32 | The tox configuration is already included. 33 | Simply launch: 34 | ``` 35 | $ tox 36 | ``` 37 | 38 | (Do not forget to 'pip install tox' if you do not have it.) 39 | Tests are written in the tests directory. 40 | tests/data folder contains samples of EzvizLife API for tests purposes. 41 | 42 | 43 | ## Side notes 44 | 45 | As there is no official documentation on the API, I had to reverse-engineer what is the one used in the Ezviz IOS APP. 46 | 47 | 48 | ## Contributing 49 | 50 | Any contribution is welcome, considering the number of features the API provides, there is room for improvement! 51 | 52 | ## Versioning 53 | 54 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/baqs/pyEzviz/tags). 55 | 56 | ## Authors 57 | 58 | ## License 59 | 60 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 61 | 62 | ## Acknowledgments 63 | 64 | 65 | ## Changelog 66 | 67 | 68 | ### 0.0.x 69 | Draft versions 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /pyezviz/camera.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | # seems to be some internal reference. 21 = sleep mode 4 | TYPE_PRIVACY_MODE = 21 5 | TYPE_AUDIO = 22 6 | TYPE_STATE_LED = 3 7 | TYPE_IR_LED = 10 8 | TYPE_FOLLOW_MOVE = 25 9 | 10 | KEY_ALARM_NOTIFICATION = 'globalStatus' 11 | 12 | ALARM_SOUND_MODE= { 0 : 'Software', 13 | 1 : 'Intensive', 14 | 2 : 'Disabled', 15 | } 16 | 17 | class EzvizCamera(object): 18 | def __init__(self, client, serial): 19 | """Initialize the camera object.""" 20 | self._serial = serial 21 | self._client = client 22 | 23 | self._loaded = 0 24 | 25 | # self.load() 26 | 27 | 28 | def load(self): 29 | """Load object properties""" 30 | page_list = self._client.get_PAGE_LIST() 31 | 32 | # we need to know the index of this camera's self._serial 33 | for device in page_list['deviceInfos']: 34 | if device['deviceSerial'] == self._serial : 35 | self._device = device 36 | break 37 | 38 | for camera in page_list['cameraInfos']: 39 | if camera['deviceSerial'] == self._serial : 40 | self._camera_infos = camera 41 | break 42 | 43 | # global status 44 | self._status = page_list['statusInfos'][self._serial] 45 | 46 | 47 | # load connection infos 48 | self._connection = page_list['connectionInfos'][self._serial] 49 | 50 | # # a bit of wifi infos 51 | # self._wifi = page_list['wifiStatusInfos'][self._serial] 52 | 53 | # # load switches 54 | switches = {} 55 | for switch in page_list['switchStatusInfos'][self._serial]: 56 | switches[switch['type']] = switch 57 | 58 | self._switch = switches 59 | 60 | # load detection sensibility 61 | self._detection_sensibility = self._client.get_detection_sensibility(self._serial) 62 | 63 | # # load camera object 64 | # try: 65 | # # self._switch = page_list['switchStatusInfos'][self._serial] 66 | # self._time_plan = page_list['timePlanInfos'][self._serial] 67 | # self._nodisturb = page_list['alarmNodisturbInfos'][self._serial] 68 | # self._kms = page_list['kmsInfos'][self._serial] 69 | # self._hiddns = page_list['hiddnsInfos'][self._serial] 70 | # self._p2p = page_list['p2pInfos'][self._serial] 71 | 72 | # except BaseException as exp: 73 | # print(exp) 74 | # return 1 75 | 76 | self._loaded = 1 77 | 78 | return True 79 | 80 | 81 | def status(self): 82 | """Return the status of the camera.""" 83 | 84 | if not self._loaded: 85 | self.load() 86 | 87 | return { 88 | 'serial': self._serial, 89 | 'name': self._device['name'], 90 | 'status': self._device['status'], 91 | 'device_sub_category': self._device['deviceSubCategory'], 92 | 93 | 'privacy': self._switch.get(TYPE_PRIVACY_MODE)['enable'], 94 | 'audio': self._switch.get(TYPE_AUDIO)['enable'], 95 | 'ir_led': self._switch.get(TYPE_IR_LED)['enable'], 96 | 'state_led': self._switch.get(TYPE_STATE_LED)['enable'], 97 | 'follow_move': self._switch.get(TYPE_FOLLOW_MOVE)['enable'], 98 | 99 | 'alarm_notify': bool(self._status[KEY_ALARM_NOTIFICATION]), 100 | 'alarm_sound_mod': ALARM_SOUND_MODE[int(self._status['alarmSoundMode'])], 101 | # 'alarm_sound_mod': 'Intensive', 102 | 103 | 'encrypted': bool(self._status['isEncrypt']), 104 | 105 | 'local_ip': self._connection['localIp'], 106 | 'local_rtsp_port': self._connection['localRtspPort'], 107 | 108 | 'detection_sensibility': self._detection_sensibility, 109 | 110 | } 111 | 112 | 113 | def move(self, direction, speed=5): 114 | """Moves the camera.""" 115 | if direction not in ['right','left','down','up']: 116 | raise PyEzvizError("Invalid direction: %s ", command) 117 | 118 | # launch the start command 119 | self._client.ptzControl(str(direction).upper(), self._serial, 'START', speed) 120 | # launch the stop command 121 | self._client.ptzControl(str(direction).upper(), self._serial, 'STOP', speed) 122 | 123 | return True 124 | 125 | def alarm_notify(self, enable): 126 | """Enable/Disable camera notification when movement is detected.""" 127 | return self._client.data_report(self._serial, enable) 128 | 129 | def alarm_sound(self, sound_type): 130 | """Enable/Disable camera sound when movement is detected.""" 131 | # we force enable = 1 , to make sound... 132 | return self._client.alarm_sound(self._serial, sound_type, 1) 133 | 134 | def alarm_detection_sensibility(self, sensibility): 135 | """Enable/Disable camera sound when movement is detected.""" 136 | # we force enable = 1 , to make sound... 137 | return self._client.detection_sensibility(self._serial, sensibility) 138 | 139 | def switch_device_audio(self, enable=0): 140 | """Switch audio status on a device.""" 141 | return self._client.switch_status(self._serial, TYPE_AUDIO, enable) 142 | 143 | def switch_device_state_led(self, enable=0): 144 | """Switch audio status on a device.""" 145 | return self._client.switch_status(self._serial, TYPE_STATE_LED, enable) 146 | 147 | def switch_device_ir_led(self, enable=0): 148 | """Switch audio status on a device.""" 149 | return self._client.switch_status(self._serial, TYPE_IR_LED, enable) 150 | 151 | def switch_privacy_mode(self, enable=0): 152 | """Switch privacy mode on a device.""" 153 | return self._client.switch_status(self._serial, TYPE_PRIVACY_MODE, enable) 154 | 155 | def switch_follow_move(self, enable=0): 156 | """Switch follow move.""" 157 | return self._client.switch_status(self._serial, TYPE_FOLLOW_MOVE, enable) 158 | -------------------------------------------------------------------------------- /pyezviz/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import json 4 | import logging 5 | import pandas 6 | 7 | 8 | from pyezviz import EzvizClient, EzvizCamera 9 | 10 | 11 | def main(): 12 | """Main function""" 13 | parser = argparse.ArgumentParser(prog='pyezviz') 14 | parser.add_argument('-u', '--username', required=True, help='Ezviz username') 15 | parser.add_argument('-p', '--password', required=True, help='Ezviz Password') 16 | parser.add_argument('--debug', '-d', action='store_true', help='Print debug messages to stderr') 17 | 18 | subparsers = parser.add_subparsers(dest='action') 19 | 20 | 21 | parser_device = subparsers.add_parser('devices', help='Play with all devices at once') 22 | parser_device.add_argument('device_action', type=str, 23 | default='status', help='Device action to perform', choices=['device','status','switch','connection','switch-all']) 24 | 25 | 26 | parser_camera = subparsers.add_parser('camera', help='Camera actions') 27 | parser_camera.add_argument('--serial', required=True, help='camera SERIAL') 28 | 29 | subparsers_camera = parser_camera.add_subparsers(dest='camera_action') 30 | 31 | parser_camera_status = subparsers_camera.add_parser('status', help='Get the status of the camera') 32 | # parser_camera_status.add_argument('--status', required=True, help='Status to status', choices=['device','camera','switch','connection','wifi','status']) 33 | 34 | parser_camera_move = subparsers_camera.add_parser('move', help='Move the camera') 35 | parser_camera_move.add_argument('--direction', required=True, help='Direction to move the camera to', choices=['up','down','right','left']) 36 | parser_camera_move.add_argument('--speed', required=False, help='Speed of the movement', default=5, type=int, choices=range(1, 10)) 37 | 38 | 39 | parser_camera_switch = subparsers_camera.add_parser('switch', help='Change the status of a switch') 40 | parser_camera_switch.add_argument('--switch', required=True, help='Switch to switch', choices=['audio','ir','state','privacy','follow_move']) 41 | parser_camera_switch.add_argument('--enable', required=False, help='Enable (or not)', default=1, type=int, choices=[0,1] ) 42 | 43 | parser_camera_alarm = subparsers_camera.add_parser('alarm', help='Configure the camera alarm') 44 | parser_camera_alarm.add_argument('--notify', required=False, help='Enable (or not)', default=0, type=int, choices=[0,1] ) 45 | parser_camera_alarm.add_argument('--sound', required=False, help='Sound level (2 is disabled, 1 intensive, 0 software)', type=int, choices=[0,1,2]) 46 | parser_camera_alarm.add_argument('--sensibility', required=False, help='Sensibility level (form 1 to 6)', default=3, type=int, choices=[0,1,2,3,4,5,6] ) 47 | 48 | 49 | args = parser.parse_args() 50 | 51 | # print("--------------args") 52 | # print("--------------args: %s",args) 53 | # print("--------------args") 54 | 55 | client = EzvizClient(args.username, args.password) 56 | 57 | if args.debug: 58 | 59 | import http.client 60 | http.client.HTTPConnection.debuglevel = 5 61 | # You must initialize logging, otherwise you'll not see debug output. 62 | logging.basicConfig() 63 | logging.getLogger().setLevel(logging.DEBUG) 64 | requests_log = logging.getLogger("requests.packages.urllib3") 65 | requests_log.setLevel(logging.DEBUG) 66 | requests_log.propagate = True 67 | 68 | 69 | if args.action == 'devices': 70 | 71 | if args.device_action == 'device': 72 | try: 73 | client.login() 74 | print(json.dumps(client.get_DEVICE(), indent=2)) 75 | except BaseException as exp: 76 | print(exp) 77 | return 1 78 | finally: 79 | client.close_session() 80 | 81 | if args.device_action == 'status': 82 | try: 83 | client.login() 84 | # print(json.dumps(client.load_cameras(), indent=2)) 85 | print(pandas.DataFrame(client.load_cameras())) 86 | except BaseException as exp: 87 | print(exp) 88 | return 1 89 | finally: 90 | client.close_session() 91 | 92 | if args.device_action == 'switch': 93 | try: 94 | client.login() 95 | print(json.dumps(client.get_SWITCH_STATUS(), indent=2)) 96 | except BaseException as exp: 97 | print(exp) 98 | return 1 99 | finally: 100 | client.close_session() 101 | 102 | elif args.device_action == 'connection': 103 | try: 104 | client.login() 105 | print(json.dumps(client.get_CONNECTION(), indent=2)) 106 | except BaseException as exp: 107 | print(exp) 108 | return 1 109 | finally: 110 | client.close_session() 111 | 112 | elif args.device_action == 'switch-all': 113 | try: 114 | client.login() 115 | print(json.dumps(client.switch_devices(args.enable), indent=2)) 116 | except BaseException as exp: 117 | print(exp) 118 | return 1 119 | finally: 120 | client.close_session() 121 | 122 | 123 | elif args.action == 'camera': 124 | 125 | # load camera object 126 | try: 127 | client.login() 128 | camera = EzvizCamera(client, args.serial) 129 | logging.debug("Camera loaded") 130 | except BaseException as exp: 131 | print(exp) 132 | return 1 133 | 134 | # if args.camera_action == 'list': 135 | # try: 136 | # pagelist = client.get_PAGE_LIST() 137 | # df = pandas.DataFrame(pagelist['statusInfos']) 138 | # df 139 | 140 | # except BaseException as exp: 141 | # print(exp) 142 | # return 1 143 | # finally: 144 | # client.close_session() 145 | 146 | if args.camera_action == 'move': 147 | try: 148 | camera.move(args.direction, args.speed) 149 | except BaseException as exp: 150 | print(exp) 151 | return 1 152 | finally: 153 | client.close_session() 154 | 155 | elif args.camera_action == 'status': 156 | try: 157 | # camera.load() 158 | # if args.status == 'device': 159 | # print(camera._device) 160 | # elif args.status == 'status': 161 | # print(camera._status) 162 | # elif args.status == 'switch': 163 | # # print(json.dumps(camera._switch, indent=2)) 164 | # print(camera._switch) 165 | # elif args.status == 'connection': 166 | # # print(json.dumps(camera._switch, indent=2)) 167 | # print(camera._connection) 168 | # elif args.status == 'wifi': 169 | # # print(json.dumps(camera._switch, indent=2)) 170 | # print(camera._wifi) 171 | # print(camera.status()) 172 | print(json.dumps(camera.status(), indent=2)) 173 | 174 | except BaseException as exp: 175 | print(exp) 176 | return 1 177 | finally: 178 | client.close_session() 179 | 180 | elif args.camera_action == 'switch': 181 | try: 182 | if args.switch == 'ir': 183 | camera.switch_device_ir_led(args.enable) 184 | elif args.switch == 'state': 185 | camera.switch_device_state_led(args.enable) 186 | elif args.switch == 'audio': 187 | camera.switch_device_audio(args.enable) 188 | elif args.switch == 'privacy': 189 | camera.switch_privacy_mode(args.enable) 190 | elif args.switch == 'follow_move': 191 | camera.switch_follow_move(args.enable) 192 | except BaseException as exp: 193 | print(exp) 194 | return 1 195 | finally: 196 | client.close_session() 197 | 198 | elif args.camera_action == 'alarm': 199 | try: 200 | if args.sound: 201 | camera.alarm_sound(args.sound) 202 | if args.notify: 203 | camera.alarm_notify(args.notify) 204 | if args.sensibility: 205 | camera.alarm_detection_sensibility(args.sensibility) 206 | except BaseException as exp: 207 | print(exp) 208 | return 1 209 | finally: 210 | client.close_session() 211 | else: 212 | print("Action not implemented: %s", args.action) 213 | 214 | if __name__ == '__main__': 215 | sys.exit(main()) 216 | 217 | 218 | -------------------------------------------------------------------------------- /pyezviz/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import logging 4 | import hashlib 5 | import time 6 | from fake_useragent import UserAgent 7 | from uuid import uuid4 8 | from .camera import EzvizCamera 9 | # from pyezviz.camera import EzvizCamera 10 | 11 | COOKIE_NAME = "sessionId" 12 | CAMERA_DEVICE_CATEGORY = "IPC" 13 | DOORBELL_DEVICE_CATEGORY = "BDoorBell" 14 | 15 | 16 | EU_API_DOMAIN = "apiieu" 17 | API_BASE_TLD = "ezvizlife.com" 18 | API_BASE_URI = "https://" + EU_API_DOMAIN + "." + API_BASE_TLD 19 | API_ENDPOINT_LOGIN = "/v3/users/login" 20 | API_ENDPOINT_CLOUDDEVICES = "/api/cloud/v2/cloudDevices/getAll" 21 | API_ENDPOINT_PAGELIST = "/v3/userdevices/v1/devices/pagelist" 22 | API_ENDPOINT_DEVICES = "/v3/devices/" 23 | API_ENDPOINT_SWITCH_STATUS = '/api/device/switchStatus' 24 | API_ENDPOINT_PTZCONTROL = "/ptzControl" 25 | API_ENDPOINT_ALARM_SOUND = "/alarm/sound" 26 | API_ENDPOINT_DATA_REPORT = "/api/other/data/report" 27 | API_ENDPOINT_DETECTION_SENSIBILITY = "/api/device/configAlgorithm" 28 | API_ENDPOINT_DETECTION_SENSIBILITY_GET = "/api/device/queryAlgorithmConfig" 29 | 30 | LOGIN_URL = API_BASE_URI + API_ENDPOINT_LOGIN 31 | CLOUDDEVICES_URL = API_BASE_URI + API_ENDPOINT_CLOUDDEVICES 32 | DEVICES_URL = API_BASE_URI + API_ENDPOINT_DEVICES 33 | PAGELIST_URL = API_BASE_URI + API_ENDPOINT_PAGELIST 34 | DATA_REPORT_URL = API_BASE_URI + API_ENDPOINT_DATA_REPORT 35 | 36 | SWITCH_STATUS_URL = API_BASE_URI + API_ENDPOINT_SWITCH_STATUS 37 | DETECTION_SENSIBILITY_URL = API_BASE_URI + API_ENDPOINT_DETECTION_SENSIBILITY 38 | DETECTION_SENSIBILITY_GET_URL = API_BASE_URI + API_ENDPOINT_DETECTION_SENSIBILITY_GET 39 | 40 | 41 | 42 | DEFAULT_TIMEOUT = 10 43 | MAX_RETRIES = 3 44 | 45 | 46 | 47 | 48 | class PyEzvizError(Exception): 49 | pass 50 | 51 | 52 | class EzvizClient(object): 53 | def __init__(self, account, password, session=None, sessionId=None, timeout=None, cloud=None, connection=None): 54 | """Initialize the client object.""" 55 | self.account = account 56 | self.password = password 57 | # self._user_id = None 58 | # self._user_reference = None 59 | self._session = session 60 | self._sessionId = sessionId 61 | self._data = {} 62 | self._timeout = timeout 63 | self._CLOUD = cloud 64 | self._CONNECTION = connection 65 | 66 | def _login(self, apiDomain=EU_API_DOMAIN): 67 | """Login to Ezviz' API.""" 68 | 69 | # Ezviz API sends md5 of password 70 | m = hashlib.md5() 71 | m.update(self.password.encode('utf-8')) 72 | md5pass = m.hexdigest() 73 | payload = {"account": self.account, "password": md5pass, "featureCode": "92c579faa0902cbfcfcc4fc004ef67e7"} 74 | 75 | try: 76 | req = self._session.post("https://" + apiDomain + "." + API_BASE_TLD + API_ENDPOINT_LOGIN, 77 | data=payload, 78 | headers={"Content-Type": "application/x-www-form-urlencoded", 79 | "clientType": "1", 80 | "customNo": "1000001"}, 81 | timeout=self._timeout) 82 | except OSError: 83 | raise PyEzvizError("Can not login to API") 84 | 85 | if req.status_code == 400: 86 | raise PyEzvizError("Login error: Please check your username/password: %s ", str(req.text)) 87 | 88 | 89 | # let's parse the answer, session is in {.."loginSession":{"sessionId":"xxx...} 90 | try: 91 | response_json = req.json() 92 | 93 | # if the apidomain is not proper 94 | if response_json["meta"]["code"] == 1100: 95 | return self._login(response_json["loginArea"]["apiDomain"]) 96 | 97 | sessionId = str(response_json["loginSession"]["sessionId"]) 98 | if not sessionId: 99 | raise PyEzvizError("Login error: Please check your username/password: %s ", str(req.text)) 100 | 101 | self._sessionId = sessionId 102 | 103 | except (OSError, json.decoder.JSONDecodeError) as e: 104 | raise PyEzvizError("Impossible to decode response: \nResponse was: [%s] %s", str(e), str(req.status_code), str(req.text)) 105 | 106 | 107 | return True 108 | 109 | def _get_pagelist(self, filter=None, json_key=None, max_retries=0): 110 | """Get data from pagelist API.""" 111 | 112 | if max_retries > MAX_RETRIES: 113 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 114 | 115 | if filter == None: 116 | raise PyEzvizError("Trying to call get_pagelist without filter") 117 | 118 | try: 119 | req = self._session.get(PAGELIST_URL, 120 | params={'filter': filter}, 121 | headers={ 'sessionId': self._sessionId}, 122 | timeout=self._timeout) 123 | 124 | except OSError as e: 125 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 126 | 127 | if req.status_code == 401: 128 | # session is wrong, need to relogin 129 | self.login() 130 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 131 | return self._get_pagelist(max_retries+1) 132 | 133 | if req.text is "": 134 | raise PyEzvizError("No data") 135 | 136 | try: 137 | json_output = req.json() 138 | except (OSError, json.decoder.JSONDecodeError) as e: 139 | raise PyEzvizError("Impossible to decode response: " + str(e) + "\nResponse was: " + str(req.text)) 140 | 141 | if json_key == None: 142 | json_result = json_output 143 | else: 144 | json_result = json_output[json_key] 145 | 146 | if not json_result: 147 | raise PyEzvizError("Impossible to load the devices, here is the returned response: %s ", str(req.text)) 148 | 149 | return json_result 150 | 151 | def _switch_status(self, serial, status_type, enable, max_retries=0): 152 | """Switch status on a device""" 153 | 154 | try: 155 | req = self._session.post(SWITCH_STATUS_URL, 156 | data={ 'sessionId': self._sessionId, 157 | 'enable': enable, 158 | 'serial': serial, 159 | 'channel': '0', 160 | 'netType' : 'WIFI', 161 | 'clientType': '1', 162 | 'type': status_type}, 163 | timeout=self._timeout) 164 | 165 | 166 | if req.status_code == 401: 167 | # session is wrong, need to relogin 168 | self.login() 169 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 170 | return self._switch_status(serial, type, enable, max_retries+1) 171 | 172 | response_json = req.json() 173 | if response_json['resultCode'] != '0': 174 | raise PyEzvizError("Could not set the switch, maybe a permission issue ?: Got %s : %s)",str(req.status_code), str(req.text)) 175 | return False 176 | except OSError as e: 177 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 178 | 179 | return True 180 | 181 | def _switch_devices_privacy(self, enable=0): 182 | """Switch privacy status on ALL devices (batch)""" 183 | 184 | # enable=1 means privacy is ON 185 | 186 | # get all devices 187 | devices = self._get_devices() 188 | 189 | # foreach, launch a switchstatus for the proper serial 190 | for idx, device in enumerate(devices): 191 | serial = devices[idx]['serial'] 192 | self._switch_status(serial, TYPE_PRIVACY_MODE, enable) 193 | 194 | return True 195 | 196 | def load_cameras(self): 197 | """Load and return all cameras objects""" 198 | 199 | # get all devices 200 | devices = self.get_DEVICE() 201 | cameras = [] 202 | 203 | # foreach, launch a switchstatus for the proper serial 204 | for idx, device in enumerate(devices): 205 | if devices[idx]['deviceCategory'] == CAMERA_DEVICE_CATEGORY: 206 | camera = EzvizCamera(self, device['deviceSerial']) 207 | camera.load() 208 | cameras.append(camera.status()) 209 | if devices[idx]['deviceCategory'] == DOORBELL_DEVICE_CATEGORY: 210 | camera = EzvizCamera(self, device['deviceSerial']) 211 | camera.load() 212 | cameras.append(camera.status()) 213 | 214 | return cameras 215 | 216 | def ptzControl(self, command, serial, action, speed=5, max_retries=0): 217 | """PTZ Control by API.""" 218 | if max_retries > MAX_RETRIES: 219 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 220 | 221 | if command == None: 222 | raise PyEzvizError("Trying to call ptzControl without command") 223 | if action == None: 224 | raise PyEzvizError("Trying to call ptzControl without action") 225 | 226 | 227 | try: 228 | req = self._session.put(DEVICES_URL + serial + API_ENDPOINT_PTZCONTROL, 229 | data={'command': command, 230 | 'action': action, 231 | 'channelNo': "1", 232 | 'speed': speed, 233 | 'uuid': str(uuid4()), 234 | 'serial': serial}, 235 | headers={ 'sessionId': self._sessionId, 236 | 'clientType': "1"}, 237 | timeout=self._timeout) 238 | 239 | except OSError as e: 240 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 241 | 242 | if req.status_code == 401: 243 | # session is wrong, need to re-log-in 244 | self.login() 245 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 246 | return self.ptzControl(max_retries+1) 247 | 248 | def login(self): 249 | """Set http session.""" 250 | if self._sessionId is None: 251 | self._session = requests.session() 252 | # adding fake user-agent header 253 | self._session.headers.update({'User-agent': str(UserAgent().random)}) 254 | 255 | return self._login() 256 | 257 | def data_report(self, serial, enable=1, max_retries=0): 258 | """Enable alarm notifications.""" 259 | if max_retries > MAX_RETRIES: 260 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 261 | 262 | # operationType = 2 if disable, and 1 if enable 263 | operationType = 2 - int(enable) 264 | print(f"enable: {enable}, operationType: {operationType}") 265 | 266 | try: 267 | req = self._session.post(DATA_REPORT_URL, 268 | data={ 'clientType': '1', 269 | 'infoDetail': json.dumps({ 270 | "operationType" : int(operationType), 271 | "detail" : '0', 272 | "deviceSerial" : serial + ",2" 273 | }, separators=(',',':')), 274 | 'infoType': '3', 275 | 'netType': 'WIFI', 276 | 'reportData': None, 277 | 'requestType': '0', 278 | 'sessionId': self._sessionId 279 | }, 280 | timeout=self._timeout) 281 | 282 | except OSError as e: 283 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 284 | 285 | if req.status_code == 401: 286 | # session is wrong, need to re-log-in 287 | self.login() 288 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 289 | return self.data_report(serial, enable, max_retries+1) 290 | 291 | return True 292 | # soundtype: 0 = normal, 1 = intensive, 2 = disabled ... don't ask me why... 293 | 294 | def detection_sensibility(self, serial, sensibility=3, max_retries=0): 295 | """Enable alarm notifications.""" 296 | if max_retries > MAX_RETRIES: 297 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 298 | 299 | if sensibility not in [0,1,2,3,4,5,6]: 300 | raise PyEzvizError("Unproper sensibility (should be within 1 to 6).") 301 | 302 | try: 303 | req = self._session.post(DETECTION_SENSIBILITY_URL, 304 | data={ 'subSerial' : serial, 305 | 'type': '0', 306 | 'sessionId': self._sessionId, 307 | 'value': sensibility, 308 | }, 309 | timeout=self._timeout) 310 | 311 | except OSError as e: 312 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 313 | 314 | if req.status_code == 401: 315 | # session is wrong, need to re-log-in 316 | self.login() 317 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 318 | return self.detection_sensibility(serial, enable, max_retries+1) 319 | 320 | return True 321 | 322 | def get_detection_sensibility(self, serial, max_retries=0): 323 | """Enable alarm notifications.""" 324 | if max_retries > MAX_RETRIES: 325 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 326 | 327 | try: 328 | req = self._session.post(DETECTION_SENSIBILITY_GET_URL, 329 | data={ 'subSerial' : serial, 330 | 'sessionId': self._sessionId, 331 | 'clientType': 1 332 | }, 333 | timeout=self._timeout) 334 | 335 | except OSError as e: 336 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 337 | 338 | if req.status_code == 401: 339 | # session is wrong, need to re-log-in 340 | self.login() 341 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 342 | return self.get_detection_sensibility(serial, enable, max_retries+1) 343 | elif req.status_code != 200: 344 | raise PyEzvizError("Could not get detection sensibility: Got %s : %s)",str(req.status_code), str(req.text)) 345 | 346 | response_json = req.json() 347 | if response_json['resultCode'] != '0': 348 | # raise PyEzvizError("Could not get detection sensibility: Got %s : %s)",str(req.status_code), str(req.text)) 349 | return 'Unknown' 350 | else: 351 | return response_json['algorithmConfig']['algorithmList'][0]['value'] 352 | 353 | def alarm_sound(self, serial, soundType, enable=1, max_retries=0): 354 | """Enable alarm sound by API.""" 355 | if max_retries > MAX_RETRIES: 356 | raise PyEzvizError("Can't gather proper data. Max retries exceeded.") 357 | 358 | if soundType not in [0,1,2]: 359 | raise PyEzvizError("Invalid soundType, should be 0,1,2: " + str(soundType)) 360 | 361 | try: 362 | req = self._session.put(DEVICES_URL + serial + API_ENDPOINT_ALARM_SOUND, 363 | data={ 'enable': enable, 364 | 'soundType': soundType, 365 | 'voiceId': '0', 366 | 'deviceSerial': serial 367 | }, 368 | headers={ 'sessionId': self._sessionId}, 369 | timeout=self._timeout) 370 | 371 | except OSError as e: 372 | raise PyEzvizError("Could not access Ezviz' API: " + str(e)) 373 | 374 | if req.status_code == 401: 375 | # session is wrong, need to re-log-in 376 | self.login() 377 | logging.info("Got 401, relogging (max retries: %s)",str(max_retries)) 378 | return self.alarm_sound(serial, enable, soundType, max_retries+1) 379 | elif req.status_code != 200: 380 | logging.error("Got %s : %s)",str(req.status_code), str(req.text)) 381 | 382 | return True 383 | 384 | def switch_devices_privacy(self,enable=0): 385 | """Switch status on all devices.""" 386 | return self._switch_devices_privacy(enable) 387 | 388 | def switch_status(self, serial, status_type, enable=0): 389 | """Switch status of a device.""" 390 | return self._switch_status(serial, status_type, enable) 391 | 392 | def get_PAGE_LIST(self, max_retries=0): 393 | return self._get_pagelist(filter='CLOUD,TIME_PLAN,CONNECTION,SWITCH,STATUS,WIFI,STATUS_EXT,NODISTURB,P2P,TTS,KMS,HIDDNS', json_key=None) 394 | 395 | def get_DEVICE(self, max_retries=0): 396 | return self._get_pagelist(filter='CLOUD',json_key='deviceInfos') 397 | 398 | def get_CONNECTION(self, max_retries=0): 399 | return self._get_pagelist(filter='CONNECTION',json_key='connectionInfos') 400 | 401 | def get_STATUS(self, max_retries=0): 402 | return self._get_pagelist(filter='STATUS',json_key='statusInfos') 403 | 404 | def get_SWITCH(self, max_retries=0): 405 | return self._get_pagelist(filter='SWITCH',json_key='switchStatusInfos') 406 | 407 | def get_WIFI(self, max_retries=0): 408 | return self._get_pagelist(filter='WIFI',json_key='wifiInfos') 409 | 410 | def get_NODISTURB(self, max_retries=0): 411 | return self._get_pagelist(filter='NODISTURB',json_key='alarmNodisturbInfos') 412 | 413 | def get_P2P(self, max_retries=0): 414 | return self._get_pagelist(filter='P2P',json_key='p2pInfos') 415 | 416 | def get_KMS(self, max_retries=0): 417 | return self._get_pagelist(filter='KMS',json_key='kmsInfos') 418 | 419 | def get_TIME_PLAN(self, max_retries=0): 420 | return self._get_pagelist(filter='TIME_PLAN',json_key='timePlanInfos') 421 | 422 | def close_session(self): 423 | """Close current session.""" 424 | self._session.close() 425 | self._session = None --------------------------------------------------------------------------------