pyezvizapi 1.0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/__init__.py +54 -0
- pyezvizapi/__main__.py +459 -0
- pyezvizapi/api_endpoints.py +52 -0
- pyezvizapi/camera.py +258 -0
- pyezvizapi/cas.py +169 -0
- pyezvizapi/client.py +2089 -0
- pyezvizapi/constants.py +381 -0
- pyezvizapi/exceptions.py +29 -0
- pyezvizapi/light_bulb.py +126 -0
- pyezvizapi/mqtt.py +259 -0
- pyezvizapi/test_cam_rtsp.py +149 -0
- pyezvizapi/utils.py +160 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE +201 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE.md +201 -0
- pyezvizapi-1.0.0.0.dist-info/METADATA +18 -0
- pyezvizapi-1.0.0.0.dist-info/RECORD +19 -0
- pyezvizapi-1.0.0.0.dist-info/WHEEL +5 -0
- pyezvizapi-1.0.0.0.dist-info/entry_points.txt +2 -0
- pyezvizapi-1.0.0.0.dist-info/top_level.txt +1 -0
pyezvizapi/camera.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""pyezvizapi camera api."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from .constants import BatteryCameraWorkMode, DeviceSwitchType, SoundMode
|
|
8
|
+
from .exceptions import PyEzvizError
|
|
9
|
+
from .utils import fetch_nested_value, string_to_list
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import EzvizClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EzvizCamera:
|
|
16
|
+
"""Initialize Ezviz camera object."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, client: EzvizClient, serial: str, device_obj: dict | None = None
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize the camera object."""
|
|
22
|
+
self._client = client
|
|
23
|
+
self._serial = serial
|
|
24
|
+
self._alarmmotiontrigger: dict[str, Any] = {
|
|
25
|
+
"alarm_trigger_active": False,
|
|
26
|
+
"timepassed": None,
|
|
27
|
+
}
|
|
28
|
+
self._device = (
|
|
29
|
+
device_obj if device_obj else self._client.get_device_infos(self._serial)
|
|
30
|
+
)
|
|
31
|
+
self._last_alarm: dict[str, Any] = {}
|
|
32
|
+
self._switch: dict[int, bool] = {
|
|
33
|
+
switch["type"]: switch["enable"] for switch in self._device["SWITCH"]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def fetch_key(self, keys: list, default_value: Any = None) -> Any:
|
|
37
|
+
"""Fetch dictionary key."""
|
|
38
|
+
return fetch_nested_value(self._device, keys, default_value)
|
|
39
|
+
|
|
40
|
+
def _alarm_list(self) -> None:
|
|
41
|
+
"""Get last alarm info for this camera's self._serial."""
|
|
42
|
+
_alarmlist = self._client.get_alarminfo(self._serial)
|
|
43
|
+
|
|
44
|
+
if fetch_nested_value(_alarmlist, ["page", "totalResults"], 0) > 0:
|
|
45
|
+
self._last_alarm = _alarmlist["alarms"][0]
|
|
46
|
+
return self._motion_trigger()
|
|
47
|
+
|
|
48
|
+
def _local_ip(self) -> Any:
|
|
49
|
+
"""Fix empty ip value for certain cameras."""
|
|
50
|
+
if (
|
|
51
|
+
self.fetch_key(["WIFI", "address"])
|
|
52
|
+
and self._device["WIFI"]["address"] != "0.0.0.0"
|
|
53
|
+
):
|
|
54
|
+
return self._device["WIFI"]["address"]
|
|
55
|
+
|
|
56
|
+
# Seems to return none or 0.0.0.0 on some.
|
|
57
|
+
if (
|
|
58
|
+
self.fetch_key(["CONNECTION", "localIp"])
|
|
59
|
+
and self._device["CONNECTION"]["localIp"] != "0.0.0.0"
|
|
60
|
+
):
|
|
61
|
+
return self._device["CONNECTION"]["localIp"]
|
|
62
|
+
|
|
63
|
+
return "0.0.0.0"
|
|
64
|
+
|
|
65
|
+
def _motion_trigger(self) -> None:
|
|
66
|
+
"""Create motion sensor based on last alarm time."""
|
|
67
|
+
if not self._last_alarm.get("alarmStartTimeStr"):
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
_today_date = datetime.date.today()
|
|
71
|
+
_now = datetime.datetime.now().replace(microsecond=0)
|
|
72
|
+
|
|
73
|
+
_last_alarm_time = datetime.datetime.strptime(
|
|
74
|
+
self._last_alarm["alarmStartTimeStr"].replace("Today", str(_today_date)),
|
|
75
|
+
"%Y-%m-%d %H:%M:%S",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# returns a timedelta object
|
|
79
|
+
timepassed = _now - _last_alarm_time
|
|
80
|
+
|
|
81
|
+
self._alarmmotiontrigger = {
|
|
82
|
+
"alarm_trigger_active": bool(timepassed < datetime.timedelta(seconds=60)),
|
|
83
|
+
"timepassed": timepassed.total_seconds(),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def _is_alarm_schedules_enabled(self) -> bool:
|
|
87
|
+
"""Check if alarm schedules enabled."""
|
|
88
|
+
_alarm_schedules = [
|
|
89
|
+
item for item in self._device["TIME_PLAN"] if item.get("type") == 2
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if _alarm_schedules:
|
|
93
|
+
return bool(_alarm_schedules[0].get("enable"))
|
|
94
|
+
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def status(self) -> dict[Any, Any]:
|
|
98
|
+
"""Return the status of the camera."""
|
|
99
|
+
self._alarm_list()
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"serial": self._serial,
|
|
103
|
+
"name": self.fetch_key(["deviceInfos", "name"]),
|
|
104
|
+
"version": self.fetch_key(["deviceInfos", "version"]),
|
|
105
|
+
"upgrade_available": bool(
|
|
106
|
+
self.fetch_key(["UPGRADE", "isNeedUpgrade"]) == 3
|
|
107
|
+
),
|
|
108
|
+
"status": self.fetch_key(["deviceInfos", "status"]),
|
|
109
|
+
"device_category": self.fetch_key(["deviceInfos", "deviceCategory"]),
|
|
110
|
+
"device_sub_category": self.fetch_key(["deviceInfos", "deviceSubCategory"]),
|
|
111
|
+
"upgrade_percent": self.fetch_key(["STATUS", "upgradeProcess"]),
|
|
112
|
+
"upgrade_in_progress": bool(
|
|
113
|
+
self.fetch_key(["STATUS", "upgradeStatus"]) == 0
|
|
114
|
+
),
|
|
115
|
+
"latest_firmware_info": self.fetch_key(["UPGRADE", "upgradePackageInfo"]),
|
|
116
|
+
"alarm_notify": bool(self.fetch_key(["STATUS", "globalStatus"])),
|
|
117
|
+
"alarm_schedules_enabled": self._is_alarm_schedules_enabled(),
|
|
118
|
+
"alarm_sound_mod": SoundMode(
|
|
119
|
+
self.fetch_key(["STATUS", "alarmSoundMode"], -1)
|
|
120
|
+
).name,
|
|
121
|
+
"encrypted": bool(self.fetch_key(["STATUS", "isEncrypt"])),
|
|
122
|
+
"encrypted_pwd_hash": self.fetch_key(["STATUS", "encryptPwd"]),
|
|
123
|
+
"local_ip": self._local_ip(),
|
|
124
|
+
"wan_ip": self.fetch_key(["CONNECTION", "netIp"]),
|
|
125
|
+
"mac_address": self.fetch_key(["deviceInfos", "mac"]),
|
|
126
|
+
"local_rtsp_port": self.fetch_key(["CONNECTION", "localRtspPort"], "554")
|
|
127
|
+
if self.fetch_key(["CONNECTION", "localRtspPort"], "554") != 0
|
|
128
|
+
else "554",
|
|
129
|
+
"supported_channels": self.fetch_key(["deviceInfos", "channelNumber"]),
|
|
130
|
+
"battery_level": self.fetch_key(["STATUS", "optionals", "powerRemaining"]),
|
|
131
|
+
"PIR_Status": self.fetch_key(["STATUS", "pirStatus"]),
|
|
132
|
+
"Motion_Trigger": self._alarmmotiontrigger["alarm_trigger_active"],
|
|
133
|
+
"Seconds_Last_Trigger": self._alarmmotiontrigger["timepassed"],
|
|
134
|
+
"last_alarm_time": self._last_alarm.get("alarmStartTimeStr"),
|
|
135
|
+
"last_alarm_pic": self._last_alarm.get(
|
|
136
|
+
"picUrl",
|
|
137
|
+
"https://eustatics.ezvizlife.com/ovs_mall/web/img/index/EZVIZ_logo.png?ver=3007907502",
|
|
138
|
+
),
|
|
139
|
+
"last_alarm_type_code": self._last_alarm.get("alarmType", "0000"),
|
|
140
|
+
"last_alarm_type_name": self._last_alarm.get("sampleName", "NoAlarm"),
|
|
141
|
+
"cam_timezone": self.fetch_key(["STATUS", "optionals", "timeZone"]),
|
|
142
|
+
"push_notify_alarm": not bool(self.fetch_key(["NODISTURB", "alarmEnable"])),
|
|
143
|
+
"push_notify_call": not bool(
|
|
144
|
+
self.fetch_key(["NODISTURB", "callingEnable"])
|
|
145
|
+
),
|
|
146
|
+
"alarm_light_luminance": self.fetch_key(
|
|
147
|
+
["STATUS", "optionals", "Alarm_Light", "luminance"]
|
|
148
|
+
),
|
|
149
|
+
"Alarm_DetectHumanCar": self.fetch_key(
|
|
150
|
+
["STATUS", "optionals", "Alarm_DetectHumanCar", "type"]
|
|
151
|
+
),
|
|
152
|
+
"diskCapacity": string_to_list(
|
|
153
|
+
self.fetch_key(["STATUS", "optionals", "diskCapacity"])
|
|
154
|
+
),
|
|
155
|
+
"NightVision_Model": self.fetch_key(
|
|
156
|
+
["STATUS", "optionals", "NightVision_Model"]
|
|
157
|
+
),
|
|
158
|
+
"battery_camera_work_mode": BatteryCameraWorkMode(
|
|
159
|
+
self.fetch_key(["STATUS", "optionals", "batteryCameraWorkMode"], -1)
|
|
160
|
+
).name,
|
|
161
|
+
"Alarm_AdvancedDetect": self.fetch_key(
|
|
162
|
+
["STATUS", "optionals", "Alarm_AdvancedDetect", "type"]
|
|
163
|
+
),
|
|
164
|
+
"wifiInfos": self._device["WIFI"],
|
|
165
|
+
"switches": self._switch,
|
|
166
|
+
"optionals": self.fetch_key(["STATUS", "optionals"]),
|
|
167
|
+
"supportExt": self._device["deviceInfos"]["supportExt"],
|
|
168
|
+
"ezDeviceCapability": self.fetch_key(["deviceInfos", "ezDeviceCapability"]),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def move(self, direction: str, speed: int = 5) -> bool:
|
|
172
|
+
"""Move camera."""
|
|
173
|
+
if direction not in ["right", "left", "down", "up"]:
|
|
174
|
+
raise PyEzvizError(f"Invalid direction: {direction} ")
|
|
175
|
+
|
|
176
|
+
# launch the start command
|
|
177
|
+
self._client.ptz_control(str(direction).upper(), self._serial, "START", speed)
|
|
178
|
+
# launch the stop command
|
|
179
|
+
self._client.ptz_control(str(direction).upper(), self._serial, "STOP", speed)
|
|
180
|
+
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def move_coordinates(self, x_axis: float, y_axis: float) -> bool:
|
|
184
|
+
"""Move camera to specified coordinates."""
|
|
185
|
+
return self._client.ptz_control_coordinates(self._serial, x_axis, y_axis)
|
|
186
|
+
|
|
187
|
+
def alarm_notify(self, enable: int) -> bool:
|
|
188
|
+
"""Enable/Disable camera notification when movement is detected."""
|
|
189
|
+
return self._client.set_camera_defence(self._serial, enable)
|
|
190
|
+
|
|
191
|
+
def alarm_sound(self, sound_type: int) -> bool:
|
|
192
|
+
"""Enable/Disable camera sound when movement is detected."""
|
|
193
|
+
# we force enable = 1 , to make sound...
|
|
194
|
+
return self._client.alarm_sound(self._serial, sound_type, 1)
|
|
195
|
+
|
|
196
|
+
def do_not_disturb(self, enable: int) -> bool:
|
|
197
|
+
"""Enable/Disable do not disturb.
|
|
198
|
+
|
|
199
|
+
if motion triggers are normally sent to your device as a
|
|
200
|
+
notification, then enabling this feature stops these notification being sent.
|
|
201
|
+
The alarm event is still recorded in the EzViz app as normal.
|
|
202
|
+
"""
|
|
203
|
+
return self._client.do_not_disturb(self._serial, enable)
|
|
204
|
+
|
|
205
|
+
def alarm_detection_sensibility(
|
|
206
|
+
self, sensibility: int, type_value: int = 0
|
|
207
|
+
) -> bool | str:
|
|
208
|
+
"""Enable/Disable camera sound when movement is detected."""
|
|
209
|
+
# we force enable = 1 , to make sound...
|
|
210
|
+
return self._client.detection_sensibility(self._serial, sensibility, type_value)
|
|
211
|
+
|
|
212
|
+
def switch_device_audio(self, enable: int = 0) -> bool:
|
|
213
|
+
"""Switch audio status on a device."""
|
|
214
|
+
return self._client.switch_status(
|
|
215
|
+
self._serial, DeviceSwitchType.SOUND.value, enable
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def switch_device_state_led(self, enable: int = 0) -> bool:
|
|
219
|
+
"""Switch led status on a device."""
|
|
220
|
+
return self._client.switch_status(
|
|
221
|
+
self._serial, DeviceSwitchType.LIGHT.value, enable
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def switch_device_ir_led(self, enable: int = 0) -> bool:
|
|
225
|
+
"""Switch ir status on a device."""
|
|
226
|
+
return self._client.switch_status(
|
|
227
|
+
self._serial, DeviceSwitchType.INFRARED_LIGHT.value, enable
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def switch_privacy_mode(self, enable: int = 0) -> bool:
|
|
231
|
+
"""Switch privacy mode on a device."""
|
|
232
|
+
return self._client.switch_status(
|
|
233
|
+
self._serial, DeviceSwitchType.PRIVACY.value, enable
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def switch_sleep_mode(self, enable: int = 0) -> bool:
|
|
237
|
+
"""Switch sleep mode on a device."""
|
|
238
|
+
return self._client.switch_status(
|
|
239
|
+
self._serial, DeviceSwitchType.SLEEP.value, enable
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def switch_follow_move(self, enable: int = 0) -> bool:
|
|
243
|
+
"""Switch follow move."""
|
|
244
|
+
return self._client.switch_status(
|
|
245
|
+
self._serial, DeviceSwitchType.MOBILE_TRACKING.value, enable
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def switch_sound_alarm(self, enable: int = 0) -> bool:
|
|
249
|
+
"""Sound alarm on a device."""
|
|
250
|
+
return self._client.sound_alarm(self._serial, enable)
|
|
251
|
+
|
|
252
|
+
def change_defence_schedule(self, schedule: str, enable: int = 0) -> bool:
|
|
253
|
+
"""Change defence schedule. Requires json formatted schedules."""
|
|
254
|
+
return self._client.api_set_defence_schedule(self._serial, schedule, enable)
|
|
255
|
+
|
|
256
|
+
def set_battery_camera_work_mode(self, work_mode: BatteryCameraWorkMode) -> bool:
|
|
257
|
+
"""Change work mode for battery powered camera device."""
|
|
258
|
+
return self._client.set_battery_camera_work_mode(self._serial, work_mode.value)
|
pyezvizapi/cas.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""pyezvizapi CAS API Functions."""
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from itertools import cycle
|
|
5
|
+
import random
|
|
6
|
+
import socket
|
|
7
|
+
import ssl
|
|
8
|
+
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
import xmltodict
|
|
11
|
+
|
|
12
|
+
from .constants import FEATURE_CODE, XOR_KEY
|
|
13
|
+
from .exceptions import InvalidHost
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def xor_enc_dec(msg, xor_key=XOR_KEY):
|
|
17
|
+
"""Xor encodes camera serial."""
|
|
18
|
+
with BytesIO(msg) as stream:
|
|
19
|
+
xor_msg = bytes(a ^ b for a, b in zip(stream.read(), cycle(xor_key)))
|
|
20
|
+
return xor_msg
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EzvizCAS:
|
|
24
|
+
"""Ezviz CAS server client."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, token) -> None:
|
|
27
|
+
"""Initialize the client object."""
|
|
28
|
+
self._session = None
|
|
29
|
+
self._token = token or {
|
|
30
|
+
"session_id": None,
|
|
31
|
+
"rf_session_id": None,
|
|
32
|
+
"username": None,
|
|
33
|
+
"api_url": "apiieu.ezvizlife.com",
|
|
34
|
+
}
|
|
35
|
+
self._service_urls = token["service_urls"]
|
|
36
|
+
|
|
37
|
+
def cas_get_encryption(self, devserial):
|
|
38
|
+
"""Fetch encryption code from ezviz cas server."""
|
|
39
|
+
|
|
40
|
+
# Random hex 64 characters long.
|
|
41
|
+
rand_hex = random.randrange(10**80)
|
|
42
|
+
rand_hex = "%064x" % rand_hex
|
|
43
|
+
rand_hex = rand_hex[:64]
|
|
44
|
+
|
|
45
|
+
payload = (
|
|
46
|
+
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00"
|
|
47
|
+
f"\x00\x02" # Check or order?
|
|
48
|
+
f"\x00\x00\x00\x00\x00\x00 "
|
|
49
|
+
f"\x01" # Check or order?
|
|
50
|
+
f"\x00\x00\x00\x00\x00\x00\x02\t\x00\x00\x00\x00"
|
|
51
|
+
f'<?xml version="1.0" encoding="utf-8"?>\n<Request>\n\t'
|
|
52
|
+
f'<ClientID>{self._token["session_id"]}</ClientID>'
|
|
53
|
+
f"\n\t<Sign>{FEATURE_CODE}</Sign>\n\t"
|
|
54
|
+
f"<DevSerial>{devserial}</DevSerial>"
|
|
55
|
+
f"\n\t<ClientType>0</ClientType>\n</Request>\n"
|
|
56
|
+
).encode("latin1")
|
|
57
|
+
|
|
58
|
+
payload_end_padding = rand_hex.encode("latin1")
|
|
59
|
+
|
|
60
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
|
61
|
+
|
|
62
|
+
context.set_ciphers(
|
|
63
|
+
"DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Create a TCP/IP socket
|
|
67
|
+
my_socket = socket.create_connection(
|
|
68
|
+
(self._service_urls["sysConf"][15], self._service_urls["sysConf"][16])
|
|
69
|
+
)
|
|
70
|
+
my_socket = context.wrap_socket(
|
|
71
|
+
my_socket, server_hostname=self._service_urls["sysConf"][15]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Get CAS Encryption Key
|
|
75
|
+
try:
|
|
76
|
+
my_socket.send(payload + payload_end_padding)
|
|
77
|
+
response = my_socket.recv(1024)
|
|
78
|
+
print(f"Get Encryption Key: {response}")
|
|
79
|
+
|
|
80
|
+
except (socket.gaierror, ConnectionRefusedError) as err:
|
|
81
|
+
raise InvalidHost("Invalid IP or Hostname") from err
|
|
82
|
+
|
|
83
|
+
finally:
|
|
84
|
+
my_socket.close()
|
|
85
|
+
|
|
86
|
+
# Trim header, tail and convert xml to dict.
|
|
87
|
+
response = response[32::]
|
|
88
|
+
response = response[:-32:]
|
|
89
|
+
response = xmltodict.parse(response)
|
|
90
|
+
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
def set_camera_defence_state(self, serial, enable=1):
|
|
94
|
+
"""Enable alarm notifications."""
|
|
95
|
+
|
|
96
|
+
# Random hex 64 characters long.
|
|
97
|
+
rand_hex = random.randrange(10**80)
|
|
98
|
+
rand_hex = "%064x" % rand_hex
|
|
99
|
+
rand_hex = rand_hex[:64]
|
|
100
|
+
|
|
101
|
+
payload = (
|
|
102
|
+
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00"
|
|
103
|
+
f"\x00\x14" # Check or order?
|
|
104
|
+
f"\x00\x00\x00\x00\x00\x00 "
|
|
105
|
+
f"\x05"
|
|
106
|
+
f"\x00\x00\x00\x00\x00\x00\x02\xd0\x00\x00\x01\xe0"
|
|
107
|
+
f'<?xml version="1.0" encoding="utf-8"?>\n<Request>\n\t'
|
|
108
|
+
f'<Verify ClientSession="{self._token["session_id"]}" '
|
|
109
|
+
f'ToDevice="{serial}" ClientType="0" />\n\t'
|
|
110
|
+
f'<Message Length="240" />\n</Request>\n'
|
|
111
|
+
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00"
|
|
112
|
+
f"\x00\x13"
|
|
113
|
+
f"\x00\x00\x00\x00\x00\x000\x0f\xff\xff\xff\xff"
|
|
114
|
+
f"\x00\x00\x00\xb0\x00\x00\x00\x00"
|
|
115
|
+
).encode("latin1")
|
|
116
|
+
|
|
117
|
+
payload_end_padding = rand_hex.encode("latin1")
|
|
118
|
+
|
|
119
|
+
# xor camera serial
|
|
120
|
+
xor_cam_serial = xor_enc_dec(serial.encode("latin1"))
|
|
121
|
+
|
|
122
|
+
defence_msg_string = (
|
|
123
|
+
f'{xor_cam_serial.decode()}2+,*xdv.0" '
|
|
124
|
+
f'encoding="utf-8"?>\n'
|
|
125
|
+
f"<Request>\n"
|
|
126
|
+
f"\t<OperationCode>ABCDEFG</OperationCode>\n"
|
|
127
|
+
f'\t<Defence Type="Global" Status="{enable}" Actor="V" Channel="0" />\n'
|
|
128
|
+
f"</Request>\n"
|
|
129
|
+
f"\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10"
|
|
130
|
+
).encode("latin1")
|
|
131
|
+
|
|
132
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
|
133
|
+
|
|
134
|
+
context.set_ciphers(
|
|
135
|
+
"DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Create a TCP/IP socket
|
|
139
|
+
my_socket = socket.create_connection(
|
|
140
|
+
(self._service_urls["sysConf"][15], self._service_urls["sysConf"][16])
|
|
141
|
+
)
|
|
142
|
+
my_socket = context.wrap_socket(
|
|
143
|
+
my_socket, server_hostname=self._service_urls["sysConf"][15]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
cas_client = self.cas_get_encryption(serial)
|
|
147
|
+
|
|
148
|
+
aes_key = cas_client["Response"]["Session"]["@Key"].encode("latin1")
|
|
149
|
+
iv_value = (
|
|
150
|
+
f"{serial}{cas_client['Response']['Session']['@OperationCode']}".encode(
|
|
151
|
+
"latin1"
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Message encryption
|
|
156
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, iv_value)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
defence_msg_string = cipher.encrypt(defence_msg_string)
|
|
160
|
+
my_socket.send(payload + defence_msg_string + payload_end_padding)
|
|
161
|
+
print(f"Set camera response: {my_socket.recv()}")
|
|
162
|
+
|
|
163
|
+
except (socket.gaierror, ConnectionRefusedError) as err:
|
|
164
|
+
raise InvalidHost("Invalid IP or Hostname") from err
|
|
165
|
+
|
|
166
|
+
finally:
|
|
167
|
+
my_socket.close()
|
|
168
|
+
|
|
169
|
+
return True
|