pyezvizapi 1.0.3.5__tar.gz → 1.0.3.7__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.
Files changed (29) hide show
  1. {pyezvizapi-1.0.3.5/pyezvizapi.egg-info → pyezvizapi-1.0.3.7}/PKG-INFO +1 -1
  2. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/__init__.py +6 -0
  3. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/__main__.py +2 -2
  4. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/client.py +40 -0
  5. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/constants.py +1 -0
  6. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/feature.py +47 -9
  7. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/mqtt.py +1 -1
  8. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/test_mqtt.py +1 -1
  9. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/utils.py +52 -11
  10. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7/pyezvizapi.egg-info}/PKG-INFO +1 -1
  11. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi.egg-info/SOURCES.txt +1 -0
  12. pyezvizapi-1.0.3.7/pyproject.toml +45 -0
  13. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/setup.py +3 -3
  14. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/LICENSE +0 -0
  15. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/LICENSE.md +0 -0
  16. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/MANIFEST.in +0 -0
  17. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/README.md +0 -0
  18. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/api_endpoints.py +0 -0
  19. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/camera.py +0 -0
  20. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/cas.py +0 -0
  21. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/exceptions.py +0 -0
  22. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/light_bulb.py +0 -0
  23. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/models.py +0 -0
  24. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi/test_cam_rtsp.py +0 -0
  25. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi.egg-info/dependency_links.txt +0 -0
  26. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi.egg-info/entry_points.txt +0 -0
  27. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi.egg-info/requires.txt +0 -0
  28. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/pyezvizapi.egg-info/top_level.txt +0 -0
  29. {pyezvizapi-1.0.3.5 → pyezvizapi-1.0.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.3.5
3
+ Version: 1.0.3.7
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
@@ -57,6 +57,9 @@ from .feature import (
57
57
  port_security_has_port,
58
58
  port_security_port_enabled,
59
59
  resolve_channel,
60
+ supplement_light_available,
61
+ supplement_light_enabled,
62
+ supplement_light_params,
60
63
  support_ext_value,
61
64
  )
62
65
  from .light_bulb import EzvizLightBulb
@@ -118,5 +121,8 @@ __all__ = [
118
121
  "port_security_has_port",
119
122
  "port_security_port_enabled",
120
123
  "resolve_channel",
124
+ "supplement_light_available",
125
+ "supplement_light_enabled",
126
+ "supplement_light_params",
121
127
  "support_ext_value",
122
128
  ]
@@ -421,7 +421,7 @@ def _handle_devices_light(args: argparse.Namespace, client: EzvizClient) -> int:
421
421
 
422
422
  def _handle_pagelist(client: EzvizClient) -> int:
423
423
  """Output full pagelist (raw JSON) for exploration in editors like Notepad++."""
424
- data = client._get_page_list() # noqa: SLF001
424
+ data = client.get_page_list()
425
425
  _write_json(data)
426
426
  return 0
427
427
 
@@ -611,7 +611,7 @@ def main(argv: list[str] | None = None) -> int:
611
611
  return 2
612
612
  finally:
613
613
  if args.save_token and args.token_file:
614
- _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
614
+ _save_token_file(args.token_file, client.export_token())
615
615
  client.close_session()
616
616
 
617
617
 
@@ -1615,6 +1615,35 @@ class EzvizClient:
1615
1615
  error_message="Could not fetch device feature value",
1616
1616
  )
1617
1617
 
