pyezvizapi 1.0.1.5__py3-none-any.whl → 1.0.1.7__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 +4 -1
- pyezvizapi/client.py +17 -1
- pyezvizapi/constants.py +3 -1
- pyezvizapi/mqtt.py +463 -134
- pyezvizapi/test_mqtt.py +64 -0
- pyezvizapi/utils.py +28 -1
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/METADATA +1 -1
- pyezvizapi-1.0.1.7.dist-info/RECORD +20 -0
- pyezvizapi-1.0.1.5.dist-info/RECORD +0 -19
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.1.5.dist-info → pyezvizapi-1.0.1.7.dist-info}/top_level.txt +0 -0
pyezvizapi/__init__.py
CHANGED
|
@@ -28,7 +28,7 @@ from .exceptions import (
|
|
|
28
28
|
PyEzvizError,
|
|
29
29
|
)
|
|
30
30
|
from .light_bulb import EzvizLightBulb
|
|
31
|
-
from .mqtt import MQTTClient
|
|
31
|
+
from .mqtt import EzvizToken, MQTTClient, MqttData, ServiceUrls
|
|
32
32
|
from .test_cam_rtsp import TestRTSPAuth
|
|
33
33
|
|
|
34
34
|
__all__ = [
|
|
@@ -47,14 +47,17 @@ __all__ = [
|
|
|
47
47
|
"EzvizCamera",
|
|
48
48
|
"EzvizClient",
|
|
49
49
|
"EzvizLightBulb",
|
|
50
|
+
"EzvizToken",
|
|
50
51
|
"HTTPError",
|
|
51
52
|
"IntelligentDetectionSmartApp",
|
|
52
53
|
"InvalidHost",
|
|
53
54
|
"InvalidURL",
|
|
54
55
|
"MQTTClient",
|
|
55
56
|
"MessageFilterType",
|
|
57
|
+
"MqttData",
|
|
56
58
|
"NightVisionMode",
|
|
57
59
|
"PyEzvizError",
|
|
60
|
+
"ServiceUrls",
|
|
58
61
|
"SoundMode",
|
|
59
62
|
"SupportExt",
|
|
60
63
|
"TestRTSPAuth",
|
pyezvizapi/client.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
import hashlib
|
|
7
8
|
import json
|
|
@@ -79,6 +80,7 @@ from .exceptions import (
|
|
|
79
80
|
PyEzvizError,
|
|
80
81
|
)
|
|
81
82
|
from .light_bulb import EzvizLightBulb
|
|
83
|
+
from .mqtt import MQTTClient
|
|
82
84
|
from .utils import convert_to_dict, deep_merge
|
|
83
85
|
|
|
84
86
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -112,6 +114,7 @@ class EzvizClient:
|
|
|
112
114
|
self._timeout = timeout
|
|
113
115
|
self._cameras: dict[str, Any] = {}
|
|
114
116
|
self._light_bulbs: dict[str, Any] = {}
|
|
117
|
+
self.mqtt_client: MQTTClient | None = None
|
|
115
118
|
|
|
116
119
|
def _login(self, smscode: int | None = None) -> dict[Any, Any]:
|
|
117
120
|
"""Login to Ezviz API."""
|
|
@@ -1517,7 +1520,7 @@ class EzvizClient:
|
|
|
1517
1520
|
if json_output["meta"]["code"] == 80000:
|
|
1518
1521
|
raise EzvizAuthVerificationCode("Operation requires 2FA check")
|
|
1519
1522
|
|
|
1520
|
-
if json_output["
|
|
1523
|
+
if json_output["meta"]["code"] == 2009:
|
|
1521
1524
|
raise DeviceException(f"Device not reachable: Got {json_output}")
|
|
1522
1525
|
|
|
1523
1526
|
if json_output["meta"]["code"] != 200:
|
|
@@ -2488,6 +2491,19 @@ class EzvizClient:
|
|
|
2488
2491
|
|
|
2489
2492
|
return True
|
|
2490
2493
|
|
|
2494
|
+
def get_mqtt_client(
|
|
2495
|
+
self, on_message_callback: Callable[[dict[str, Any]], None] | None = None
|
|
2496
|
+
) -> MQTTClient:
|
|
2497
|
+
"""Return a configured MQTTClient using this client's session."""
|
|
2498
|
+
if self.mqtt_client is None:
|
|
2499
|
+
self.mqtt_client = MQTTClient(
|
|
2500
|
+
token=self._token,
|
|
2501
|
+
session=self._session,
|
|
2502
|
+
timeout=self._timeout,
|
|
2503
|
+
on_message_callback=on_message_callback,
|
|
2504
|
+
)
|
|
2505
|
+
return self.mqtt_client
|
|
2506
|
+
|
|
2491
2507
|
def _get_page_list(self) -> Any:
|
|
2492
2508
|
"""Get ezviz device info broken down in sections."""
|
|
2493
2509
|
return self._api_get_pagelist(
|
pyezvizapi/constants.py
CHANGED
pyezvizapi/mqtt.py
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
-
"""Ezviz cloud MQTT client for push messages.
|
|
1
|
+
"""Ezviz cloud MQTT client for push messages.
|
|
2
|
+
|
|
3
|
+
Synchronous MQTT client tailored for EZVIZ push notifications as used by
|
|
4
|
+
`pyezvizapi` and Home Assistant integrations. Handles the EZVIZ registration
|
|
5
|
+
flow, starts/stops push, maintains a long-lived MQTT connection, and decodes
|
|
6
|
+
incoming payloads into a structured form.
|
|
7
|
+
|
|
8
|
+
This module is intentionally synchronous (uses `requests` and
|
|
9
|
+
`paho-mqtt`'s background network thread via `loop_start()`), which keeps
|
|
10
|
+
integration code simple. If you later migrate to an async HA integration,
|
|
11
|
+
wrap the blocking calls with `hass.async_add_executor_job`.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> client = MQTTClient(token)
|
|
15
|
+
>>> client.connect()
|
|
16
|
+
>>> # ... handle callbacks or read client.messages_by_device ...
|
|
17
|
+
>>> client.stop()
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
2
21
|
from __future__ import annotations
|
|
3
22
|
|
|
4
23
|
import base64
|
|
24
|
+
from collections import OrderedDict
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from contextlib import suppress
|
|
5
27
|
import json
|
|
6
28
|
import logging
|
|
7
|
-
import
|
|
8
|
-
import time
|
|
9
|
-
from typing import Any
|
|
29
|
+
from typing import Any, Final, TypedDict
|
|
10
30
|
|
|
11
31
|
import paho.mqtt.client as mqtt
|
|
12
32
|
import requests
|
|
@@ -16,111 +36,314 @@ from .api_endpoints import (
|
|
|
16
36
|
API_ENDPOINT_START_MQTT,
|
|
17
37
|
API_ENDPOINT_STOP_MQTT,
|
|
18
38
|
)
|
|
19
|
-
from .constants import
|
|
20
|
-
APP_SECRET,
|
|
21
|
-
DEFAULT_TIMEOUT,
|
|
22
|
-
FEATURE_CODE,
|
|
23
|
-
MQTT_APP_KEY,
|
|
24
|
-
REQUEST_HEADER,
|
|
25
|
-
)
|
|
39
|
+
from .constants import APP_SECRET, DEFAULT_TIMEOUT, FEATURE_CODE, MQTT_APP_KEY
|
|
26
40
|
from .exceptions import HTTPError, InvalidURL, PyEzvizError
|
|
27
41
|
|
|
28
42
|
_LOGGER = logging.getLogger(__name__)
|
|
29
43
|
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Typed structures
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ServiceUrls(TypedDict):
|
|
51
|
+
"""Service URLs present in the EZVIZ auth token.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
pushAddr: Hostname of the EZVIZ push/MQTT entry point.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
pushAddr: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EzvizToken(TypedDict):
|
|
61
|
+
"""Minimal shape of the EZVIZ token required for MQTT.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
username: Internal EZVIZ username.
|
|
65
|
+
session_id: Current session id.
|
|
66
|
+
service_urls: Nested object containing at least ``pushAddr``.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
username: str
|
|
70
|
+
session_id: str
|
|
71
|
+
service_urls: ServiceUrls
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MqttData(TypedDict):
|
|
75
|
+
"""Typed dictionary for EZVIZ MQTT connection data."""
|
|
76
|
+
|
|
77
|
+
mqtt_clientid: str | None
|
|
78
|
+
ticket: str | None
|
|
79
|
+
push_url: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Payload decoding helpers
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
# Field names in the comma‑separated ``ext`` payload from EZVIZ.
|
|
87
|
+
EXT_FIELD_NAMES: Final[tuple[str, ...]] = (
|
|
88
|
+
"channel_type",
|
|
89
|
+
"time",
|
|
90
|
+
"device_serial",
|
|
91
|
+
"channel_no",
|
|
92
|
+
"alert_type_code",
|
|
93
|
+
"unused1",
|
|
94
|
+
"unused2",
|
|
95
|
+
"unused3",
|
|
96
|
+
"unused4",
|
|
97
|
+
"status_flag",
|
|
98
|
+
"file_id",
|
|
99
|
+
"is_encrypted",
|
|
100
|
+
"encrypted_pwd_hash",
|
|
101
|
+
"unknown_flag",
|
|
102
|
+
"unused5",
|
|
103
|
+
"alarm_log_id",
|
|
104
|
+
"image",
|
|
105
|
+
"device_name",
|
|
106
|
+
"unused6",
|
|
107
|
+
"sequence_number",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Fields that should be converted to ``int`` if present.
|
|
111
|
+
EXT_INT_FIELDS: Final[frozenset[str]] = frozenset(
|
|
112
|
+
{
|
|
113
|
+
"channel_type",
|
|
114
|
+
"channel_no",
|
|
115
|
+
"alert_type_code",
|
|
116
|
+
"status_flag",
|
|
117
|
+
"is_encrypted",
|
|
118
|
+
"sequence_number",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Client
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MQTTClient:
|
|
129
|
+
"""MQTT client for Ezviz push notifications.
|
|
130
|
+
|
|
131
|
+
Handles the Ezviz-specific registration and connection process,
|
|
132
|
+
maintains a persistent MQTT connection, and processes incoming messages.
|
|
133
|
+
|
|
134
|
+
Messages are stored per device_serial in `messages_by_device`, and an optional
|
|
135
|
+
callback can be provided to handle messages as they arrive.
|
|
136
|
+
|
|
137
|
+
Typical usage::
|
|
138
|
+
|
|
139
|
+
client = MQTTClient(token=auth_token)
|
|
140
|
+
client.connect(clean_session=True)
|
|
141
|
+
|
|
142
|
+
# Access last message for a device
|
|
143
|
+
last_msg = client.messages_by_device.get(device_serial)
|
|
144
|
+
|
|
145
|
+
# Stop the client when done
|
|
146
|
+
client.stop()
|
|
147
|
+
"""
|
|
33
148
|
|
|
34
149
|
def __init__(
|
|
35
150
|
self,
|
|
36
|
-
token: dict,
|
|
151
|
+
token: EzvizToken | dict,
|
|
152
|
+
session: requests.Session,
|
|
37
153
|
timeout: int = DEFAULT_TIMEOUT,
|
|
154
|
+
on_message_callback: Callable[[dict[str, Any]], None] | None = None,
|
|
155
|
+
*,
|
|
156
|
+
max_messages: int = 1000,
|
|
38
157
|
) -> None:
|
|
39
|
-
"""Initialize the client
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
158
|
+
"""Initialize the Ezviz MQTT client.
|
|
159
|
+
|
|
160
|
+
This client handles registration with the Ezviz push service, maintains
|
|
161
|
+
a persistent MQTT connection, and decodes incoming push messages.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
token (dict): Authentication token dictionary returned by EzvizClient.login().
|
|
165
|
+
Must include:
|
|
166
|
+
- 'username': Ezviz account username (The account aliase or generated one.)
|
|
167
|
+
- 'session_id': session token for API access
|
|
168
|
+
- 'service_urls': dictionary containing at least 'pushAddr'
|
|
169
|
+
timeout (int, optional): HTTP request timeout in seconds. Defaults to DEFAULT_TIMEOUT.
|
|
170
|
+
session (requests.Session): Pre-configured requests session for HTTP calls.
|
|
171
|
+
on_message_callback (Callable[[dict[str, Any]], None], optional): Optional callback function
|
|
172
|
+
that will be called for each decoded MQTT message. The callback receives
|
|
173
|
+
a dictionary with the message data. Defaults to None.
|
|
174
|
+
max_messages:
|
|
175
|
+
Maximum number of device entries kept in :attr:`messages_by_device`.
|
|
176
|
+
Oldest entries are evicted when the limit is exceeded. Defaults to ``1000``.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
PyEzvizError: If the provided token is missing required fields.
|
|
180
|
+
"""
|
|
181
|
+
if not token or not token.get("username"):
|
|
182
|
+
raise PyEzvizError(
|
|
183
|
+
"Ezviz internal username is required. Ensure EzvizClient.login() was called first."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Requests session (synchronous)
|
|
187
|
+
self._session = session
|
|
188
|
+
|
|
189
|
+
self._token: EzvizToken | dict = token
|
|
190
|
+
self._timeout: int = timeout
|
|
191
|
+
self._topic: str = f"{MQTT_APP_KEY}/#"
|
|
192
|
+
self._on_message_callback = on_message_callback
|
|
193
|
+
self._max_messages: int = max_messages
|
|
194
|
+
|
|
195
|
+
self._mqtt_data: MqttData = {
|
|
52
196
|
"mqtt_clientid": None,
|
|
53
197
|
"ticket": None,
|
|
54
198
|
"push_url": token["service_urls"]["pushAddr"],
|
|
55
199
|
}
|
|
56
|
-
self.mqtt_client = None
|
|
57
|
-
self.rcv_message: dict[Any, Any] = {}
|
|
58
200
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"""On MQTT message subscribe."""
|
|
63
|
-
# pylint: disable=unused-argument
|
|
64
|
-
_LOGGER.info("Subscribed: %s %s", mid, granted_qos)
|
|
201
|
+
self.mqtt_client: mqtt.Client | None = None
|
|
202
|
+
# Keep last payload per device, bounded by ``max_messages``
|
|
203
|
+
self.messages_by_device: OrderedDict[str, dict[str, Any]] = OrderedDict()
|
|
65
204
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"""On MQTT connect."""
|
|
70
|
-
# pylint: disable=unused-argument
|
|
71
|
-
if return_code == 0:
|
|
72
|
-
_LOGGER.info("Connected OK with return code %s", return_code)
|
|
73
|
-
else:
|
|
74
|
-
_LOGGER.info("Connection Error with Return code %s", return_code)
|
|
75
|
-
client.reconnect()
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
# Public API
|
|
207
|
+
# ------------------------------------------------------------------
|
|
76
208
|
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
79
|
-
# pylint: disable=unused-argument
|
|
80
|
-
try:
|
|
81
|
-
mqtt_message = json.loads(msg.payload)
|
|
82
|
-
|
|
83
|
-
except ValueError as err:
|
|
84
|
-
self.stop()
|
|
85
|
-
raise PyEzvizError(
|
|
86
|
-
"Impossible to decode mqtt message: " + str(err)
|
|
87
|
-
) from err
|
|
209
|
+
def connect(self, *, clean_session: bool = False, keepalive: int = 60) -> None:
|
|
210
|
+
"""Connect to the Ezviz MQTT broker and start receiving push messages.
|
|
88
211
|
|
|
89
|
-
|
|
212
|
+
This method performs the following steps:
|
|
213
|
+
1. Registers the client with Ezviz push service.
|
|
214
|
+
2. Starts push notifications for this client.
|
|
215
|
+
3. Configures and connects the underlying MQTT client.
|
|
216
|
+
4. Starts the MQTT network loop in a background thread.
|
|
90
217
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"alert": mqtt_message["alert"],
|
|
95
|
-
"time": mqtt_message["ext"][1],
|
|
96
|
-
"alert type": mqtt_message["ext"][4],
|
|
97
|
-
"image": mqtt_message["ext"][16] if len(mqtt_message["ext"]) > 16 else None,
|
|
98
|
-
}
|
|
218
|
+
Keyword Args:
|
|
219
|
+
clean_session (bool, optional): Whether to start a clean MQTT session. Defaults to False.
|
|
220
|
+
keepalive (int, optional): Keep-alive interval in seconds for the MQTT connection. Defaults to 60.
|
|
99
221
|
|
|
100
|
-
|
|
222
|
+
Raises:
|
|
223
|
+
PyEzvizError: If required Ezviz credentials are missing or registration/start fails.
|
|
224
|
+
"""
|
|
225
|
+
self._register_ezviz_push()
|
|
226
|
+
self._start_ezviz_push()
|
|
227
|
+
self._configure_mqtt(clean_session=clean_session)
|
|
228
|
+
assert self.mqtt_client is not None
|
|
229
|
+
self.mqtt_client.connect(self._mqtt_data["push_url"], 1882, keepalive)
|
|
230
|
+
self.mqtt_client.loop_start()
|
|
101
231
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
232
|
+
def stop(self) -> None:
|
|
233
|
+
"""Stop the MQTT client and push notifications.
|
|
234
|
+
|
|
235
|
+
This method stops the MQTT network loop, disconnects from the broker,
|
|
236
|
+
and signals the Ezviz API to stop push notifications.
|
|
237
|
+
|
|
238
|
+
This method is idempotent and can be called multiple times safely.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
PyEzvizError: If stopping the push service fails.
|
|
242
|
+
"""
|
|
243
|
+
if self.mqtt_client:
|
|
244
|
+
try:
|
|
245
|
+
# Stop background thread and disconnect
|
|
246
|
+
self.mqtt_client.loop_stop()
|
|
247
|
+
self.mqtt_client.disconnect()
|
|
248
|
+
except Exception as err: # noqa: BLE001
|
|
249
|
+
_LOGGER.debug("MQTT disconnect failed: %s", err)
|
|
250
|
+
# Always attempt to stop push on server side
|
|
251
|
+
self._stop_ezviz_push()
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# MQTT callbacks
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def _on_subscribe(
|
|
258
|
+
self, client: mqtt.Client, userdata: Any, mid: int, granted_qos: tuple[int, ...]
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Handle subscription acknowledgement from the broker."""
|
|
261
|
+
_LOGGER.info("Subscribed: mid=%s qos=%s", mid, granted_qos)
|
|
262
|
+
_LOGGER.info("Subscribed to EZVIZ MQTT topic: %s", self._topic)
|
|
104
263
|
|
|
105
|
-
|
|
106
|
-
|
|
264
|
+
def _on_connect(
|
|
265
|
+
self, client: mqtt.Client, userdata: Any, flags: dict, rc: int
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Handle successful or failed MQTT connection attempts.
|
|
268
|
+
|
|
269
|
+
Subscribes to the topic if this is a new session and logs connection status.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
client (mqtt.Client): The MQTT client instance.
|
|
273
|
+
userdata (Any): The user data passed to the client (not used).
|
|
274
|
+
flags (dict): MQTT flags dictionary, includes 'session present'.
|
|
275
|
+
rc (int): MQTT connection result code. 0 indicates success.
|
|
276
|
+
"""
|
|
277
|
+
session_present = (
|
|
278
|
+
flags.get("session present") if isinstance(flags, dict) else None
|
|
107
279
|
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ezviz_mqtt_client.on_message = self.on_message
|
|
111
|
-
ezviz_mqtt_client.username_pw_set(MQTT_APP_KEY, APP_SECRET)
|
|
112
|
-
|
|
113
|
-
ezviz_mqtt_client.connect(self._mqtt_data["push_url"], 1882, 60)
|
|
114
|
-
ezviz_mqtt_client.subscribe(
|
|
115
|
-
f"{MQTT_APP_KEY}/ticket/{self._mqtt_data['ticket']}", qos=2
|
|
280
|
+
_LOGGER.info(
|
|
281
|
+
"Connected to EZVIZ broker rc=%s session_present=%s", rc, session_present
|
|
116
282
|
)
|
|
283
|
+
if rc == 0 and not session_present:
|
|
284
|
+
client.subscribe(self._topic, qos=2)
|
|
285
|
+
if rc != 0:
|
|
286
|
+
# Let paho handle reconnects (reconnect_delay_set configured)
|
|
287
|
+
_LOGGER.error("MQTT connection failed, return code: %s", rc)
|
|
288
|
+
|
|
289
|
+
def _on_disconnect(self, client: mqtt.Client, userdata: Any, rc: int) -> None:
|
|
290
|
+
"""Called when the MQTT client disconnects from the broker.
|
|
291
|
+
|
|
292
|
+
Logs the disconnection. Automatic reconnects are handled by paho-mqtt.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
client (mqtt.Client): The MQTT client instance.
|
|
296
|
+
userdata (Any): The user data passed to the client (not used).
|
|
297
|
+
rc (int): Disconnect result code. 0 indicates a clean disconnect.
|
|
298
|
+
"""
|
|
299
|
+
_LOGGER.warning("Disconnected from EZVIZ MQTT broker (rc=%s)", rc)
|
|
300
|
+
|
|
301
|
+
def _on_message(
|
|
302
|
+
self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Handle incoming MQTT messages.
|
|
305
|
+
|
|
306
|
+
Decodes the payload, updates `messages_by_device` with the latest message,
|
|
307
|
+
and calls the optional user callback.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
client (mqtt.Client): The MQTT client instance.
|
|
311
|
+
userdata (Any): The user data passed to the client (not used).
|
|
312
|
+
msg (mqtt.MQTTMessage): The MQTT message object containing payload and topic.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
decoded = self.decode_mqtt_message(msg.payload)
|
|
316
|
+
except PyEzvizError as err:
|
|
317
|
+
_LOGGER.warning("Failed to decode MQTT message: %s", err)
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
device_serial = decoded.get("ext", {}).get("device_serial")
|
|
321
|
+
if device_serial:
|
|
322
|
+
self._cache_message(device_serial, decoded)
|
|
323
|
+
_LOGGER.debug("Stored message for device_serial %s", device_serial)
|
|
324
|
+
else:
|
|
325
|
+
_LOGGER.warning("Received message with no device_serial: %s", decoded)
|
|
326
|
+
|
|
327
|
+
if self._on_message_callback:
|
|
328
|
+
try:
|
|
329
|
+
self._on_message_callback(decoded)
|
|
330
|
+
except Exception:
|
|
331
|
+
_LOGGER.exception("The on_message_callback raised")
|
|
117
332
|
|
|
118
|
-
|
|
119
|
-
|
|
333
|
+
# ------------------------------------------------------------------
|
|
334
|
+
# HTTP helpers
|
|
335
|
+
# ------------------------------------------------------------------
|
|
120
336
|
|
|
121
337
|
def _register_ezviz_push(self) -> None:
|
|
122
|
-
"""Register
|
|
338
|
+
"""Register the client with the Ezviz push service.
|
|
123
339
|
|
|
340
|
+
Sends the necessary information to Ezviz to obtain a unique MQTT client ID.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
PyEzvizError: If the registration fails or the API returns a non-200 status.
|
|
344
|
+
InvalidURL: If the push service URL is invalid or unreachable.
|
|
345
|
+
HTTPError: If the HTTP request fails for other reasons.
|
|
346
|
+
"""
|
|
124
347
|
auth_seq = (
|
|
125
348
|
"Basic "
|
|
126
349
|
+ base64.b64encode(f"{MQTT_APP_KEY}:{APP_SECRET}".encode("ascii")).decode()
|
|
@@ -142,118 +365,224 @@ class MQTTClient(threading.Thread):
|
|
|
142
365
|
data=payload,
|
|
143
366
|
timeout=self._timeout,
|
|
144
367
|
)
|
|
145
|
-
|
|
146
368
|
req.raise_for_status()
|
|
147
|
-
|
|
148
|
-
except requests.ConnectionError as err:
|
|
149
|
-
raise InvalidURL("A Invalid URL or Proxy error occured") from err
|
|
150
|
-
|
|
151
|
-
except requests.HTTPError as err:
|
|
369
|
+
except requests.HTTPError as err: # network OK, HTTP error status
|
|
152
370
|
raise HTTPError from err
|
|
153
371
|
|
|
154
372
|
try:
|
|
155
|
-
|
|
156
|
-
|
|
373
|
+
json_output = req.json()
|
|
374
|
+
except requests.ConnectionError as err:
|
|
375
|
+
raise InvalidURL("Invalid URL or proxy error") from err
|
|
157
376
|
except ValueError as err:
|
|
158
377
|
raise PyEzvizError(
|
|
159
378
|
"Impossible to decode response: "
|
|
160
379
|
+ str(err)
|
|
161
|
-
+ "
|
|
380
|
+
+ "Response was: "
|
|
162
381
|
+ str(req.text)
|
|
163
382
|
) from err
|
|
164
383
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def run(self) -> None:
|
|
168
|
-
"""Start mqtt thread."""
|
|
169
|
-
|
|
170
|
-
if self._token.get("username") is None:
|
|
171
|
-
self._stop_event.set()
|
|
384
|
+
if json_output.get("status") != 200:
|
|
172
385
|
raise PyEzvizError(
|
|
173
|
-
"
|
|
386
|
+
f"Could not register to EZVIZ mqtt server: Got {json_output})"
|
|
174
387
|
)
|
|
175
388
|
|
|
176
|
-
|
|
177
|
-
self.
|
|
178
|
-
self.mqtt_client = self._mqtt()
|
|
179
|
-
|
|
180
|
-
def start(self) -> None:
|
|
181
|
-
"""Start mqtt thread as application. Set logging first to see messages."""
|
|
182
|
-
self.run()
|
|
389
|
+
# Persist client id from payload
|
|
390
|
+
self._mqtt_data["mqtt_clientid"] = json_output["data"]["clientId"]
|
|
183
391
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
time.sleep(1)
|
|
187
|
-
except KeyboardInterrupt:
|
|
188
|
-
self.stop()
|
|
392
|
+
def _start_ezviz_push(self) -> None:
|
|
393
|
+
"""Start push notifications for this client with the Ezviz API.
|
|
189
394
|
|
|
190
|
-
|
|
191
|
-
|
|
395
|
+
Sends the client ID, session ID, and username to Ezviz so that the server
|
|
396
|
+
will start pushing messages to this client.
|
|
192
397
|
|
|
398
|
+
Raises:
|
|
399
|
+
PyEzvizError: If the API fails to start push notifications or returns a non-200 status.
|
|
400
|
+
InvalidURL: If the push service URL is invalid or unreachable.
|
|
401
|
+
HTTPError: If the HTTP request fails for other reasons.
|
|
402
|
+
"""
|
|
193
403
|
payload = {
|
|
194
404
|
"appKey": MQTT_APP_KEY,
|
|
195
405
|
"clientId": self._mqtt_data["mqtt_clientid"],
|
|
196
406
|
"clientType": 5,
|
|
197
407
|
"sessionId": self._token["session_id"],
|
|
198
408
|
"username": self._token["username"],
|
|
409
|
+
"token": "123456",
|
|
199
410
|
}
|
|
200
411
|
|
|
201
412
|
try:
|
|
202
413
|
req = self._session.post(
|
|
203
|
-
f"https://{self._mqtt_data['push_url']}{
|
|
414
|
+
f"https://{self._mqtt_data['push_url']}{API_ENDPOINT_START_MQTT}",
|
|
415
|
+
allow_redirects=False,
|
|
204
416
|
data=payload,
|
|
205
417
|
timeout=self._timeout,
|
|
206
418
|
)
|
|
207
|
-
|
|
208
419
|
req.raise_for_status()
|
|
420
|
+
except requests.HTTPError as err:
|
|
421
|
+
raise HTTPError from err
|
|
209
422
|
|
|
423
|
+
try:
|
|
424
|
+
json_output = req.json()
|
|
210
425
|
except requests.ConnectionError as err:
|
|
211
|
-
raise InvalidURL("
|
|
426
|
+
raise InvalidURL("Invalid URL or proxy error") from err
|
|
427
|
+
except ValueError as err:
|
|
428
|
+
raise PyEzvizError(
|
|
429
|
+
"Impossible to decode response: "
|
|
430
|
+
+ str(err)
|
|
431
|
+
+ "Response was: "
|
|
432
|
+
+ str(req.text)
|
|
433
|
+
) from err
|
|
212
434
|
|
|
213
|
-
|
|
214
|
-
raise
|
|
435
|
+
if json_output.get("status") != 200:
|
|
436
|
+
raise PyEzvizError(
|
|
437
|
+
f"Could not signal EZVIZ mqtt server to start pushing messages: Got {json_output})"
|
|
438
|
+
)
|
|
215
439
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
self.mqtt_client.loop_stop()
|
|
440
|
+
self._mqtt_data["ticket"] = json_output["ticket"]
|
|
441
|
+
_LOGGER.info("EZVIZ MQTT ticket acquired")
|
|
219
442
|
|
|
220
|
-
def
|
|
221
|
-
"""
|
|
443
|
+
def _stop_ezviz_push(self) -> None:
|
|
444
|
+
"""Stop push notifications for this client via the Ezviz API.
|
|
445
|
+
|
|
446
|
+
Sends the client ID and session information to stop further messages.
|
|
222
447
|
|
|
448
|
+
Raises:
|
|
449
|
+
PyEzvizError: If the API fails to stop push notifications or returns a non-200 status.
|
|
450
|
+
InvalidURL: If the push service URL is invalid or unreachable.
|
|
451
|
+
HTTPError: If the HTTP request fails for other reasons.
|
|
452
|
+
"""
|
|
223
453
|
payload = {
|
|
224
454
|
"appKey": MQTT_APP_KEY,
|
|
225
455
|
"clientId": self._mqtt_data["mqtt_clientid"],
|
|
226
456
|
"clientType": 5,
|
|
227
457
|
"sessionId": self._token["session_id"],
|
|
228
458
|
"username": self._token["username"],
|
|
229
|
-
"token": "123456",
|
|
230
459
|
}
|
|
231
460
|
|
|
232
461
|
try:
|
|
233
462
|
req = self._session.post(
|
|
234
|
-
f"https://{self._mqtt_data['push_url']}{
|
|
235
|
-
allow_redirects=False,
|
|
463
|
+
f"https://{self._mqtt_data['push_url']}{API_ENDPOINT_STOP_MQTT}",
|
|
236
464
|
data=payload,
|
|
237
465
|
timeout=self._timeout,
|
|
238
466
|
)
|
|
239
|
-
|
|
240
467
|
req.raise_for_status()
|
|
241
|
-
|
|
242
|
-
except requests.ConnectionError as err:
|
|
243
|
-
raise InvalidURL("A Invalid URL or Proxy error occured") from err
|
|
244
|
-
|
|
245
468
|
except requests.HTTPError as err:
|
|
246
469
|
raise HTTPError from err
|
|
247
470
|
|
|
248
471
|
try:
|
|
249
|
-
|
|
250
|
-
|
|
472
|
+
json_output = req.json()
|
|
473
|
+
except requests.ConnectionError as err:
|
|
474
|
+
raise InvalidURL("Invalid URL or proxy error") from err
|
|
251
475
|
except ValueError as err:
|
|
252
476
|
raise PyEzvizError(
|
|
253
477
|
"Impossible to decode response: "
|
|
254
478
|
+ str(err)
|
|
255
|
-
+ "
|
|
479
|
+
+ "Response was: "
|
|
256
480
|
+ str(req.text)
|
|
257
481
|
) from err
|
|
258
482
|
|
|
259
|
-
|
|
483
|
+
if json_output.get("status") != 200:
|
|
484
|
+
raise PyEzvizError(
|
|
485
|
+
f"Could not signal EZVIZ mqtt server to stop pushing messages: Got {json_output})"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# ------------------------------------------------------------------
|
|
489
|
+
# Internal helpers
|
|
490
|
+
# ------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
def _configure_mqtt(self, *, clean_session: bool) -> None:
|
|
493
|
+
"""Internal helper to configure and connect the paho-mqtt client.
|
|
494
|
+
|
|
495
|
+
This method sets up the MQTT client with:
|
|
496
|
+
- Callbacks for connect, disconnect, subscribe, and message
|
|
497
|
+
- Username and password authentication
|
|
498
|
+
- Reconnect delay settings
|
|
499
|
+
- Broker connection on the configured topic
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
clean_session (bool): Whether to start a clean MQTT session.
|
|
503
|
+
|
|
504
|
+
Notes:
|
|
505
|
+
This method is called automatically by `connect()`.
|
|
506
|
+
|
|
507
|
+
"""
|
|
508
|
+
broker = self._mqtt_data["push_url"]
|
|
509
|
+
|
|
510
|
+
self.mqtt_client = mqtt.Client(
|
|
511
|
+
callback_api_version=mqtt.CallbackAPIVersion.VERSION1,
|
|
512
|
+
client_id=self._mqtt_data["mqtt_clientid"],
|
|
513
|
+
clean_session=clean_session,
|
|
514
|
+
protocol=mqtt.MQTTv311,
|
|
515
|
+
transport="tcp",
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Bind callbacks
|
|
519
|
+
self.mqtt_client.on_connect = self._on_connect
|
|
520
|
+
self.mqtt_client.on_disconnect = self._on_disconnect
|
|
521
|
+
self.mqtt_client.on_subscribe = self._on_subscribe
|
|
522
|
+
self.mqtt_client.on_message = self._on_message
|
|
523
|
+
|
|
524
|
+
# Auth (do not log these!)
|
|
525
|
+
self.mqtt_client.username_pw_set(MQTT_APP_KEY, APP_SECRET)
|
|
526
|
+
|
|
527
|
+
# Backoff for reconnects handled by paho
|
|
528
|
+
self.mqtt_client.reconnect_delay_set(min_delay=5, max_delay=10)
|
|
529
|
+
|
|
530
|
+
_LOGGER.debug("Configured MQTT client for broker %s", broker)
|
|
531
|
+
|
|
532
|
+
def _cache_message(self, device_serial: str, payload: dict[str, Any]) -> None:
|
|
533
|
+
"""Cache latest message per device with an LRU-like policy.
|
|
534
|
+
|
|
535
|
+
Parameters:
|
|
536
|
+
device_serial (str): Device serial extracted from the message ``ext``.
|
|
537
|
+
payload (dict[str, Any]): Decoded message dictionary to store.
|
|
538
|
+
"""
|
|
539
|
+
# Move existing to the end or insert new
|
|
540
|
+
if device_serial in self.messages_by_device:
|
|
541
|
+
del self.messages_by_device[device_serial]
|
|
542
|
+
self.messages_by_device[device_serial] = payload
|
|
543
|
+
# Evict oldest if above limit
|
|
544
|
+
while len(self.messages_by_device) > self._max_messages:
|
|
545
|
+
self.messages_by_device.popitem(last=False)
|
|
546
|
+
|
|
547
|
+
# ------------------------------------------------------------------
|
|
548
|
+
# Public decoding API
|
|
549
|
+
# ------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
def decode_mqtt_message(self, payload_bytes: bytes) -> dict[str, Any]:
|
|
552
|
+
"""Decode raw MQTT message payload into a structured dictionary.
|
|
553
|
+
|
|
554
|
+
The returned dictionary will contain all top-level fields from the message,
|
|
555
|
+
and the 'ext' field is parsed into named subfields with numeric fields converted to int.
|
|
556
|
+
|
|
557
|
+
Parameters:
|
|
558
|
+
payload_bytes (bytes): Raw payload received from MQTT broker.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
dict: Decoded message with ``ext`` mapped to named fields; numeric fields
|
|
562
|
+
converted to ``int`` where appropriate.
|
|
563
|
+
|
|
564
|
+
Raises:
|
|
565
|
+
PyEzvizError: If the payload is not valid JSON.
|
|
566
|
+
"""
|
|
567
|
+
try:
|
|
568
|
+
payload_str = payload_bytes.decode("utf-8")
|
|
569
|
+
data: dict[str, Any] = json.loads(payload_str)
|
|
570
|
+
|
|
571
|
+
if "ext" in data and isinstance(data["ext"], str):
|
|
572
|
+
ext_parts = data["ext"].split(",")
|
|
573
|
+
ext_dict: dict[str, Any] = {}
|
|
574
|
+
for i, name in enumerate(EXT_FIELD_NAMES):
|
|
575
|
+
value: Any = ext_parts[i] if i < len(ext_parts) else None
|
|
576
|
+
if value is not None and name in EXT_INT_FIELDS:
|
|
577
|
+
with suppress(ValueError):
|
|
578
|
+
value = int(value)
|
|
579
|
+
ext_dict[name] = value
|
|
580
|
+
data["ext"] = ext_dict
|
|
581
|
+
|
|
582
|
+
except json.JSONDecodeError as err:
|
|
583
|
+
# Stop the client on malformed payloads as a defensive measure,
|
|
584
|
+
# mirroring previous behaviour.
|
|
585
|
+
self.stop()
|
|
586
|
+
raise PyEzvizError(f"Unable to decode MQTT message: {err}") from err
|
|
587
|
+
|
|
588
|
+
return data
|
pyezvizapi/test_mqtt.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""MQTT test module."""
|
|
2
|
+
|
|
3
|
+
from getpass import getpass
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .client import EzvizClient # Your login client
|
|
11
|
+
from .mqtt import MQTTClient # The refactored MQTT class
|
|
12
|
+
|
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
|
14
|
+
|
|
15
|
+
LOG_FILE = Path("mqtt_messages.jsonl") # JSON Lines format
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def message_handler(msg: dict[str, Any]) -> None:
|
|
19
|
+
"""Handle new MQTT messages by printing and saving them to a file.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
msg (Dict[str, Any]): The decoded MQTT message.
|
|
23
|
+
"""
|
|
24
|
+
print("📩 New MQTT message:", msg)
|
|
25
|
+
|
|
26
|
+
# Append to JSONL file
|
|
27
|
+
with LOG_FILE.open("a", encoding="utf-8") as f:
|
|
28
|
+
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
"""Entry point for testing MQTT messages.
|
|
33
|
+
|
|
34
|
+
Prompts for username and password, logs into Ezviz, starts MQTT listener,
|
|
35
|
+
and writes incoming messages to a JSONL file.
|
|
36
|
+
"""
|
|
37
|
+
# Prompt for credentials
|
|
38
|
+
username = input("Ezviz username: ")
|
|
39
|
+
password = getpass("Ezviz password: ")
|
|
40
|
+
|
|
41
|
+
# Step 1: Log into Ezviz to get a token
|
|
42
|
+
client = EzvizClient(account=username, password=password)
|
|
43
|
+
client.login()
|
|
44
|
+
|
|
45
|
+
# Step 2: Start MQTT client
|
|
46
|
+
mqtt_client: MQTTClient = client.get_mqtt_client(
|
|
47
|
+
on_message_callback=message_handler
|
|
48
|
+
)
|
|
49
|
+
mqtt_client.connect()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
print("Listening for MQTT messages... (Ctrl+C to quit)")
|
|
53
|
+
while True:
|
|
54
|
+
time.sleep(1) # Keep process alive
|
|
55
|
+
except KeyboardInterrupt:
|
|
56
|
+
print("\nStopping...")
|
|
57
|
+
finally:
|
|
58
|
+
mqtt_client.stop()
|
|
59
|
+
print("Stopped.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
64
|
+
|
pyezvizapi/utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from hashlib import md5
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
from typing import Any
|
|
9
|
+
import uuid
|
|
9
10
|
|
|
10
11
|
from Crypto.Cipher import AES
|
|
11
12
|
|
|
@@ -61,10 +62,12 @@ def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
|
|
|
61
62
|
try:
|
|
62
63
|
for key in keys:
|
|
63
64
|
data = data[key]
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
except (KeyError, TypeError):
|
|
66
67
|
return default_value
|
|
67
68
|
|
|
69
|
+
return data
|
|
70
|
+
|
|
68
71
|
|
|
69
72
|
def decrypt_image(input_data: bytes, password: str) -> bytes:
|
|
70
73
|
"""Decrypts image data with provided password.
|
|
@@ -162,3 +165,27 @@ def deep_merge(dict1: Any, dict2: Any) -> Any:
|
|
|
162
165
|
merged[key] = dict2[key]
|
|
163
166
|
|
|
164
167
|
return merged
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def generate_unique_code() -> str:
|
|
171
|
+
"""Generate a deterministic, platform-agnostic unique code for the current host.
|
|
172
|
+
|
|
173
|
+
This function retrieves the host's MAC address using Python's standard
|
|
174
|
+
`uuid.getnode()` (works on Windows, Linux, macOS), converts it to a
|
|
175
|
+
canonical string representation, and then hashes it using MD5 to produce
|
|
176
|
+
a fixed-length hexadecimal string.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
str: A 32-character hexadecimal string uniquely representing
|
|
180
|
+
the host's MAC address. For example:
|
|
181
|
+
'a94e6756hghjgfghg49e0f310d9e44a'.
|
|
182
|
+
|
|
183
|
+
Notes:
|
|
184
|
+
- The output is deterministic: the same machine returns the same code.
|
|
185
|
+
- If the MAC address changes (e.g., different network adapter),
|
|
186
|
+
the output will change.
|
|
187
|
+
- MD5 is used here only for ID generation, not for security.
|
|
188
|
+
"""
|
|
189
|
+
mac_int = uuid.getnode()
|
|
190
|
+
mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
|
|
191
|
+
return md5(mac_str.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pyezvizapi/__init__.py,sha256=ByMIDi_hdJa7Sc7GoJmviTOBe4DILx2E4UzhEpEzQis,1422
|
|
2
|
+
pyezvizapi/__main__.py,sha256=OsawbYa-eoLUTOMr4xWeryq4sDzs09WL9GELKQTbda8,16396
|
|
3
|
+
pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
|
|
4
|
+
pyezvizapi/camera.py,sha256=BpmLySbnWWEKimSwgjx_cB60Q-6dbgdY-NA--NzUvps,11486
|
|
5
|
+
pyezvizapi/cas.py,sha256=d31ZflYSD9P40MnsNRNZbT0HVLvlnHokKpLbdAjWQ74,5631
|
|
6
|
+
pyezvizapi/client.py,sha256=uhtQ4bdM0zmnAPEt3cvxGtrqLFf0C15A5zIBE719pDE,86179
|
|
7
|
+
pyezvizapi/constants.py,sha256=ntH7gNNRHQ66Dek2Uhk9PD4wXT0h7QTDn929LFGSP9I,12333
|
|
8
|
+
pyezvizapi/exceptions.py,sha256=28lLyM0ILTRHgWqr9D-DqqKFXx7POuF0WAZctdC8Kbc,735
|
|
9
|
+
pyezvizapi/light_bulb.py,sha256=ADLrPZ6NL4vANzmohU63QuD9qVGkKHkX9C0o7Evbv-A,5730
|
|
10
|
+
pyezvizapi/mqtt.py,sha256=9i2dkVuNgf9KB2b-58HqHuKIzl-Ouuodg0dJ0DYpUOo,21649
|
|
11
|
+
pyezvizapi/test_cam_rtsp.py,sha256=w7GPcYIeK78TxL8zFDihdGSDQNWcYrurwZOr6uFzzgo,4902
|
|
12
|
+
pyezvizapi/test_mqtt.py,sha256=6thgcsfvl-wSR2Xrp8miG7PHfg9Q-a4AZ8VvOhEsBBQ,1651
|
|
13
|
+
pyezvizapi/utils.py,sha256=zjbvQiJ_Q-qwbB_FImXVjuaRct9cogBRKDxzFHbx4ek,5940
|
|
14
|
+
pyezvizapi-1.0.1.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
15
|
+
pyezvizapi-1.0.1.7.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
+
pyezvizapi-1.0.1.7.dist-info/METADATA,sha256=e4GDIcD5BMofWTvFXl6EIUpF-2vrRGp1n9VTMZCOKPs,694
|
|
17
|
+
pyezvizapi-1.0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
pyezvizapi-1.0.1.7.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
19
|
+
pyezvizapi-1.0.1.7.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
20
|
+
pyezvizapi-1.0.1.7.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
pyezvizapi/__init__.py,sha256=yWITfZOi5zvKblfaeB2ft-wXYmZ2NviYVq_tsK8YkSk,1334
|
|
2
|
-
pyezvizapi/__main__.py,sha256=OsawbYa-eoLUTOMr4xWeryq4sDzs09WL9GELKQTbda8,16396
|
|
3
|
-
pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
|
|
4
|
-
pyezvizapi/camera.py,sha256=BpmLySbnWWEKimSwgjx_cB60Q-6dbgdY-NA--NzUvps,11486
|
|
5
|
-
pyezvizapi/cas.py,sha256=d31ZflYSD9P40MnsNRNZbT0HVLvlnHokKpLbdAjWQ74,5631
|
|
6
|
-
pyezvizapi/client.py,sha256=6NfWGAYalInpIlmsbraEK_CUyR1CMGIw4jqhKQUfkk8,85561
|
|
7
|
-
pyezvizapi/constants.py,sha256=jjLO-Ne9jq9m9_giYB4rnPXDZKkzKhesVXBqP1B3-00,12304
|
|
8
|
-
pyezvizapi/exceptions.py,sha256=28lLyM0ILTRHgWqr9D-DqqKFXx7POuF0WAZctdC8Kbc,735
|
|
9
|
-
pyezvizapi/light_bulb.py,sha256=ADLrPZ6NL4vANzmohU63QuD9qVGkKHkX9C0o7Evbv-A,5730
|
|
10
|
-
pyezvizapi/mqtt.py,sha256=Y4X99Z0Avm32SE8vog7CNsv6tGUPmPYUZUgPDGS0QJA,7866
|
|
11
|
-
pyezvizapi/test_cam_rtsp.py,sha256=w7GPcYIeK78TxL8zFDihdGSDQNWcYrurwZOr6uFzzgo,4902
|
|
12
|
-
pyezvizapi/utils.py,sha256=5J10o3h-y8prWDvl3LSAF-9wS1jBgBMg5cpAEebcuSM,4936
|
|
13
|
-
pyezvizapi-1.0.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
14
|
-
pyezvizapi-1.0.1.5.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
15
|
-
pyezvizapi-1.0.1.5.dist-info/METADATA,sha256=ZHsHhFN88VbuD53CDJcFslvEzDTR_TQaDAlHf9PKVhc,694
|
|
16
|
-
pyezvizapi-1.0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
-
pyezvizapi-1.0.1.5.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
18
|
-
pyezvizapi-1.0.1.5.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
19
|
-
pyezvizapi-1.0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|