pyezvizapi 1.0.1.6__py3-none-any.whl → 1.0.1.8__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/constants.py CHANGED
@@ -1,8 +1,15 @@
1
- """Device switch types relationship."""
1
+ """Constants and enums used by the Ezviz Cloud API wrapper.
2
+
3
+ Includes default timeouts, request headers used to emulate the mobile
4
+ client, and a large collection of enums that map integers/strings from
5
+ the Ezviz API to descriptive names.
6
+ """
2
7
 
3
8
  from enum import Enum, unique
4
9
 
5
- FEATURE_CODE = "1fc28fa018178a1cd1c091b13b2f9f02"
10
+ from .utils import generate_unique_code
11
+
12
+ FEATURE_CODE = generate_unique_code()
6
13
  XOR_KEY = b"\x0c\x0eJ^X\x15@Rr"
7
14
  DEFAULT_TIMEOUT = 25
8
15
  MAX_RETRIES = 3
pyezvizapi/exceptions.py CHANGED
@@ -1,33 +1,33 @@
1
- """PyEzviz Exceptions."""
1
+ """Custom exceptions raised by the Ezviz Cloud API wrapper."""
2
2
 
3
3
 
4
4
  class PyEzvizError(Exception):
5
- """Ezviz api exception."""
5
+ """Base exception for all Ezviz API related errors."""
6
6
 
7
7
 
8
8
  class InvalidURL(PyEzvizError):
9
- """Invalid url exception."""
9
+ """Raised when a request fails due to an invalid URL or proxy settings."""
10
10
 
11
11
 
12
12
  class HTTPError(PyEzvizError):
13
- """Invalid host exception."""
13
+ """Raised when a non-success HTTP status code is returned by the API."""
14
14
 
15
15
 
16
16
  class InvalidHost(PyEzvizError):
17
- """Invalid host exception."""
17
+ """Raised when a hostname/IP is invalid or a TCP connection fails."""
18
18
 
19
19
 
20
20
  class AuthTestResultFailed(PyEzvizError):
21
- """Authentication failed."""
21
+ """Raised by RTSP auth test helpers if credentials are invalid."""
22
22
 
23
23
 
24
24
  class EzvizAuthTokenExpired(PyEzvizError):
25
- """Authentication failed because token is invalid or expired."""
25
+ """Raised when a stored session token is no longer valid (expired/revoked)."""
26
26
 
27
27
 
28
28
  class EzvizAuthVerificationCode(PyEzvizError):
29
- """Authentication failed because MFA verification code is required."""
29
+ """Raised when a login or action requires an MFA (verification) code."""
30
30
 
31
31
 
32
32
  class DeviceException(PyEzvizError):
33
- """The device network is abnormal, please check the device network or try again."""
33
+ """Raised when the physical device reports network or operational issues."""
pyezvizapi/light_bulb.py CHANGED
@@ -1,4 +1,9 @@
1
- """Ezviz camera api."""
1
+ """Ezviz light bulb API.
2
+
3
+ Light-bulb specific helpers to read device status and control
4
+ features exposed via the Ezviz cloud API (on/off, brightness,
5
+ color temperature, etc.).
6
+ """
2
7
 
3
8
  from __future__ import annotations
4
9
 
@@ -11,53 +16,84 @@ from .utils import fetch_nested_value
11
16
 
12
17
  if TYPE_CHECKING:
13
18
  from .client import EzvizClient
19
+ from .models import EzvizDeviceRecord
14
20
 
15
21
 
16
22
  class EzvizLightBulb:
17
- """Initialize Ezviz Light Bulb object."""
23
+ """Representation of an Ezviz light bulb.
24
+
25
+ Provides a thin, typed wrapper over the pagelist/device payload
26
+ for a light bulb, plus convenience methods to toggle and set
27
+ brightness. This class mirrors the camera interface where
28
+ possible to keep integration code simple.
29
+ """
18
30
 
19
31
  def __init__(
20
- self, client: EzvizClient, serial: str, device_obj: dict | None = None
32
+ self,
33
+ client: EzvizClient,
34
+ serial: str,
35
+ device_obj: EzvizDeviceRecord | dict | None = None,
21
36
  ) -> None:
22
- """Initialize the light bulb object."""
37
+ """Initialize the light bulb object.
38
+
39
+ Raises:
40
+ InvalidURL: If the API endpoint/connection is invalid when fetching device info.
41
+ HTTPError: If the API returns a non-success HTTP status while fetching device info.
42
+ PyEzvizError: On Ezviz API contract errors or decoding failures.
43
+ """
23
44
  self._client = client
24
45
  self._serial = serial
25
- self._device = (
26
- device_obj if device_obj else self._client.get_device_infos(self._serial)
27
- )
46
+ if device_obj is None:
47
+ self._device = self._client.get_device_infos(self._serial)
48
+ elif isinstance(device_obj, EzvizDeviceRecord):
49
+ self._device = dict(device_obj.raw)
50
+ else:
51
+ self._device = device_obj
28
52
  self._feature_json = self.get_feature_json()