1618
+ def set_intelligent_fill_light(
1619
+ self,
1620
+ serial: str,
1621
+ *,
1622
+ enabled: bool,
1623
+ local_index: str = "1",
1624
+ max_retries: int = 0,
1625
+ ) -> dict:
1626
+ """Toggle the intelligent fill light mode via the IoT feature API."""
1627
+
1628
+ payload = {
1629
+ "value": {
1630
+ "enabled": bool(enabled),
1631
+ "supplementLightSwitchMode": "eventIntelligence"
1632
+ if enabled
1633
+ else "irLight",
1634
+ }
1635
+ }
1636
+ body = self._normalize_json_payload(payload)
1637
+ return self.set_iot_feature(
1638
+ serial,
1639
+ resource_identifier="Video",
1640
+ local_index=local_index,
1641
+ domain_id="SupplementLightMgr",
1642
+ action_id="ImageSupplementLightModeSwitchParams",
1643
+ value=body,
1644
+ max_retries=max_retries,
1645
+ )
1646
+
1618
1647
  def set_image_flip_iot(
1619
1648
  self,
1620
1649
  serial: str,
@@ -2074,6 +2103,7 @@ class EzvizClient:
2074
2103
  # Create camera object
2075
2104
  cam = EzvizCamera(self, device, dict(rec.raw))
2076
2105
  self._cameras[device] = cam.status(refresh=refresh)
2106
+
2077
2107
  except (
2078
2108
  PyEzvizError,
2079
2109
  KeyError,
@@ -4368,6 +4398,16 @@ class EzvizClient:
4368
4398
  json_key=None,
4369
4399
  )
4370
4400
 
4401
+ def get_page_list(self) -> Any:
4402
+ """Return the full pagelist payload without filtering."""
4403
+
4404
+ return self._get_page_list()
4405
+
4406
+ def export_token(self) -> dict[str, Any]:
4407
+ """Return a shallow copy of the current authentication token."""
4408
+
4409
+ return dict(self._token)
4410
+
4371
4411
  def get_device(self) -> Any:
4372
4412
  """Get ezviz devices filter."""
4373
4413
  return self._api_get_pagelist(page_filter="CLOUD", json_key="deviceInfos")
@@ -13,6 +13,7 @@ FEATURE_CODE = generate_unique_code()
13
13
  XOR_KEY = b"\x0c\x0eJ^X\x15@Rr"
14
14
  DEFAULT_TIMEOUT = 25
15
15
  MAX_RETRIES = 3
16
+ HIK_ENCRYPTION_HEADER = b"hikencodepicture"
16
17
  REQUEST_HEADER = {
17
18
  "featureCode": FEATURE_CODE,
18
19
  "clientType": "3",
@@ -5,23 +5,61 @@ from __future__ import annotations
5
5
  from collections.abc import Iterable, Iterator, Mapping, MutableMapping
6
6
  from typing import Any, cast
7
7
 
8
- from .utils import coerce_int, decode_json
8
+ from .utils import WILDCARD_STEP, coerce_int, decode_json, first_nested
9
9
 
10
10
 
11
11
  def _feature_video_section(camera_data: Mapping[str, Any]) -> dict[str, Any]:
12
12
  """Return the nested Video feature section from feature info payload."""
13
13
 
14
- feature = camera_data.get("FEATURE_INFO")
15
- if not isinstance(feature, Mapping):
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:
16
28
  return {}
17
29
 
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)
30
+ manager: Any = video.get("SupplementLightMgr")
31
+ manager = decode_json(manager)
32
+ if not isinstance(manager, Mapping):
33
+ return {}
23
34
 
24
- return {}
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))
25
63
 
26
64
 
27
65
  def lens_defog_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
@@ -247,7 +247,7 @@ class MQTTClient:
247
247
  # Stop background thread and disconnect
248
248
  self.mqtt_client.loop_stop()
249
249
  self.mqtt_client.disconnect()
250
- except Exception as err: # noqa: BLE001
250
+ except (OSError, ValueError, RuntimeError) as err:
251
251
  _LOGGER.debug("MQTT disconnect failed: %s", err)
252
252
  # Always attempt to stop push on server side
253
253
  self._stop_ezviz_push()
@@ -125,7 +125,7 @@ def main(argv: list[str] | None = None) -> int:
125
125
  print("Stopped.")
126
126
 
127
127
  if args.save_token and args.token_file:
128
- _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
128
+ _save_token_file(args.token_file, client.export_token())
129
129
 
130
130
  return 0
131
131
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Iterable, Iterator
5
6
  import datetime
6
7
  from hashlib import md5
7
8
  import json
@@ -13,6 +14,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
13
14
 
14
15
  from Crypto.Cipher import AES
15
16
 
17
+ from .constants import HIK_ENCRYPTION_HEADER
16
18
  from .exceptions import PyEzvizError
17
19
 
18
20
  _LOGGER = logging.getLogger(__name__)
@@ -74,6 +76,49 @@ def string_to_list(data: Any, separator: str = ",") -> Any:
74
76
  return data
75
77
 
76
78
 
79
+ PathComponent = str | int
80
+ WILDCARD_STEP = "*"
81
+ _MISSING = object()
82
+
83
+
84
+ def iter_nested(data: Any, path: Iterable[PathComponent]) -> Iterator[Any]:
85
+ """Yield values reachable by following a dotted path with optional wildcards."""
86
+
87
+ current: list[Any] = [data]
88
+
89
+ for step in path:
90
+ next_level: list[Any] = []
91
+ for candidate in current:
92
+ if step == WILDCARD_STEP:
93
+ if isinstance(candidate, dict):
94
+ next_level.extend(candidate.values())
95
+ elif isinstance(candidate, (list, tuple)):
96
+ next_level.extend(candidate)
97
+ continue
98
+
99
+ if isinstance(candidate, dict) and step in candidate:
100
+ next_level.append(candidate[step])
101
+ continue
102
+
103
+ if isinstance(candidate, (list, tuple)) and isinstance(step, int):
104
+ if -len(candidate) <= step < len(candidate):
105
+ next_level.append(candidate[step])
106
+
107
+ current = next_level
108
+ if not current:
109
+ break
110
+
111
+ yield from current
112
+
113
+
114
+ def first_nested(
115
+ data: Any, path: Iterable[PathComponent], default: Any = None
116
+ ) -> Any:
117
+ """Return the first value produced by iter_nested or ``default``."""
118
+
119
+ return next(iter_nested(data, path), default)
120
+
121
+
77
122
  def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
