pyezvizapi 1.0.2.3__py3-none-any.whl → 1.0.4.3__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.
- pyezvizapi/__init__.py +54 -0
- pyezvizapi/__main__.py +124 -10
- pyezvizapi/api_endpoints.py +53 -1
- pyezvizapi/camera.py +196 -15
- pyezvizapi/cas.py +4 -2
- pyezvizapi/client.py +3693 -953
- pyezvizapi/constants.py +33 -4
- pyezvizapi/feature.py +536 -0
- pyezvizapi/light_bulb.py +1 -1
- pyezvizapi/mqtt.py +22 -16
- pyezvizapi/test_cam_rtsp.py +43 -21
- pyezvizapi/test_mqtt.py +53 -11
- pyezvizapi/utils.py +182 -71
- pyezvizapi-1.0.4.3.dist-info/METADATA +286 -0
- pyezvizapi-1.0.4.3.dist-info/RECORD +21 -0
- pyezvizapi-1.0.2.3.dist-info/METADATA +0 -27
- pyezvizapi-1.0.2.3.dist-info/RECORD +0 -21
- pyezvizapi-1.0.2.3.dist-info/entry_points.txt +0 -2
- {pyezvizapi-1.0.2.3.dist-info → pyezvizapi-1.0.4.3.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.2.3.dist-info → pyezvizapi-1.0.4.3.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.2.3.dist-info → pyezvizapi-1.0.4.3.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.2.3.dist-info → pyezvizapi-1.0.4.3.dist-info}/top_level.txt +0 -0
pyezvizapi/constants.py
CHANGED
|
@@ -6,13 +6,25 @@ the Ezviz API to descriptive names.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from enum import Enum, unique
|
|
9
|
+
from hashlib import md5
|
|
10
|
+
import uuid
|
|
9
11
|
|
|
10
|
-
from .utils import generate_unique_code
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
def _generate_unique_code() -> str:
|
|
14
|
+
"""Generate a deterministic unique code for this host."""
|
|
15
|
+
|
|
16
|
+
mac_int = uuid.getnode()
|
|
17
|
+
mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
|
|
18
|
+
return md5(mac_str.encode("utf-8")).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FEATURE_CODE = _generate_unique_code()
|
|
13
22
|
XOR_KEY = b"\x0c\x0eJ^X\x15@Rr"
|
|
14
23
|
DEFAULT_TIMEOUT = 25
|
|
15
24
|
MAX_RETRIES = 3
|
|
25
|
+
# Unified message API default subtype that returns all alarm categories.
|
|
26
|
+
DEFAULT_UNIFIEDMSG_STYPE = "92"
|
|
27
|
+
HIK_ENCRYPTION_HEADER = b"hikencodepicture"
|
|
16
28
|
REQUEST_HEADER = {
|
|
17
29
|
"featureCode": FEATURE_CODE,
|
|
18
30
|
"clientType": "3",
|
|
@@ -34,7 +46,7 @@ APP_SECRET = "17454517-cc1c-42b3-a845-99b4a15dd3e6"
|
|
|
34
46
|
|
|
35
47
|
@unique
|
|
36
48
|
class MessageFilterType(Enum):
|
|
37
|
-
"""
|
|
49
|
+
"""Fine-grained message filters used by the unified list API."""
|
|
38
50
|
|
|
39
51
|
FILTER_TYPE_MOTION = 2402
|
|
40
52
|
FILTER_TYPE_PERSON = 2403
|
|
@@ -44,6 +56,16 @@ class MessageFilterType(Enum):
|
|
|
44
56
|
FILTER_TYPE_SYSTEM_MESSAGE = 2101
|
|
45
57
|
|
|
46
58
|
|
|
59
|
+
@unique
|
|
60
|
+
class UnifiedMessageSubtype(str, Enum):
|
|
61
|
+
"""High-level subtype bundles supported by the Ezviz mobile app."""
|
|
62
|
+
|
|
63
|
+
# Equivalent to the "All alarm" chip in the official app UI.
|
|
64
|
+
ALL_ALARMS = "92"
|
|
65
|
+
# Same comma-separated bundle returned by msgDefaultSubtype() inside the app.
|
|
66
|
+
DEFAULT_APP_SUBTYPE = "9904,2701"
|
|
67
|
+
|
|
68
|
+
|
|
47
69
|
@unique
|
|
48
70
|
class DeviceSwitchType(Enum):
|
|
49
71
|
"""Device switch name and number."""
|
|
@@ -84,11 +106,14 @@ class DeviceSwitchType(Enum):
|
|
|
84
106
|
TAMPER_ALARM = 306
|
|
85
107
|
DETECTION_TYPE = 451
|
|
86
108
|
OUTLET_RECOVER = 600
|
|
109
|
+
WIDE_DYNAMIC_RANGE = 604
|
|
87
110
|
CHIME_INDICATOR_LIGHT = 611
|
|
111
|
+
DISTORTION_CORRECTION = 617
|
|
88
112
|
TRACKING = 650
|
|
89
113
|
CRUISE_TRACKING = 651
|
|
90
114
|
PARTIAL_IMAGE_OPTIMIZE = 700
|
|
91
115
|
FEATURE_TRACKING = 701
|
|
116
|
+
LOGO_WATERMARK = 702
|
|
92
117
|
|
|
93
118
|
|
|
94
119
|
@unique
|
|
@@ -173,6 +198,7 @@ class SupportExt(Enum):
|
|
|
173
198
|
SupportDisk = 4
|
|
174
199
|
SupportDiskBlackList = 367
|
|
175
200
|
SupportDistributionNetworkBetweenDevice = 420
|
|
201
|
+
SupportDistortionCorrection = 490
|
|
176
202
|
SupportDisturbMode = 217
|
|
177
203
|
SupportDisturbNewMode = 292
|
|
178
204
|
SupportDoorCallPlayBack = 545
|
|
@@ -221,6 +247,7 @@ class SupportExt(Enum):
|
|
|
221
247
|
SupportLightRelate = 297
|
|
222
248
|
SupportLocalConnect = 507
|
|
223
249
|
SupportLocalLockGate = 662
|
|
250
|
+
SupportLogoWatermark = 632
|
|
224
251
|
SupportLockConfigWay = 679
|
|
225
252
|
SupportMessage = 6
|
|
226
253
|
SupportMicroVolumnSet = 77
|
|
@@ -233,6 +260,7 @@ class SupportExt(Enum):
|
|
|
233
260
|
SupportMultiChannelFlip = 732
|
|
234
261
|
SupportMultiChannelSharedService = 720
|
|
235
262
|
SupportMultiChannelType = 719
|
|
263
|
+
SupportAdvancedDetectType = 793
|
|
236
264
|
SupportMultiScreen = 17
|
|
237
265
|
SupportMultiSubsys = 255
|
|
238
266
|
SupportMultilensPlay = 665
|
|
@@ -328,6 +356,7 @@ class SupportExt(Enum):
|
|
|
328
356
|
SupportSleep = 62
|
|
329
357
|
SupportSmartBodyDetect = 244
|
|
330
358
|
SupportSmartNightVision = 274
|
|
359
|
+
SupportWideDynamicRange = 273
|
|
331
360
|
SupportSoundLightAlarm = 214
|
|
332
361
|
SupportSsl = 25
|
|
333
362
|
SupportStopRecordVideo = 219
|
|
@@ -464,4 +493,4 @@ class DeviceCatagories(Enum):
|
|
|
464
493
|
BASE_STATION_DEVICE_CATEGORY = "XVR"
|
|
465
494
|
CAT_EYE_CATEGORY = "CatEye"
|
|
466
495
|
LIGHTING = "lighting"
|
|
467
|
-
W2H_BASE_STATION_DEVICE_CATEGORY = "IGateWay"
|
|
496
|
+
W2H_BASE_STATION_DEVICE_CATEGORY = "IGateWay"
|
pyezvizapi/feature.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Helpers for working with Ezviz feature metadata payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from .utils import WILDCARD_STEP, coerce_int, decode_json, first_nested
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _feature_video_section(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
12
|
+
"""Return the nested Video feature section from feature info payload."""
|
|
13
|
+
|
|
14
|
+
video = first_nested(
|
|
15
|
+
camera_data,
|
|
16
|
+
("FEATURE_INFO", WILDCARD_STEP, "Video"),
|
|
17
|
+
)
|
|
18
|
+
if isinstance(video, MutableMapping):
|
|
19
|
+
return cast(dict[str, Any], video)
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def supplement_light_params(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
24
|
+
"""Return SupplementLightMgr parameters if present."""
|
|
25
|
+
|
|
26
|
+
video = _feature_video_section(camera_data)
|
|
27
|
+
if not video:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
manager: Any = video.get("SupplementLightMgr")
|
|
31
|
+
manager = decode_json(manager)
|
|
32
|
+
if not isinstance(manager, Mapping):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
params: Any = manager.get("ImageSupplementLightModeSwitchParams")
|
|
36
|
+
params = decode_json(params)
|
|
37
|
+
return dict(params) if isinstance(params, Mapping) else {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def supplement_light_enabled(camera_data: Mapping[str, Any]) -> bool:
|
|
41
|
+
"""Return True when intelligent fill light is enabled."""
|
|
42
|
+
|
|
43
|
+
params = supplement_light_params(camera_data)
|
|
44
|
+
if not params:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
enabled = params.get("enabled")
|
|
48
|
+
if isinstance(enabled, bool):
|
|
49
|
+
return enabled
|
|
50
|
+
if isinstance(enabled, str):
|
|
51
|
+
lowered = enabled.strip().lower()
|
|
52
|
+
if lowered in {"true", "1", "yes", "on"}:
|
|
53
|
+
return True
|
|
54
|
+
if lowered in {"false", "0", "no", "off"}:
|
|
55
|
+
return False
|
|
56
|
+
return bool(enabled)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def supplement_light_available(camera_data: Mapping[str, Any]) -> bool:
|
|
60
|
+
"""Return True when intelligent fill light parameters are present."""
|
|
61
|
+
|
|
62
|
+
return bool(supplement_light_params(camera_data))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def lens_defog_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
66
|
+
"""Return the LensCleaning defog configuration if present."""
|
|
67
|
+
|
|
68
|
+
video = _feature_video_section(camera_data)
|
|
69
|
+
lens = video.get("LensCleaning") if isinstance(video, Mapping) else None
|
|
70
|
+
if not isinstance(lens, MutableMapping):
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
config = lens.get("DefogCfg")
|
|
74
|
+
if isinstance(config, MutableMapping):
|
|
75
|
+
return cast(dict[str, Any], config)
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def lens_defog_value(camera_data: Mapping[str, Any]) -> int:
|
|
80
|
+
"""Return canonical defogging mode (0=auto,1=on,2=off)."""
|
|
81
|
+
|
|
82
|
+
cfg = lens_defog_config(camera_data)
|
|
83
|
+
if not cfg:
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
enabled = bool(cfg.get("enabled"))
|
|
87
|
+
mode = str(cfg.get("defogMode") or "").lower()
|
|
88
|
+
|
|
89
|
+
if not enabled:
|
|
90
|
+
return 2
|
|
91
|
+
|
|
92
|
+
if mode == "open":
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def optionals_mapping(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
99
|
+
"""Return decoded optionals mapping from the camera payload."""
|
|
100
|
+
|
|
101
|
+
status_info = camera_data.get("statusInfo")
|
|
102
|
+
optionals: Any = None
|
|
103
|
+
if isinstance(status_info, Mapping):
|
|
104
|
+
optionals = status_info.get("optionals")
|
|
105
|
+
|
|
106
|
+
optionals = decode_json(optionals)
|
|
107
|
+
|
|
108
|
+
if not isinstance(optionals, Mapping):
|
|
109
|
+
optionals = decode_json(camera_data.get("optionals"))
|
|
110
|
+
|
|
111
|
+
if not isinstance(optionals, Mapping):
|
|
112
|
+
status = camera_data.get("STATUS")
|
|
113
|
+
if isinstance(status, Mapping):
|
|
114
|
+
optionals = decode_json(status.get("optionals"))
|
|
115
|
+
|
|
116
|
+
return dict(optionals) if isinstance(optionals, Mapping) else {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def optionals_dict(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
120
|
+
"""Return convenience wrapper for optionals mapping."""
|
|
121
|
+
|
|
122
|
+
return optionals_mapping(camera_data)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def custom_voice_volume_config(camera_data: Mapping[str, Any]) -> dict[str, int] | None:
|
|
126
|
+
"""Return current CustomVoice volume configuration."""
|
|
127
|
+
|
|
128
|
+
optionals = optionals_mapping(camera_data)
|
|
129
|
+
config = optionals.get("CustomVoice_Volume")
|
|
130
|
+
config = decode_json(config)
|
|
131
|
+
if not isinstance(config, Mapping):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
volume = coerce_int(config.get("volume"))
|
|
135
|
+
mic = coerce_int(config.get("microphone_volume"))
|
|
136
|
+
result: dict[str, int] = {}
|
|
137
|
+
if isinstance(volume, int):
|
|
138
|
+
result["volume"] = volume
|
|
139
|
+
if isinstance(mic, int):
|
|
140
|
+
result["microphone_volume"] = mic
|
|
141
|
+
return result or None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def iter_algorithm_entries(camera_data: Mapping[str, Any]) -> Iterator[dict[str, Any]]:
|
|
145
|
+
"""Yield entries from the AlgorithmInfo optionals list."""
|
|
146
|
+
|
|
147
|
+
entries = optionals_dict(camera_data).get("AlgorithmInfo")
|
|
148
|
+
if not isinstance(entries, Iterable):
|
|
149
|
+
return
|
|
150
|
+
for entry in entries:
|
|
151
|
+
if isinstance(entry, Mapping):
|
|
152
|
+
yield dict(entry)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def iter_channel_algorithm_entries(
|
|
156
|
+
camera_data: Mapping[str, Any], channel: int
|
|
157
|
+
) -> Iterator[dict[str, Any]]:
|
|
158
|
+
"""Yield AlgorithmInfo entries filtered by channel."""
|
|
159
|
+
|
|
160
|
+
for entry in iter_algorithm_entries(camera_data):
|
|
161
|
+
entry_channel = coerce_int(entry.get("channel")) or 1
|
|
162
|
+
if entry_channel == channel:
|
|
163
|
+
yield entry
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_algorithm_value(
|
|
167
|
+
camera_data: Mapping[str, Any], subtype: str, channel: int
|
|
168
|
+
) -> int | None:
|
|
169
|
+
"""Return AlgorithmInfo value for provided subtype/channel."""
|
|
170
|
+
|
|
171
|
+
for entry in iter_channel_algorithm_entries(camera_data, channel):
|
|
172
|
+
if entry.get("SubType") != subtype:
|
|
173
|
+
continue
|
|
174
|
+
return coerce_int(entry.get("Value"))
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def has_algorithm_subtype(
|
|
179
|
+
camera_data: Mapping[str, Any], subtype: str, channel: int = 1
|
|
180
|
+
) -> bool:
|
|
181
|
+
"""Return True when AlgorithmInfo contains subtype for channel."""
|
|
182
|
+
|
|
183
|
+
return get_algorithm_value(camera_data, subtype, channel) is not None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def support_ext_value(camera_data: Mapping[str, Any], ext_key: str) -> str | None:
|
|
187
|
+
"""Fetch a supportExt entry as a string when present."""
|
|
188
|
+
|
|
189
|
+
raw = camera_data.get("supportExt")
|
|
190
|
+
if not isinstance(raw, Mapping):
|
|
191
|
+
device_infos = camera_data.get("deviceInfos")
|
|
192
|
+
if isinstance(device_infos, Mapping):
|
|
193
|
+
raw = device_infos.get("supportExt")
|
|
194
|
+
|
|
195
|
+
if not isinstance(raw, Mapping):
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
value = raw.get(ext_key)
|
|
199
|
+
return str(value) if value is not None else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _normalize_port_list(value: Any) -> list[dict[str, Any]] | None:
|
|
203
|
+
"""Decode a list of port-security entries."""
|
|
204
|
+
|
|
205
|
+
value = decode_json(value)
|
|
206
|
+
if not isinstance(value, Iterable):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
normalized: list[dict[str, Any]] = []
|
|
210
|
+
for raw_entry in value:
|
|
211
|
+
entry = decode_json(raw_entry)
|
|
212
|
+
if not isinstance(entry, Mapping):
|
|
213
|
+
return None
|
|
214
|
+
port = coerce_int(entry.get("portNo"))
|
|
215
|
+
if port is None:
|
|
216
|
+
continue
|
|
217
|
+
normalized.append({"portNo": port, "enabled": bool(entry.get("enabled"))})
|
|
218
|
+
|
|
219
|
+
return normalized
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def normalize_port_security(payload: Any) -> dict[str, Any]:
|
|
223
|
+
"""Normalize IoT port-security payloads."""
|
|
224
|
+
|
|
225
|
+
seen: set[int] = set()
|
|
226
|
+
|
|
227
|
+
def _apply_hint(
|
|
228
|
+
candidate: dict[str, Any] | None, hint_value: bool | None
|
|
229
|
+
) -> dict[str, Any] | None:
|
|
230
|
+
if (
|
|
231
|
+
candidate is not None
|
|
232
|
+
and "enabled" not in candidate
|
|
233
|
+
and isinstance(hint_value, bool)
|
|
234
|
+
):
|
|
235
|
+
candidate["enabled"] = hint_value
|
|
236
|
+
return candidate
|
|
237
|
+
|
|
238
|
+
def _walk_mapping(obj: Mapping[str, Any], hint: bool | None) -> dict[str, Any] | None:
|
|
239
|
+
obj_id = id(obj)
|
|
240
|
+
if obj_id in seen:
|
|
241
|
+
return None
|
|
242
|
+
seen.add(obj_id)
|
|
243
|
+
|
|
244
|
+
enabled_local = obj.get("enabled")
|
|
245
|
+
if isinstance(enabled_local, bool):
|
|
246
|
+
hint = enabled_local
|
|
247
|
+
|
|
248
|
+
ports = _normalize_port_list(obj.get("portSecurityList"))
|
|
249
|
+
if ports is not None:
|
|
250
|
+
return {
|
|
251
|
+
"portSecurityList": ports,
|
|
252
|
+
"enabled": bool(enabled_local)
|
|
253
|
+
if isinstance(enabled_local, bool)
|
|
254
|
+
else bool(hint)
|
|
255
|
+
if isinstance(hint, bool)
|
|
256
|
+
else True,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for key in ("PortSecurity", "value", "data", "NetworkSecurityProtection"):
|
|
260
|
+
if key in obj:
|
|
261
|
+
candidate = _apply_hint(_walk(obj[key], hint), hint)
|
|
262
|
+
if candidate:
|
|
263
|
+
return candidate
|
|
264
|
+
|
|
265
|
+
for value in obj.values():
|
|
266
|
+
candidate = _apply_hint(_walk(value, hint), hint)
|
|
267
|
+
if candidate:
|
|
268
|
+
return candidate
|
|
269
|
+
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
def _walk_iterable(values: Iterable[Any], hint: bool | None) -> dict[str, Any] | None:
|
|
273
|
+
for item in values:
|
|
274
|
+
candidate = _walk(item, hint)
|
|
275
|
+
if candidate:
|
|
276
|
+
return candidate
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
def _walk(obj: Any, hint: bool | None = None) -> dict[str, Any] | None:
|
|
280
|
+
obj = decode_json(obj)
|
|
281
|
+
if obj is None:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
if isinstance(obj, Mapping):
|
|
285
|
+
return _walk_mapping(obj, hint)
|
|
286
|
+
|
|
287
|
+
if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
|
|
288
|
+
return _walk_iterable(obj, hint)
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
normalized = _walk(payload)
|
|
293
|
+
if isinstance(normalized, dict):
|
|
294
|
+
normalized.setdefault("enabled", True)
|
|
295
|
+
return normalized
|
|
296
|
+
return {}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def port_security_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
300
|
+
"""Return the normalized port-security mapping for a camera payload."""
|
|
301
|
+
|
|
302
|
+
direct = camera_data.get("NetworkSecurityProtection")
|
|
303
|
+
normalized = normalize_port_security(direct)
|
|
304
|
+
if normalized:
|
|
305
|
+
return normalized
|
|
306
|
+
|
|
307
|
+
feature = camera_data.get("FEATURE_INFO")
|
|
308
|
+
if isinstance(feature, Mapping):
|
|
309
|
+
normalized = normalize_port_security(feature)
|
|
310
|
+
if normalized:
|
|
311
|
+
return normalized
|
|
312
|
+
|
|
313
|
+
return {}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def port_security_has_port(camera_data: Mapping[str, Any], port: int) -> bool:
|
|
317
|
+
"""Return True if the normalized config contains the port."""
|
|
318
|
+
|
|
319
|
+
ports = port_security_config(camera_data).get("portSecurityList")
|
|
320
|
+
if not isinstance(ports, Iterable):
|
|
321
|
+
return False
|
|
322
|
+
return any(
|
|
323
|
+
isinstance(entry, Mapping) and coerce_int(entry.get("portNo")) == port
|
|
324
|
+
for entry in ports
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def port_security_port_enabled(camera_data: Mapping[str, Any], port: int) -> bool:
|
|
329
|
+
"""Return True if the specific port is enabled."""
|
|
330
|
+
|
|
331
|
+
ports = port_security_config(camera_data).get("portSecurityList")
|
|
332
|
+
if not isinstance(ports, Iterable):
|
|
333
|
+
return False
|
|
334
|
+
for entry in ports:
|
|
335
|
+
if isinstance(entry, Mapping) and coerce_int(entry.get("portNo")) == port:
|
|
336
|
+
return bool(entry.get("enabled"))
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def display_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
341
|
+
"""Return display mode value (1..3) from camera data."""
|
|
342
|
+
|
|
343
|
+
optionals = optionals_mapping(camera_data)
|
|
344
|
+
display_mode = optionals.get("display_mode")
|
|
345
|
+
display_mode = decode_json(display_mode)
|
|
346
|
+
|
|
347
|
+
mode = (
|
|
348
|
+
display_mode.get("mode") if isinstance(display_mode, Mapping) else display_mode
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if isinstance(mode, int) and mode in (1, 2, 3):
|
|
352
|
+
return mode
|
|
353
|
+
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def blc_current_value(camera_data: Mapping[str, Any]) -> int:
|
|
358
|
+
"""Return BLC position (0..5) from camera data. 0 = Off."""
|
|
359
|
+
optionals = optionals_mapping(camera_data)
|
|
360
|
+
inverse_mode = optionals.get("inverse_mode")
|
|
361
|
+
inverse_mode = decode_json(inverse_mode)
|
|
362
|
+
|
|
363
|
+
# Expected: {"mode": int, "enable": 0|1, "position": 0..5}
|
|
364
|
+
if isinstance(inverse_mode, Mapping):
|
|
365
|
+
enable = inverse_mode.get("enable", 0)
|
|
366
|
+
position = inverse_mode.get("position", 0)
|
|
367
|
+
if (
|
|
368
|
+
isinstance(enable, int)
|
|
369
|
+
and enable == 1
|
|
370
|
+
and isinstance(position, int)
|
|
371
|
+
and position in (1, 2, 3, 4, 5)
|
|
372
|
+
):
|
|
373
|
+
return position
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
# Fallbacks if backend ever returns a bare int (position) instead of the object
|
|
377
|
+
if isinstance(inverse_mode, int) and inverse_mode in (0, 1, 2, 3, 4, 5):
|
|
378
|
+
return inverse_mode
|
|
379
|
+
|
|
380
|
+
# Default to Off
|
|
381
|
+
return 0
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def device_icr_dss_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
385
|
+
"""Decode and return the device_ICR_DSS configuration."""
|
|
386
|
+
|
|
387
|
+
optionals = optionals_mapping(camera_data)
|
|
388
|
+
icr = decode_json(optionals.get("device_ICR_DSS"))
|
|
389
|
+
|
|
390
|
+
return dict(icr) if isinstance(icr, Mapping) else {}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def day_night_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
394
|
+
"""Return current day/night mode (0=auto,1=day,2=night)."""
|
|
395
|
+
|
|
396
|
+
config = device_icr_dss_config(camera_data)
|
|
397
|
+
mode = config.get("mode")
|
|
398
|
+
if isinstance(mode, int) and mode in (0, 1, 2):
|
|
399
|
+
return mode
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def day_night_sensitivity_value(camera_data: Mapping[str, Any]) -> int:
|
|
404
|
+
"""Return current day/night sensitivity value (1..3)."""
|
|
405
|
+
|
|
406
|
+
config = device_icr_dss_config(camera_data)
|
|
407
|
+
sensitivity = config.get("sensitivity")
|
|
408
|
+
if isinstance(sensitivity, int) and sensitivity in (1, 2, 3):
|
|
409
|
+
return sensitivity
|
|
410
|
+
return 2
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def resolve_channel(camera_data: Mapping[str, Any]) -> int:
|
|
414
|
+
"""Return the channel number to use for devconfig operations."""
|
|
415
|
+
|
|
416
|
+
candidate = camera_data.get("channelNo") or camera_data.get("channel_no")
|
|
417
|
+
if isinstance(candidate, int):
|
|
418
|
+
return candidate
|
|
419
|
+
if isinstance(candidate, str) and candidate.isdigit():
|
|
420
|
+
return int(candidate)
|
|
421
|
+
return 1
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def night_vision_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
425
|
+
"""Return decoded NightVision_Model configuration mapping."""
|
|
426
|
+
|
|
427
|
+
optionals = optionals_mapping(camera_data)
|
|
428
|
+
config: Any = optionals.get("NightVision_Model")
|
|
429
|
+
if config is None:
|
|
430
|
+
config = camera_data.get("NightVision_Model")
|
|
431
|
+
|
|
432
|
+
config = decode_json(config)
|
|
433
|
+
|
|
434
|
+
return dict(config) if isinstance(config, Mapping) else {}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def night_vision_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
438
|
+
"""Return current night vision mode (0=BW,1=colour,2=smart,5=super)."""
|
|
439
|
+
|
|
440
|
+
config = night_vision_config(camera_data)
|
|
441
|
+
mode = coerce_int(config.get("graphicType"))
|
|
442
|
+
if mode is None:
|
|
443
|
+
return 0
|
|
444
|
+
return mode if mode in (0, 1, 2, 5) else 0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def night_vision_luminance_value(camera_data: Mapping[str, Any]) -> int:
|
|
448
|
+
"""Return the configured night vision luminance (default 40)."""
|
|
449
|
+
|
|
450
|
+
config = night_vision_config(camera_data)
|
|
451
|
+
value = coerce_int(config.get("luminance"))
|
|
452
|
+
if value is None:
|
|
453
|
+
value = 40
|
|
454
|
+
return max(0, value)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def night_vision_duration_value(camera_data: Mapping[str, Any]) -> int:
|
|
458
|
+
"""Return the configured smart night vision duration (default 60)."""
|
|
459
|
+
|
|
460
|
+
config = night_vision_config(camera_data)
|
|
461
|
+
value = coerce_int(config.get("duration"))
|
|
462
|
+
return value if value is not None else 60
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def night_vision_payload(
|
|
466
|
+
camera_data: Mapping[str, Any],
|
|
467
|
+
*,
|
|
468
|
+
mode: int | None = None,
|
|
469
|
+
luminance: int | None = None,
|
|
470
|
+
duration: int | None = None,
|
|
471
|
+
) -> dict[str, Any]:
|
|
472
|
+
"""Return a sanitized NightVision_Model payload for updates."""
|
|
473
|
+
|
|
474
|
+
config = dict(night_vision_config(camera_data))
|
|
475
|
+
|
|
476
|
+
resolved_mode = (
|
|
477
|
+
int(mode)
|
|
478
|
+
if mode is not None
|
|
479
|
+
else int(config.get("graphicType") or night_vision_mode_value(camera_data))
|
|
480
|
+
)
|
|
481
|
+
config["graphicType"] = resolved_mode
|
|
482
|
+
|
|
483
|
+
if luminance is None:
|
|
484
|
+
luminance_value = night_vision_luminance_value(camera_data)
|
|
485
|
+
else:
|
|
486
|
+
coerced_luminance = coerce_int(luminance)
|
|
487
|
+
luminance_value = (
|
|
488
|
+
coerced_luminance
|
|
489
|
+
if coerced_luminance is not None
|
|
490
|
+
else night_vision_luminance_value(camera_data)
|
|
491
|
+
)
|
|
492
|
+
if resolved_mode == 1:
|
|
493
|
+
config["luminance"] = 0 if luminance_value <= 0 else max(20, luminance_value)
|
|
494
|
+
elif resolved_mode == 2:
|
|
495
|
+
config["luminance"] = max(
|
|
496
|
+
20,
|
|
497
|
+
luminance_value if luminance_value > 0 else 40,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
config["luminance"] = max(0, luminance_value)
|
|
501
|
+
|
|
502
|
+
if duration is None:
|
|
503
|
+
duration_value = night_vision_duration_value(camera_data)
|
|
504
|
+
else:
|
|
505
|
+
coerced_duration = coerce_int(duration)
|
|
506
|
+
duration_value = (
|
|
507
|
+
coerced_duration
|
|
508
|
+
if coerced_duration is not None
|
|
509
|
+
else night_vision_duration_value(camera_data)
|
|
510
|
+
)
|
|
511
|
+
if resolved_mode == 2:
|
|
512
|
+
config["duration"] = max(15, min(120, duration_value))
|
|
513
|
+
else:
|
|
514
|
+
config.pop("duration", None)
|
|
515
|
+
|
|
516
|
+
return config
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def has_osd_overlay(camera_data: Mapping[str, Any]) -> bool:
|
|
520
|
+
"""Return True when the camera has an active OSD label."""
|
|
521
|
+
|
|
522
|
+
optionals = optionals_mapping(camera_data)
|
|
523
|
+
osd_entries = optionals.get("OSD")
|
|
524
|
+
|
|
525
|
+
if isinstance(osd_entries, Mapping):
|
|
526
|
+
entries: list[Mapping[str, Any]] = [osd_entries]
|
|
527
|
+
elif isinstance(osd_entries, list):
|
|
528
|
+
entries = [entry for entry in osd_entries if isinstance(entry, Mapping)]
|
|
529
|
+
else:
|
|
530
|
+
return False
|
|
531
|
+
|
|
532
|
+
for entry in entries:
|
|
533
|
+
name = entry.get("name")
|
|
534
|
+
if isinstance(name, str) and name.strip():
|
|
535
|
+
return True
|
|
536
|
+
return False
|
pyezvizapi/light_bulb.py
CHANGED
|
@@ -176,7 +176,7 @@ class EzvizLightBulb:
|
|
|
176
176
|
def set_brightness(self, value: int) -> bool:
|
|
177
177
|
"""Set the light bulb brightness.
|
|
178
178
|
|
|
179
|
-
The value must be in range 1
|
|
179
|
+
The value must be in range 1-100. Returns True on success.
|
|
180
180
|
|
|
181
181
|
Raises:
|
|
182
182
|
PyEzvizError: On API failures.
|