29
- self._switch: dict[int, bool] = {
30
- switch["type"]: switch["enable"] for switch in self._device["SWITCH"]
31
- }
53
+ switches = self._device.get("SWITCH") or []
54
+ self._switch: dict[int, bool] = {}
55
+ if isinstance(switches, list):
56
+ for switch in switches:
57
+ if not isinstance(switch, dict):
58
+ continue
59
+ t = switch.get("type")
60
+ en = switch.get("enable")
61
+ if isinstance(t, int) and isinstance(en, (bool, int)):
62
+ self._switch[t] = bool(en)
32
63
  if DeviceSwitchType.ALARM_LIGHT.value not in self._switch:
33
64
  # trying to have same interface as the camera's light
34
65
  self._switch[DeviceSwitchType.ALARM_LIGHT.value] = self.get_feature_item(
35
66
  "light_switch"
36
67
  )["dataValue"]
37
68
 
38
- def fetch_key(self, keys: list, default_value: Any = None) -> Any:
39
- """Fetch dictionary key."""
69
+ def fetch_key(self, keys: list[Any], default_value: Any = None) -> Any:
70
+ """Fetch a nested key from the device payload.
71
+
72
+ Uses the same semantics as the camera helper.
73
+ """
40
74
  return fetch_nested_value(self._device, keys, default_value)
41
75
 
42
- def _local_ip(self) -> Any:
43
- """Fix empty ip value for certain cameras."""
44
- if (
45
- self.fetch_key(["WIFI", "address"])
46
- and self._device["WIFI"]["address"] != "0.0.0.0"
47
- ):
48
- return self._device["WIFI"]["address"]
76
+ def _local_ip(self) -> str:
77
+ """Best-effort local IP address for devices that report 0.0.0.0."""
78
+ wifi = self._device.get("WIFI") or {}
79
+ addr = wifi.get("address")
80
+ if isinstance(addr, str) and addr != "0.0.0.0":
81
+ return addr
49
82
 
50
83
  # Seems to return none or 0.0.0.0 on some.
51
- if (
52
- self.fetch_key(["CONNECTION", "localIp"])
53
- and self._device["CONNECTION"]["localIp"] != "0.0.0.0"
54
- ):
55
- return self._device["CONNECTION"]["localIp"]
84
+ conn = self._device.get("CONNECTION") or {}
85
+ local_ip = conn.get("localIp")
86
+ if isinstance(local_ip, str) and local_ip != "0.0.0.0":
87
+ return local_ip
56
88
 
57
89
  return "0.0.0.0"
58
90
 
59
91
  def get_feature_json(self) -> Any:
60
- """Parse the FEATURE json."""
92
+ """Parse the FEATURE JSON string into a Python structure.
93
+
94
+ Raises:
95
+ PyEzvizError: If the FEATURE JSON cannot be decoded.
96
+ """
61
97
  try:
62
98
  json_output = json.loads(self._device["FEATURE"]["featureJson"])
63
99
 
@@ -67,7 +103,7 @@ class EzvizLightBulb:
67
103
  return json_output
68
104
 
69
105
  def get_feature_item(self, key: str, default_value: Any = None) -> Any:
70
- """Get items from FEATURE."""
106
+ """Return a feature item by key from the parsed FEATURE structure."""
71
107
  items = self._feature_json["featureItemDtos"]
72
108
 
73
109
  for item in items:
@@ -77,11 +113,11 @@ class EzvizLightBulb:
77
113
  return default_value if default_value else {"dataValue": ""}
78
114
 
79
115
  def get_product_id(self) -> Any:
80
- """Get product id."""
116
+ """Return the product ID from the FEATURE metadata."""
81
117
  return self._feature_json["productId"]
82
118
 
83
119
  def status(self) -> dict[Any, Any]:
84
- """Return the status of the light bulb."""
120
+ """Return a status dictionary mirroring the camera status shape where possible."""
85
121
  return {
86
122
  "serial": self._serial,
87
123
  "name": self.fetch_key(["deviceInfos", "name"]),
@@ -118,7 +154,16 @@ class EzvizLightBulb:
118
154
  }
119
155
 
120
156
  def _write_state(self, state: bool | None = None) -> bool:
121
- """Set the light bulb state."""
157
+ """Set the light bulb state.
158
+
159
+ If ``state`` is None, the current state will be toggled.
160
+ Returns True on success.
161
+
162
+ Raises:
163
+ PyEzvizError: On API failures.
164
+ InvalidURL: If the API endpoint/connection is invalid.
165
+ HTTPError: If the API returns a non-success HTTP status.
166
+ """
122
167
  item = self.get_feature_item("light_switch")
123
168
 
124
169
  return self._client.set_device_feature_by_key(
@@ -131,15 +176,19 @@ class EzvizLightBulb:
131
176
  def set_brightness(self, value: int) -> bool:
132
177
  """Set the light bulb brightness.
133
178
 
134
- The value must be in range 1-100.
179
+ The value must be in range 1100. Returns True on success.
135
180
 
181
+ Raises:
182
+ PyEzvizError: On API failures.
183
+ InvalidURL: If the API endpoint/connection is invalid.
184
+ HTTPError: If the API returns a non-success HTTP status.
136
185
  """
137
186
  return self._client.set_device_feature_by_key(
138
187
  self._serial, self.get_product_id(), value, "brightness"
139
188
  )
140
189
 
141
190
  def toggle_switch(self) -> bool:
142
- """Toggle on/off light bulb."""
191
+ """Toggle the light bulb on/off."""
143
192
  return self._write_state()
144
193
 
145
194
  def power_on(self) -> bool:
pyezvizapi/models.py ADDED
@@ -0,0 +1,103 @@
1
+ """Lightweight models for Ezviz API payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class EzvizDeviceRecord:
12
+ """A light, ergonomic view over Ezviz get_device_infos() output.
13
+
14
+ Captures commonly used fields with a stable API while preserving
15
+ the full raw mapping for advanced/one-off access.
16
+ """
17
+
18
+ serial: str
19
+ name: str | None
20
+ device_category: str | None
21
+ device_sub_category: str | None
22
+ version: str | None
23
+ status: int | None
24
+
25
+ # Popular sections (pass-through subsets)
26
+ support_ext: Mapping[str, Any] | None = None
27
+ connection: Mapping[str, Any] | None = None
28
+ wifi: Mapping[str, Any] | None = None
29
+ qos: Mapping[str, Any] | None = None
30
+ vtm: Mapping[str, Any] | None = None
31
+ cloud: Mapping[str, Any] | None = None
32
+ p2p: Any | None = None
33
+ time_plan: Any | None = None
34
+ optionals: Mapping[str, Any] | None = None
35
+
36
+ # Switches collapsed to a simple type->enabled map for convenience
37
+ switches: Mapping[int, bool] = field(default_factory=dict)
38
+
39
+ # Full unmodified mapping for anything not yet modeled
40
+ raw: Mapping[str, Any] = field(default_factory=dict)
41
+
42
+ @classmethod
43
+ def from_api(cls, serial: str, data: Mapping[str, Any]) -> EzvizDeviceRecord:
44
+ """Build EzvizDeviceRecord from raw pagelist mapping.
45
+
46
+ Tolerates missing or partially shaped keys.
47
+ """
48
+ device_infos = data.get("deviceInfos", {}) or {}
49
+ status = (data.get("STATUS", {}) or {})
50
+ optionals = status.get("optionals") if isinstance(status, dict) else None
51
+
52
+ # Collapse SWITCH list[{type, enable}] to {type: enable}
53
+ switches_list = data.get("SWITCH") or []
54
+ switches: dict[int, bool] = {}
55
+ for item in switches_list if isinstance(switches_list, list) else []:
56
+ t = item.get("type")
57
+ en = item.get("enable")
58
+ if isinstance(t, int) and isinstance(en, (bool, int)):
59
+ switches[t] = bool(en)
60
+
61
+ return cls(
62
+ serial=serial,
63
+ name=device_infos.get("name"),
64
+ device_category=device_infos.get("deviceCategory") or device_infos.get("device_category"),
65
+ device_sub_category=device_infos.get("deviceSubCategory") or device_infos.get("device_sub_category"),
66
+ version=device_infos.get("version"),
67
+ status=device_infos.get("status") or status.get("globalStatus") if isinstance(status, dict) else None,
68
+ support_ext=device_infos.get("supportExt"),
69
+ connection=data.get("CONNECTION"),
70
+ wifi=data.get("WIFI"),
71
+ qos=data.get("QOS"),
72
+ vtm=next(iter((data.get("VTM") or {}).values()), None),
73
+ cloud=next(iter((data.get("CLOUD") or {}).values()), None),
74
+ p2p=data.get("P2P"),
75
+ time_plan=data.get("TIME_PLAN"),
76
+ optionals=optionals if isinstance(optionals, dict) else None,
77
+ switches=switches,
78
+ raw=data,
79
+ )
80
+
81
+
82
+ def build_device_records_map(devices: Mapping[str, Any]) -> dict[str, EzvizDeviceRecord]:
83
+ """Convert get_device_infos() mapping → {serial: EzvizDeviceRecord}.
84
+
85
+ Keeps behavior robust to partial/missing keys.
86
+ """
87
+ out: dict[str, EzvizDeviceRecord] = {}
88
+ for serial, payload in (devices or {}).items():
89
+ try:
90
+ out[serial] = EzvizDeviceRecord.from_api(serial, payload)
91
+ except (TypeError, KeyError, ValueError):
92
+ # Do not crash on unexpected shapes; fall back to raw wrapper
93
+ out[serial] = EzvizDeviceRecord(
94
+ serial=serial,
95
+ name=(payload.get("deviceInfos") or {}).get("name"),
96
+ device_category=(payload.get("deviceInfos") or {}).get("deviceCategory"),
97
+ device_sub_category=(payload.get("deviceInfos") or {}).get("deviceSubCategory"),
98
+ version=(payload.get("deviceInfos") or {}).get("version"),
99
+ status=(payload.get("deviceInfos") or {}).get("status"),
100
+ raw=payload,
101
+ switches={},
102
+ )
103
+ return out