78
123
  """Fetch the value corresponding to the given nested keys in a dictionary.
79
124
 
@@ -88,14 +133,8 @@ def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
88
133
  The value corresponding to the nested keys or the default value.
89
134
 
90
135
  """
91
- try:
92
- for key in keys:
93
- data = data[key]
94
-
95
- except (KeyError, TypeError):
96
- return default_value
97
-
98
- return data
136
+ value = first_nested(data, keys, _MISSING)
137
+ return default_value if value is _MISSING else value
99
138
 
100
139
 
101
140
  def decrypt_image(input_data: bytes, password: str) -> bytes:
@@ -116,8 +155,10 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
116
155
  raise PyEzvizError("Invalid image data")
117
156
 
118
157
  # check header
119
- if input_data[:16] != b"hikencodepicture":
120
- _LOGGER.debug("Image header doesn't contain 'hikencodepicture'")
158
+ header_len = len(HIK_ENCRYPTION_HEADER)
159
+
160
+ if input_data[:header_len] != HIK_ENCRYPTION_HEADER:
161
+ _LOGGER.debug("Image header doesn't contain %s", HIK_ENCRYPTION_HEADER)
121
162
  return input_data
122
163
 
123
164
  file_hash = input_data[16:48]
@@ -132,7 +173,7 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
132
173
  next_chunk = b""
133
174
  output_data = b""
134
175
  finished = False
135
- i = 48 # offset hikencodepicture + hash
176
+ i = 48 # offset HIK header + hash
136
177
  chunk_size = 1024 * AES.block_size
137
178
  while not finished:
138
179
  chunk, next_chunk = next_chunk, cipher.decrypt(input_data[i : i + chunk_size])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.3.5
3
+ Version: 1.0.3.7
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
@@ -2,6 +2,7 @@ LICENSE
2
2
  LICENSE.md
3
3
  MANIFEST.in
4
4
  README.md
5
+ pyproject.toml
5
6
  setup.py
6
7
  pyezvizapi/__init__.py
7
8
  pyezvizapi/__main__.py
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+
4
+ [tool.ruff]
5
+ line-length = 100
6
+ target-version = "py312"
7
+ src = ["pyezvizapi"]
8
+
9
+ [tool.ruff.lint]
10
+ select = ["E","F","I","UP","B","SIM","PL","RUF","SLF"]
11
+ ignore = ["E501"] # long URLs ok
12
+
13
+ # Per-file tuning
14
+ [tool.ruff.lint.per-file-ignores]
15
+ # CLI files are naturally branchy/return-heavy; quiet those here:
16
+ "pyezvizapi/__main__.py" = ["PLR0911","PLR0912","PLR0915"]
17
+ # Re-export noise in __init__ (if applicable):
18
+ "pyezvizapi/__init__.py" = ["F401"]
19
+
20
+ # Import sorting to match HA expectations
21
+ [tool.ruff.lint.isort]
22
+ known-first-party = ["pyezvizapi"]
23
+ known-third-party = ["homeassistant", "aiohttp", "voluptuous", "yarl", "pandas", "xmltodict", "Crypto"]
24
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
25
+ combine-as-imports = true
26
+ force-sort-within-sections = true
27
+
28
+ # Pylint-like thresholds (only if you prefer global relaxation instead of per-file ignores)
29
+ [tool.ruff.lint.pylint]
30
+ allow-magic-value-types = ["int", "str"]
31
+ max-args = 10
32
+
33
+ [tool.mypy]
34
+ python_version = "3.12"
35
+ files = ["pyezvizapi"]
36
+ ignore_missing_imports = true
37
+ strict_optional = true
38
+ warn_unused_ignores = true
39
+ warn_redundant_casts = true
40
+ warn_no_return = true
41
+ no_implicit_optional = true
42
+ check_untyped_defs = true
43
+
44
+ [tool.coverage.run]
45
+ source = ["pyezvizapi"]
@@ -1,11 +1,11 @@
1
+ from pathlib import Path
1
2
  import setuptools
2
3
 
3
- with open("README.md", "r") as fh:
4
- long_description = fh.read()
4
+ long_description = Path("README.md").read_text(encoding="utf-8")
5
5
 
6
6
  setuptools.setup(
7
7
  name='pyezvizapi',
8
- version="1.0.3.5",
8
+ version="1.0.3.7",
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