Last active
March 5, 2023 02:56
-
-
Save mark99i/00dbff3ceb2616dfc6be521a007ffd05 to your computer and use it in GitHub Desktop.
EZVIZ Cloud cameras (RU endpoints)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import base64 | |
import json | |
import time | |
from hashlib import md5 | |
from urllib.parse import urlencode | |
from requests import get, post, put, Response | |
import jwt | |
class BaseRequest: | |
success: bool | |
code: int | |
message: str | |
class LoginResult(BaseRequest): | |
session_id: str | |
session_rf: str | |
area_id: int | |
need_refresh: bool | |
def tostr(self): | |
return json.dumps({ | |
"session_id": self.session_id, | |
"session_rf": self.session_rf, | |
"areaid": self.area_id | |
}, indent=4) | |
@staticmethod | |
def loadstr(data: str) -> 'LoginResult': | |
lr = LoginResult() | |
try: | |
data = json.loads(data) | |
lr.session_rf = data['session_rf'] | |
lr.session_id = data['session_id'] | |
lr.area_id = data['areaid'] | |
nt = int(time.time()) | |
dec = jwt.decode(lr.session_id, verify=False, options={'verify_signature': False}) | |
exp = dec.get('exp') | |
iat = dec.get('iat') | |
halftime = round((exp - iat) / 2) | |
lr.need_refresh = iat + halftime < nt | |
dec = jwt.decode(lr.session_rf, verify=False, options={'verify_signature': False}) | |
exp = dec.get('exp') | |
lr.success = exp > nt | |
except Exception as e: | |
lr.success = False | |
lr.message = f"EzvizApi:LoginResult:{e}" | |
return lr | |
class RecordsResult(BaseRequest): | |
records: list[dict[str, str]] | |
class CamerasResult(BaseRequest): | |
class Page: | |
offset: int | |
limit: int | |
has_next: bool | |
class Camera: | |
class Connection: | |
ip: str | |
mask: str | |
gw: str | |
signal: int | |
ssid: str | |
wan_ip: str | |
cmd_port: int | |
stream_port: int | |
name: str | |
serial: str | |
device_code: str | |
model: str | |
mac: str | |
need_upgrade: bool | |
owner_username: str | |
enc_enabled: bool | |
enc_password: str | |
connection: Connection | |
class CameraList(list[Camera]): | |
def find_by(self, name: str = None, serial: str = None): | |
if serial is not None: | |
for i in self: | |
if i.serial == serial: return i | |
if name is not None: | |
for i in self: | |
if i.name == name: return i | |
return None | |
page: Page | |
cameras: CameraList | |
class Constants: | |
LOGIN_ENDPOINT = 'https://api.ezvizru.com/v3/users/login/v5' | |
LOGIN_RF_ENDPOINT = 'https://api.ezvizru.com/v3/apigateway/login' | |
RECORDS_ENDPOINT = 'https://apiirus.ezvizru.com/v3/streaming/records' | |
CAMERAS_ENDPOINT = 'https://apiirus.ezvizru.com/v3/userdevices/v1/resources/pagelist' | |
FC = "896be422a6df398453e3dd4a6894721c" | |
UA = "okhttp/3.12.1" | |
@staticmethod | |
def get_auth_headers() -> dict: | |
return { | |
"featurecode": Constants.FC, | |
"clienttype": "3", | |
"osversion": "12", | |
"clientversion": "5.9.8.0215", | |
"nettype": "LTE", | |
"customno": "1000001", | |
"ssid": "<unknown ssid>", | |
"clientno": "google", | |
"appid": "ys7", | |
"language": "ru_RU", | |
"lang": "ru", | |
"sessionid": "", | |
"content-type": "application/x-www-form-urlencoded", | |
"user-agent": Constants.UA, | |
} | |
@staticmethod | |
def get_req_headers(lr: LoginResult) -> dict: | |
return { | |
"featurecode": Constants.FC, | |
"appid": "ys7", | |
"sessionid": lr.session_id, | |
"areaid": str(lr.area_id), | |
"user-agent": Constants.UA, | |
} | |
def login(account: str, password: str, device_name: str = "M2012K11AG") -> LoginResult: | |
login_pl = { | |
"account": account, | |
"password": md5(password.encode("utf-8")).hexdigest(), | |
"featureCode": Constants.FC, | |
"msgType": "0", | |
"bizType": "", | |
"cuName": base64.b64encode(device_name.encode('utf-8')), | |
"smsCode": "", | |
} | |
res = post(Constants.LOGIN_ENDPOINT, data=login_pl, headers=Constants.get_auth_headers()) | |
res = json.loads(res.content) | |
lr = LoginResult() | |
lr.code = res['meta']['code'] | |
lr.message = res['meta']['message'] | |
lr.success = lr.code == 200 | |
if lr.success: | |
lr.session_id = res['loginSession']['sessionId'] | |
lr.session_rf = res['loginSession']['rfSessionId'] | |
lr.area_id = res['loginArea']['areaId'] | |
lr.need_refresh = False | |
return lr | |
def get_camera_record_list(lr: LoginResult, serial: str, date: str, tfrom: str, tto: str, max_records: int = 100, channel_no: int = 1) -> RecordsResult: | |
dstart = f"{date}T{tfrom}" | |
dend = f"{date}T{tto}" | |
qs = { | |
"deviceSerial": serial, | |
"channelNo": channel_no, | |
"channelSerial": serial, | |
"startTime": dstart, | |
"stopTime": dend, | |
"size": max_records | |
} | |
res: Response = get(f'{Constants.RECORDS_ENDPOINT}?{urlencode(qs)}', headers=Constants.get_req_headers(lr)) | |
res: dict = json.loads(res.content) | |
rr = RecordsResult() | |
rr.code = res['meta']['code'] | |
rr.message = res['meta']['message'] | |
rr.success = rr.code == 200 | |
if rr.success: | |
rr.records = res['records'] | |
return rr | |
def get_cameras(lr: LoginResult) -> CamerasResult: | |
qs = { | |
"groupId": -1, | |
"limit": 100, | |
"offset": 0, | |
"filter": "WIFI,UPGRADE,CONNECTION,STATUS" | |
} | |
res: Response = get(f'{Constants.CAMERAS_ENDPOINT}?{urlencode(qs)}', headers=Constants.get_req_headers(lr)) | |
res: dict = json.loads(res.content) | |
cr = CamerasResult() | |
cr.code = res['meta']['code'] | |
cr.message = res['meta']['message'] | |
cr.success = cr.code == 200 | |
if not cr.success: return cr | |
cr.page = CamerasResult.Page() | |
cr.page.limit = res['page']['offset'] | |
cr.page.limit = res['page']['limit'] | |
cr.page.has_next = res['page']['hasNext'] | |
cr.cameras = CamerasResult.CameraList() | |
for i in res['deviceInfos']: | |
cam = CamerasResult.Camera() | |
cam.name = i['name'] | |
cam.device_code = i['deviceType'] | |
cam.model = i['deviceCategory'] + " " + i['deviceSubCategory'] | |
cam.serial = i['deviceSerial'] | |
cam.mac = i['mac'] | |
cam.owner_username = i['userName'] | |
cam.need_upgrade = res['UPGRADE'][cam.serial]['isNeedUpgrade'] > 0 | |
cam.enc_enabled = res['STATUS'][cam.serial]['isEncrypt'] > 0 | |
cam.enc_password = res['STATUS'][cam.serial]['encryptPwd'] | |
cam.connection = CamerasResult.Camera.Connection() | |
cam.connection.ip = res['WIFI'][cam.serial]['address'] | |
cam.connection.gw = res['WIFI'][cam.serial]['gateway'] | |
cam.connection.mask = res['WIFI'][cam.serial]['mask'] | |
cam.connection.signal = res['WIFI'][cam.serial]['signal'] | |
cam.connection.ssid = res['WIFI'][cam.serial]['ssid'] | |
cam.connection.wan_ip = res['CONNECTION'][cam.serial]['wanIp'] | |
cam.connection.cmd_port = res['CONNECTION'][cam.serial]['localCmdPort'] | |
cam.connection.stream_port = res['CONNECTION'][cam.serial]['localStreamPort'] | |
cr.cameras.append(cam) | |
return cr | |
def refresh_session(lr: LoginResult) -> bool: | |
rf_pl = { | |
"refreshSessionId": lr.session_rf, | |
"featureCode": Constants.FC | |
} | |
res = put(Constants.LOGIN_RF_ENDPOINT, data=rf_pl, headers=Constants.get_auth_headers()) | |
res = json.loads(res.content) | |
if res['meta']['code'] != 200: | |
lr.message = res['meta']['message'] | |
return False | |
lr.session_rf = res['sessionInfo']['refreshSessionId'] | |
lr.session_id = res['sessionInfo']['sessionId'] | |
lr.need_refresh = False | |
return True | |
if __name__ == '__main__': | |
# | |
# lr.json: | |
# { "session_id": "...", "session_rf": "...", "areaid": 000} | |
# | |
test_lr: LoginResult = LoginResult.loadstr(open('lr.json', 'r').read()) | |
if not test_lr.success: | |
print("need relogin (session_rf expired)") | |
else: | |
if test_lr.need_refresh: | |
print("need session refresh (session_id can be expired)") | |
if refresh_session(test_lr): | |
with open('lr.json', 'w') as f: | |
f.write(test_lr.tostr()) | |
else: | |
raise Exception(f"Refresh session error: {test_lr.message}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment