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/__init__.py +15 -2
- pyezvizapi/__main__.py +406 -283
- pyezvizapi/camera.py +488 -118
- pyezvizapi/cas.py +36 -43
- pyezvizapi/client.py +798 -1342
- pyezvizapi/constants.py +9 -2
- pyezvizapi/exceptions.py +9 -9
- pyezvizapi/light_bulb.py +80 -31
- pyezvizapi/models.py +103 -0
- pyezvizapi/mqtt.py +490 -133
- pyezvizapi/test_cam_rtsp.py +95 -109
- pyezvizapi/test_mqtt.py +135 -0
- pyezvizapi/utils.py +28 -2
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/METADATA +2 -2
- pyezvizapi-1.0.1.8.dist-info/RECORD +21 -0
- pyezvizapi-1.0.1.6.dist-info/RECORD +0 -19
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.1.6.dist-info → pyezvizapi-1.0.1.8.dist-info}/top_level.txt +0 -0
pyezvizapi/constants.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
"""
|
|
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
|
-
|
|
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
|
-
"""
|
|
1
|
+
"""Custom exceptions raised by the Ezviz Cloud API wrapper."""
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class PyEzvizError(Exception):
|
|
5
|
-
"""Ezviz
|
|
5
|
+
"""Base exception for all Ezviz API related errors."""
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class InvalidURL(PyEzvizError):
|
|
9
|
-
"""
|
|
9
|
+
"""Raised when a request fails due to an invalid URL or proxy settings."""
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class HTTPError(PyEzvizError):
|
|
13
|
-
"""
|
|
13
|
+
"""Raised when a non-success HTTP status code is returned by the API."""
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class InvalidHost(PyEzvizError):
|
|
17
|
-
"""
|
|
17
|
+
"""Raised when a hostname/IP is invalid or a TCP connection fails."""
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class AuthTestResultFailed(PyEzvizError):
|
|
21
|
-
"""
|
|
21
|
+
"""Raised by RTSP auth test helpers if credentials are invalid."""
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class EzvizAuthTokenExpired(PyEzvizError):
|
|
25
|
-
"""
|
|
25
|
+
"""Raised when a stored session token is no longer valid (expired/revoked)."""
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class EzvizAuthVerificationCode(PyEzvizError):
|
|
29
|
-
"""
|
|
29
|
+
"""Raised when a login or action requires an MFA (verification) code."""
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class DeviceException(PyEzvizError):
|
|
33
|
-
"""
|
|
33
|
+
"""Raised when the physical device reports network or operational issues."""
|
pyezvizapi/light_bulb.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"""Ezviz
|
|
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
|
-
"""
|
|
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,
|
|
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
|
-
|
|
26
|
-
|
|
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.
|
|
30
|
-
|
|
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
|
|
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) ->
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
179
|
+
The value must be in range 1–100. 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
|
|
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
|