pyezvizapi 1.0.3.1__py3-none-any.whl → 1.0.3.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 +30 -0
- pyezvizapi/client.py +131 -13
- pyezvizapi/feature.py +251 -0
- pyezvizapi/utils.py +26 -0
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/METADATA +1 -1
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/RECORD +11 -10
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.3.1.dist-info → pyezvizapi-1.0.3.3.dist-info}/top_level.txt +0 -0
pyezvizapi/__init__.py
CHANGED
|
@@ -34,6 +34,22 @@ from .exceptions import (
|
|
|
34
34
|
InvalidURL,
|
|
35
35
|
PyEzvizError,
|
|
36
36
|
)
|
|
37
|
+
from .feature import (
|
|
38
|
+
day_night_mode_value,
|
|
39
|
+
day_night_sensitivity_value,
|
|
40
|
+
device_icr_dss_config,
|
|
41
|
+
display_mode_value,
|
|
42
|
+
has_osd_overlay,
|
|
43
|
+
lens_defog_config,
|
|
44
|
+
lens_defog_value,
|
|
45
|
+
night_vision_config,
|
|
46
|
+
night_vision_duration_value,
|
|
47
|
+
night_vision_luminance_value,
|
|
48
|
+
night_vision_mode_value,
|
|
49
|
+
night_vision_payload,
|
|
50
|
+
optionals_mapping,
|
|
51
|
+
resolve_channel,
|
|
52
|
+
)
|
|
37
53
|
from .light_bulb import EzvizLightBulb
|
|
38
54
|
from .models import EzvizDeviceRecord, build_device_records_map
|
|
39
55
|
from .mqtt import EzvizToken, MQTTClient, MqttData, ServiceUrls
|
|
@@ -71,4 +87,18 @@ __all__ = [
|
|
|
71
87
|
"SupportExt",
|
|
72
88
|
"TestRTSPAuth",
|
|
73
89
|
"build_device_records_map",
|
|
90
|
+
"day_night_mode_value",
|
|
91
|
+
"day_night_sensitivity_value",
|
|
92
|
+
"device_icr_dss_config",
|
|
93
|
+
"display_mode_value",
|
|
94
|
+
"has_osd_overlay",
|
|
95
|
+
"lens_defog_config",
|
|
96
|
+
"lens_defog_value",
|
|
97
|
+
"night_vision_config",
|
|
98
|
+
"night_vision_duration_value",
|
|
99
|
+
"night_vision_luminance_value",
|
|
100
|
+
"night_vision_mode_value",
|
|
101
|
+
"night_vision_payload",
|
|
102
|
+
"optionals_mapping",
|
|
103
|
+
"resolve_channel",
|
|
74
104
|
]
|
pyezvizapi/client.py
CHANGED
|
@@ -126,6 +126,7 @@ from .exceptions import (
|
|
|
126
126
|
InvalidURL,
|
|
127
127
|
PyEzvizError,
|
|
128
128
|
)
|
|
129
|
+
from .feature import optionals_mapping
|
|
129
130
|
from .light_bulb import EzvizLightBulb
|
|
130
131
|
from .models import EzvizDeviceRecord, build_device_records_map
|
|
131
132
|
from .mqtt import MQTTClient
|
|
@@ -1695,6 +1696,47 @@ class EzvizClient:
|
|
|
1695
1696
|
error_message="Could not set IoT feature value",
|
|
1696
1697
|
)
|
|
1697
1698
|
|
|
1699
|
+
def set_lens_defog_mode(
|
|
1700
|
+
self,
|
|
1701
|
+
serial: str,
|
|
1702
|
+
value: int,
|
|
1703
|
+
*,
|
|
1704
|
+
local_index: str = "1",
|
|
1705
|
+
max_retries: int = 0,
|
|
1706
|
+
) -> tuple[bool, str]:
|
|
1707
|
+
"""Update the lens defog configuration using canonical option index.
|
|
1708
|
+
|
|
1709
|
+
Args:
|
|
1710
|
+
serial: Device serial number.
|
|
1711
|
+
value: Select option index (0=auto, 1=on, 2=off).
|
|
1712
|
+
local_index: Channel index for multi-channel devices.
|
|
1713
|
+
max_retries: Number of retries for transient failures.
|
|
1714
|
+
|
|
1715
|
+
Returns:
|
|
1716
|
+
A tuple of (enabled flag, defog mode string) reflecting the
|
|
1717
|
+
configuration that was sent to the device.
|
|
1718
|
+
"""
|
|
1719
|
+
|
|
1720
|
+
if value == 1:
|
|
1721
|
+
enabled, mode = True, "open"
|
|
1722
|
+
elif value == 2:
|
|
1723
|
+
enabled, mode = False, "auto"
|
|
1724
|
+
else:
|
|
1725
|
+
enabled, mode = True, "auto"
|
|
1726
|
+
|
|
1727
|
+
payload = {"value": {"enabled": enabled, "defogMode": mode}}
|
|
1728
|
+
self.set_iot_feature(
|
|
1729
|
+
serial,
|
|
1730
|
+
resource_identifier="Video",
|
|
1731
|
+
local_index=local_index,
|
|
1732
|
+
domain_id="LensCleaning",
|
|
1733
|
+
action_id="DefogCfg",
|
|
1734
|
+
value=payload,
|
|
1735
|
+
max_retries=max_retries,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
return enabled, mode
|
|
1739
|
+
|
|
1698
1740
|
def update_device_name(
|
|
1699
1741
|
self,
|
|
1700
1742
|
serial: str,
|
|
@@ -2936,33 +2978,109 @@ class EzvizClient:
|
|
|
2936
2978
|
|
|
2937
2979
|
return True
|
|
2938
2980
|
|
|
2981
|
+
def _resolve_osd_text(
|
|
2982
|
+
self,
|
|
2983
|
+
serial: str,
|
|
2984
|
+
*,
|
|
2985
|
+
name: str | None = None,
|
|
2986
|
+
camera_data: Mapping[str, Any] | None = None,
|
|
2987
|
+
) -> str:
|
|
2988
|
+
"""Return the preferred OSD label for a camera."""
|
|
2989
|
+
|
|
2990
|
+
if isinstance(name, str) and name.strip():
|
|
2991
|
+
return name.strip()
|
|
2992
|
+
|
|
2993
|
+
candidates: list[Mapping[str, Any]] = []
|
|
2994
|
+
|
|
2995
|
+
if isinstance(camera_data, Mapping):
|
|
2996
|
+
candidates.append(camera_data)
|
|
2997
|
+
|
|
2998
|
+
cached = self._cameras.get(serial)
|
|
2999
|
+
if isinstance(cached, Mapping):
|
|
3000
|
+
candidates.append(cached)
|
|
3001
|
+
|
|
3002
|
+
for data in candidates:
|
|
3003
|
+
direct = data.get("name")
|
|
3004
|
+
if isinstance(direct, str) and direct.strip():
|
|
3005
|
+
return direct.strip()
|
|
3006
|
+
|
|
3007
|
+
device_info = data.get("deviceInfos")
|
|
3008
|
+
if isinstance(device_info, Mapping):
|
|
3009
|
+
alt = device_info.get("name")
|
|
3010
|
+
if isinstance(alt, str) and alt.strip():
|
|
3011
|
+
return alt.strip()
|
|
3012
|
+
|
|
3013
|
+
optionals = optionals_mapping(data)
|
|
3014
|
+
osd_entries = optionals.get("OSD")
|
|
3015
|
+
if isinstance(osd_entries, Mapping):
|
|
3016
|
+
osd_entries = [osd_entries]
|
|
3017
|
+
if isinstance(osd_entries, list):
|
|
3018
|
+
for entry in osd_entries:
|
|
3019
|
+
if not isinstance(entry, Mapping):
|
|
3020
|
+
continue
|
|
3021
|
+
text = entry.get("name")
|
|
3022
|
+
if isinstance(text, str) and text.strip():
|
|
3023
|
+
return text.strip()
|
|
3024
|
+
|
|
3025
|
+
return serial
|
|
3026
|
+
|
|
2939
3027
|
def set_camera_osd(
|
|
2940
3028
|
self,
|
|
2941
3029
|
serial: str,
|
|
2942
|
-
text: str =
|
|
3030
|
+
text: str | None = None,
|
|
3031
|
+
*,
|
|
3032
|
+
enabled: bool | None = None,
|
|
3033
|
+
name: str | None = None,
|
|
3034
|
+
camera_data: Mapping[str, Any] | None = None,
|
|
2943
3035
|
channel: int = 1,
|
|
2944
3036
|
max_retries: int = 0,
|
|
2945
3037
|
) -> bool:
|
|
2946
|
-
"""Set
|
|
3038
|
+
"""Set or clear the on-screen display text for a camera.
|
|
2947
3039
|
|
|
2948
3040
|
Args:
|
|
2949
|
-
serial
|
|
2950
|
-
text
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3041
|
+
serial: Camera serial number that should receive the update.
|
|
3042
|
+
text: Explicit OSD label to apply. If provided it takes precedence over
|
|
3043
|
+
all other inputs and `enabled` is ignored.
|
|
3044
|
+
enabled: Convenience flag used when `text` is omitted. When set to
|
|
3045
|
+
`True`, the client derives a label automatically (optionally using
|
|
3046
|
+
`name`/`camera_data`). When `False`, the overlay is cleared.
|
|
3047
|
+
name: Optional friendly name to favour when building the automatic
|
|
3048
|
+
overlay text.
|
|
3049
|
+
camera_data: Optional camera payload (matching coordinator data) that
|
|
3050
|
+
can be inspected for existing OSD labels and names.
|
|
3051
|
+
channel: Camera channel identifier (defaults to the primary channel).
|
|
3052
|
+
max_retries: Number of retry attempts for transient API failures.
|
|
2957
3053
|
|
|
2958
3054
|
Returns:
|
|
2959
|
-
bool: True
|
|
2960
|
-
|
|
3055
|
+
bool: ``True`` when the request is accepted by the Ezviz backend.
|
|
2961
3056
|
"""
|
|
3057
|
+
|
|
3058
|
+
if text is not None:
|
|
3059
|
+
resolved = text
|
|
3060
|
+
elif enabled is False:
|
|
3061
|
+
resolved = ""
|
|
3062
|
+
else:
|
|
3063
|
+
if camera_data is None:
|
|
3064
|
+
camera_data = self._cameras.get(serial)
|
|
3065
|
+
if camera_data is None:
|
|
3066
|
+
raise PyEzvizError(
|
|
3067
|
+
"Camera data unavailable; call load_devices() before setting the OSD"
|
|
3068
|
+
)
|
|
3069
|
+
|
|
3070
|
+
resolved = (
|
|
3071
|
+
self._resolve_osd_text(
|
|
3072
|
+
serial,
|
|
3073
|
+
name=name,
|
|
3074
|
+
camera_data=camera_data,
|
|
3075
|
+
)
|
|
3076
|
+
if enabled
|
|
3077
|
+
else ""
|
|
3078
|
+
)
|
|
3079
|
+
|
|
2962
3080
|
json_output = self._request_json(
|
|
2963
3081
|
"PUT",
|
|
2964
3082
|
f"{API_ENDPOINT_OSD}{serial}/{channel}/osd",
|
|
2965
|
-
data={"osd":
|
|
3083
|
+
data={"osd": resolved},
|
|
2966
3084
|
retry_401=True,
|
|
2967
3085
|
max_retries=max_retries,
|
|
2968
3086
|
)
|
pyezvizapi/feature.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Helpers for working with Ezviz feature metadata payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, MutableMapping
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from .utils import coerce_int, decode_json
|
|
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
|
+
feature = camera_data.get("FEATURE_INFO")
|
|
15
|
+
if not isinstance(feature, Mapping):
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
for group in feature.values():
|
|
19
|
+
if isinstance(group, Mapping):
|
|
20
|
+
video = group.get("Video")
|
|
21
|
+
if isinstance(video, MutableMapping):
|
|
22
|
+
return cast(dict[str, Any], video)
|
|
23
|
+
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def lens_defog_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
28
|
+
"""Return the LensCleaning defog configuration if present."""
|
|
29
|
+
|
|
30
|
+
video = _feature_video_section(camera_data)
|
|
31
|
+
lens = video.get("LensCleaning") if isinstance(video, Mapping) else None
|
|
32
|
+
if not isinstance(lens, MutableMapping):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
config = lens.get("DefogCfg")
|
|
36
|
+
if isinstance(config, MutableMapping):
|
|
37
|
+
return cast(dict[str, Any], config)
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def lens_defog_value(camera_data: Mapping[str, Any]) -> int:
|
|
42
|
+
"""Return canonical defogging mode (0=auto,1=on,2=off)."""
|
|
43
|
+
|
|
44
|
+
cfg = lens_defog_config(camera_data)
|
|
45
|
+
if not cfg:
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
enabled = bool(cfg.get("enabled"))
|
|
49
|
+
mode = str(cfg.get("defogMode") or "").lower()
|
|
50
|
+
|
|
51
|
+
if not enabled:
|
|
52
|
+
return 2
|
|
53
|
+
|
|
54
|
+
if mode == "open":
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def optionals_mapping(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
61
|
+
"""Return decoded optionals mapping from the camera payload."""
|
|
62
|
+
|
|
63
|
+
status_info = camera_data.get("statusInfo")
|
|
64
|
+
optionals: Any = None
|
|
65
|
+
if isinstance(status_info, Mapping):
|
|
66
|
+
optionals = status_info.get("optionals")
|
|
67
|
+
|
|
68
|
+
optionals = decode_json(optionals)
|
|
69
|
+
|
|
70
|
+
if not isinstance(optionals, Mapping):
|
|
71
|
+
optionals = decode_json(camera_data.get("optionals"))
|
|
72
|
+
|
|
73
|
+
if not isinstance(optionals, Mapping):
|
|
74
|
+
status = camera_data.get("STATUS")
|
|
75
|
+
if isinstance(status, Mapping):
|
|
76
|
+
optionals = decode_json(status.get("optionals"))
|
|
77
|
+
|
|
78
|
+
return dict(optionals) if isinstance(optionals, Mapping) else {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def display_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
82
|
+
"""Return display mode value (1..3) from camera data."""
|
|
83
|
+
|
|
84
|
+
optionals = optionals_mapping(camera_data)
|
|
85
|
+
display_mode = optionals.get("display_mode")
|
|
86
|
+
display_mode = decode_json(display_mode)
|
|
87
|
+
|
|
88
|
+
if isinstance(display_mode, Mapping):
|
|
89
|
+
mode = display_mode.get("mode")
|
|
90
|
+
else:
|
|
91
|
+
mode = display_mode
|
|
92
|
+
|
|
93
|
+
if isinstance(mode, int) and mode in (1, 2, 3):
|
|
94
|
+
return mode
|
|
95
|
+
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def device_icr_dss_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""Decode and return the device_ICR_DSS configuration."""
|
|
101
|
+
|
|
102
|
+
optionals = optionals_mapping(camera_data)
|
|
103
|
+
icr = decode_json(optionals.get("device_ICR_DSS"))
|
|
104
|
+
|
|
105
|
+
return dict(icr) if isinstance(icr, Mapping) else {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def day_night_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
109
|
+
"""Return current day/night mode (0=auto,1=day,2=night)."""
|
|
110
|
+
|
|
111
|
+
config = device_icr_dss_config(camera_data)
|
|
112
|
+
mode = config.get("mode")
|
|
113
|
+
if isinstance(mode, int) and mode in (0, 1, 2):
|
|
114
|
+
return mode
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def day_night_sensitivity_value(camera_data: Mapping[str, Any]) -> int:
|
|
119
|
+
"""Return current day/night sensitivity value (1..3)."""
|
|
120
|
+
|
|
121
|
+
config = device_icr_dss_config(camera_data)
|
|
122
|
+
sensitivity = config.get("sensitivity")
|
|
123
|
+
if isinstance(sensitivity, int) and sensitivity in (1, 2, 3):
|
|
124
|
+
return sensitivity
|
|
125
|
+
return 2
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve_channel(camera_data: Mapping[str, Any]) -> int:
|
|
129
|
+
"""Return the channel number to use for devconfig operations."""
|
|
130
|
+
|
|
131
|
+
candidate = camera_data.get("channelNo") or camera_data.get("channel_no")
|
|
132
|
+
if isinstance(candidate, int):
|
|
133
|
+
return candidate
|
|
134
|
+
if isinstance(candidate, str) and candidate.isdigit():
|
|
135
|
+
return int(candidate)
|
|
136
|
+
return 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def night_vision_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
140
|
+
"""Return decoded NightVision_Model configuration mapping."""
|
|
141
|
+
|
|
142
|
+
optionals = optionals_mapping(camera_data)
|
|
143
|
+
config: Any = optionals.get("NightVision_Model")
|
|
144
|
+
if config is None:
|
|
145
|
+
config = camera_data.get("NightVision_Model")
|
|
146
|
+
|
|
147
|
+
config = decode_json(config)
|
|
148
|
+
|
|
149
|
+
return dict(config) if isinstance(config, Mapping) else {}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def night_vision_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
153
|
+
"""Return current night vision mode (0=BW,1=colour,2=smart,5=super)."""
|
|
154
|
+
|
|
155
|
+
config = night_vision_config(camera_data)
|
|
156
|
+
mode = coerce_int(config.get("graphicType"))
|
|
157
|
+
if mode is None:
|
|
158
|
+
return 0
|
|
159
|
+
return mode if mode in (0, 1, 2, 5) else 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def night_vision_luminance_value(camera_data: Mapping[str, Any]) -> int:
|
|
163
|
+
"""Return the configured night vision luminance (default 40)."""
|
|
164
|
+
|
|
165
|
+
config = night_vision_config(camera_data)
|
|
166
|
+
value = coerce_int(config.get("luminance"))
|
|
167
|
+
if value is None:
|
|
168
|
+
value = 40
|
|
169
|
+
return max(0, value)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def night_vision_duration_value(camera_data: Mapping[str, Any]) -> int:
|
|
173
|
+
"""Return the configured smart night vision duration (default 60)."""
|
|
174
|
+
|
|
175
|
+
config = night_vision_config(camera_data)
|
|
176
|
+
value = coerce_int(config.get("duration"))
|
|
177
|
+
return value if value is not None else 60
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def night_vision_payload(
|
|
181
|
+
camera_data: Mapping[str, Any],
|
|
182
|
+
*,
|
|
183
|
+
mode: int | None = None,
|
|
184
|
+
luminance: int | None = None,
|
|
185
|
+
duration: int | None = None,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
"""Return a sanitized NightVision_Model payload for updates."""
|
|
188
|
+
|
|
189
|
+
config = dict(night_vision_config(camera_data))
|
|
190
|
+
|
|
191
|
+
resolved_mode = (
|
|
192
|
+
int(mode)
|
|
193
|
+
if mode is not None
|
|
194
|
+
else int(config.get("graphicType") or night_vision_mode_value(camera_data))
|
|
195
|
+
)
|
|
196
|
+
config["graphicType"] = resolved_mode
|
|
197
|
+
|
|
198
|
+
if luminance is None:
|
|
199
|
+
luminance_value = night_vision_luminance_value(camera_data)
|
|
200
|
+
else:
|
|
201
|
+
coerced_luminance = coerce_int(luminance)
|
|
202
|
+
luminance_value = (
|
|
203
|
+
coerced_luminance
|
|
204
|
+
if coerced_luminance is not None
|
|
205
|
+
else night_vision_luminance_value(camera_data)
|
|
206
|
+
)
|
|
207
|
+
if resolved_mode == 1:
|
|
208
|
+
config["luminance"] = 0 if luminance_value <= 0 else max(20, luminance_value)
|
|
209
|
+
elif resolved_mode == 2:
|
|
210
|
+
config["luminance"] = max(
|
|
211
|
+
20,
|
|
212
|
+
luminance_value if luminance_value > 0 else 40,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
config["luminance"] = max(0, luminance_value)
|
|
216
|
+
|
|
217
|
+
if duration is None:
|
|
218
|
+
duration_value = night_vision_duration_value(camera_data)
|
|
219
|
+
else:
|
|
220
|
+
coerced_duration = coerce_int(duration)
|
|
221
|
+
duration_value = (
|
|
222
|
+
coerced_duration
|
|
223
|
+
if coerced_duration is not None
|
|
224
|
+
else night_vision_duration_value(camera_data)
|
|
225
|
+
)
|
|
226
|
+
if resolved_mode == 2:
|
|
227
|
+
config["duration"] = max(15, min(120, duration_value))
|
|
228
|
+
else:
|
|
229
|
+
config.pop("duration", None)
|
|
230
|
+
|
|
231
|
+
return config
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def has_osd_overlay(camera_data: Mapping[str, Any]) -> bool:
|
|
235
|
+
"""Return True when the camera has an active OSD label."""
|
|
236
|
+
|
|
237
|
+
optionals = optionals_mapping(camera_data)
|
|
238
|
+
osd_entries = optionals.get("OSD")
|
|
239
|
+
|
|
240
|
+
if isinstance(osd_entries, Mapping):
|
|
241
|
+
entries: list[Mapping[str, Any]] = [osd_entries]
|
|
242
|
+
elif isinstance(osd_entries, list):
|
|
243
|
+
entries = [entry for entry in osd_entries if isinstance(entry, Mapping)]
|
|
244
|
+
else:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
for entry in entries:
|
|
248
|
+
name = entry.get("name")
|
|
249
|
+
if isinstance(name, str) and name.strip():
|
|
250
|
+
return True
|
|
251
|
+
return False
|
pyezvizapi/utils.py
CHANGED
|
@@ -18,6 +18,32 @@ from .exceptions import PyEzvizError
|
|
|
18
18
|
_LOGGER = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def coerce_int(value: Any) -> int | None:
|
|
22
|
+
"""Best-effort coercion to int for mixed payloads."""
|
|
23
|
+
|
|
24
|
+
if isinstance(value, bool):
|
|
25
|
+
return int(value)
|
|
26
|
+
|
|
27
|
+
if isinstance(value, (int, float)):
|
|
28
|
+
return int(value)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
return int(str(value))
|
|
32
|
+
except (TypeError, ValueError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decode_json(value: Any) -> Any:
|
|
37
|
+
"""Decode a JSON string when possible, otherwise return the original value."""
|
|
38
|
+
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(value)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return None
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
21
47
|
def convert_to_dict(data: Any) -> Any:
|
|
22
48
|
"""Recursively convert a string representation of a dictionary to a dictionary."""
|
|
23
49
|
if isinstance(data, dict):
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
pyezvizapi/__init__.py,sha256=
|
|
1
|
+
pyezvizapi/__init__.py,sha256=mp3XbdXy00TyittfvvGrQ2oAaDeoebLE6_QU0Sbuf8o,2668
|
|
2
2
|
pyezvizapi/__main__.py,sha256=9uttTuOfO22tzyomJIV8ebFJ-G-YUNDYOadZ_0AgdNA,20925
|
|
3
3
|
pyezvizapi/api_endpoints.py,sha256=2M5Vs4YB1VWZGcowT-4Fj2hhRNjFh976LT3jtRrqvrc,5754
|
|
4
4
|
pyezvizapi/camera.py,sha256=Pl5oIEdrFcv1Hz5sQI1IyyJIDCMjOjQdtExgKzmLoK8,22102
|
|
5
5
|
pyezvizapi/cas.py,sha256=3zHe-_a0KchCmGeAj1of-pV6oMPRUmSCIiDqBFsTK8A,6025
|
|
6
|
-
pyezvizapi/client.py,sha256=
|
|
6
|
+
pyezvizapi/client.py,sha256=rQ95oGgx16M69BWA8WTmmpyX9d4kHdxt6biVMrjDmOQ,140990
|
|
7
7
|
pyezvizapi/constants.py,sha256=5AxJYfof6NvebBcFvPkoKI6xinpkwmCnaauUvhvBMDY,12810
|
|
8
8
|
pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
|
|
9
|
+
pyezvizapi/feature.py,sha256=Sc-L-2qqz7GXmaWf0_j6QF84KRSw_emy1cWKhcs5HzE,7654
|
|
9
10
|
pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
|
|
10
11
|
pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
|
|
11
12
|
pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
|
|
12
13
|
pyezvizapi/test_cam_rtsp.py,sha256=O9NHh-vcNFfnzNw8jbuhM9a_5TWfNZIMXaJP7Lmkaj4,5162
|
|
13
14
|
pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
|
|
14
|
-
pyezvizapi/utils.py,sha256=
|
|
15
|
-
pyezvizapi-1.0.3.
|
|
16
|
-
pyezvizapi-1.0.3.
|
|
17
|
-
pyezvizapi-1.0.3.
|
|
18
|
-
pyezvizapi-1.0.3.
|
|
19
|
-
pyezvizapi-1.0.3.
|
|
20
|
-
pyezvizapi-1.0.3.
|
|
21
|
-
pyezvizapi-1.0.3.
|
|
15
|
+
pyezvizapi/utils.py,sha256=ozGncEyaIJJ8VYw8f-xfM2OBmqR8eNYLq728FFvbvr8,12757
|
|
16
|
+
pyezvizapi-1.0.3.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
+
pyezvizapi-1.0.3.3.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
+
pyezvizapi-1.0.3.3.dist-info/METADATA,sha256=zSGxukJtv5F88ejmK6fPoG8kPwQXO-y9t-xseLDVMQU,695
|
|
19
|
+
pyezvizapi-1.0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
pyezvizapi-1.0.3.3.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
21
|
+
pyezvizapi-1.0.3.3.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
22
|
+
pyezvizapi-1.0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|