pyezvizapi 1.0.3.1__tar.gz → 1.0.3.3__tar.gz

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.

Files changed (28) hide show
  1. {pyezvizapi-1.0.3.1/pyezvizapi.egg-info → pyezvizapi-1.0.3.3}/PKG-INFO +1 -1
  2. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/__init__.py +30 -0
  3. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/client.py +131 -13
  4. pyezvizapi-1.0.3.3/pyezvizapi/feature.py +251 -0
  5. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/utils.py +26 -0
  6. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3/pyezvizapi.egg-info}/PKG-INFO +1 -1
  7. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi.egg-info/SOURCES.txt +1 -0
  8. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/setup.py +1 -1
  9. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/LICENSE +0 -0
  10. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/LICENSE.md +0 -0
  11. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/MANIFEST.in +0 -0
  12. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/README.md +0 -0
  13. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/__main__.py +0 -0
  14. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/api_endpoints.py +0 -0
  15. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/camera.py +0 -0
  16. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/cas.py +0 -0
  17. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/constants.py +0 -0
  18. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/exceptions.py +0 -0
  19. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/light_bulb.py +0 -0
  20. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/models.py +0 -0
  21. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/mqtt.py +0 -0
  22. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/test_cam_rtsp.py +0 -0
  23. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi/test_mqtt.py +0 -0
  24. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi.egg-info/dependency_links.txt +0 -0
  25. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi.egg-info/entry_points.txt +0 -0
  26. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi.egg-info/requires.txt +0 -0
  27. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/pyezvizapi.egg-info/top_level.txt +0 -0
  28. {pyezvizapi-1.0.3.1 → pyezvizapi-1.0.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.3.1
3
+ Version: 1.0.3.3
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
@@ -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
  ]
@@ -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 OSD (on screen display) text.
3038
+ """Set or clear the on-screen display text for a camera.
2947
3039
 
2948
3040
  Args:
2949
- serial (str): The camera serial.
2950
- text (str, optional): The osd text to set. The default of "" will clear.
2951
- channel (int, optional): The cammera channel to set this on. Defaults to 1.
2952
- max_retries (int, optional): Number of retries attempted. Defaults to 0.
2953
-
2954
- Raises:
2955
- PyEzvizError: If max retries are exceeded or if the response indicates failure.
2956
- HTTPError: If an HTTP error occurs (other than a 401, which triggers re-login).
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 if the operation was successful.
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": text},
3083
+ data={"osd": resolved},
2966
3084
  retry_401=True,
2967
3085
  max_retries=max_retries,
2968
3086
  )
@@ -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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.3.1
3
+ Version: 1.0.3.3
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
@@ -11,6 +11,7 @@ pyezvizapi/cas.py
11
11
  pyezvizapi/client.py
12
12
  pyezvizapi/constants.py
13
13
  pyezvizapi/exceptions.py
14
+ pyezvizapi/feature.py
14
15
  pyezvizapi/light_bulb.py
15
16
  pyezvizapi/models.py
16
17
  pyezvizapi/mqtt.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name='pyezvizapi',
8
- version="1.0.3.1",
8
+ version="1.0.3.3",
9
9
  license='Apache Software License 2.0',
10
10
  author='Renier Moorcroft',
11
11
  author_email='RenierM26@users.github.com',
File without changes
File without changes
File without changes
File without changes
File without changes