pyezvizapi 1.0.2.3__py3-none-any.whl → 1.0.4.3__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.
pyezvizapi/mqtt.py CHANGED
@@ -83,27 +83,27 @@ class MqttData(TypedDict):
83
83
  # Payload decoding helpers
84
84
  # ---------------------------------------------------------------------------
85
85
 
86
- # Field names in the commaseparated ``ext`` payload from EZVIZ.
86
+ # Field names in the comma-separated ``ext`` payload from EZVIZ.
87
87
  EXT_FIELD_NAMES: Final[tuple[str, ...]] = (
88
88
  "channel_type",
89
89
  "time",
90
90
  "device_serial",
91
91
  "channel_no",
92
92
  "alert_type_code",
93
- "unused1",
94
- "unused2",
95
- "unused3",
96
- "unused4",
93
+ "default_pic_url",
94
+ "media_url_alt1",
95
+ "media_url_alt2",
96
+ "resource_type",
97
97
  "status_flag",
98
98
  "file_id",
99
99
  "is_encrypted",
100
100
  "picChecksum",
101
- "unknown_flag",
102
- "unused5",
101
+ "is_dev_video",
102
+ "metadata",
103
103
  "msgId",
104
104
  "image",
105
105
  "device_name",
106
- "unused6",
106
+ "reserved",
107
107
  "sequence_number",
108
108
  )
109
109
 
@@ -113,8 +113,10 @@ EXT_INT_FIELDS: Final[frozenset[str]] = frozenset(
113
113
  "channel_type",
114
114
  "channel_no",
115
115
  "alert_type_code",
116
+ "resource_type",
116
117
  "status_flag",
117
118
  "is_encrypted",
119
+ "is_dev_video",
118
120
  "sequence_number",
119
121
  }
120
122
  )
@@ -247,7 +249,7 @@ class MQTTClient:
247
249
  # Stop background thread and disconnect
248
250
  self.mqtt_client.loop_stop()
249
251
  self.mqtt_client.disconnect()
250
- except Exception as err: # noqa: BLE001
252
+ except (OSError, ValueError, RuntimeError) as err:
251
253
  _LOGGER.debug("MQTT disconnect failed: %s", err)
252
254
  # Always attempt to stop push on server side
253
255
  self._stop_ezviz_push()
@@ -535,13 +537,17 @@ class MQTTClient:
535
537
  """
536
538
  broker = self._mqtt_data["push_url"]
537
539
 
538
- self.mqtt_client = mqtt.Client(
539
- callback_api_version=mqtt.CallbackAPIVersion.VERSION1,
540
- client_id=self._mqtt_data["mqtt_clientid"],
541
- clean_session=clean_session,
542
- protocol=mqtt.MQTTv311,
543
- transport="tcp",
544
- )
540
+ client_kwargs: dict[str, Any] = {
541
+ "client_id": self._mqtt_data["mqtt_clientid"],
542
+ "clean_session": clean_session,
543
+ "protocol": mqtt.MQTTv311,
544
+ "transport": "tcp",
545
+ }
546
+ callback_api_version = getattr(mqtt, "CallbackAPIVersion", None)
547
+ if callback_api_version is not None:
548
+ client_kwargs["callback_api_version"] = callback_api_version.VERSION1
549
+
550
+ self.mqtt_client = mqtt.Client(**client_kwargs)
545
551
 
546
552
  # Bind callbacks
547
553
  self.mqtt_client.on_connect = self._on_connect
@@ -4,11 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import hashlib
7
+ import logging
7
8
  import socket
8
9
  from typing import TypedDict
9
10
 
10
11
  from .exceptions import AuthTestResultFailed, InvalidHost
11
12
 
13
+ _LOGGER = logging.getLogger(__name__)
14
+
12
15
 
13
16
  def genmsg_describe(url: str, seq: int, user_agent: str, auth_seq: str) -> str:
14
17
  """Generate RTSP DESCRIBE request message."""
@@ -22,6 +25,7 @@ def genmsg_describe(url: str, seq: int, user_agent: str, auth_seq: str) -> str:
22
25
 
23
26
  class RTSPDetails(TypedDict):
24
27
  """Typed structure for RTSP test parameters."""
28
+
25
29
  bufLen: int
26
30
  defaultServerIp: str
27
31
  defaultServerPort: int
@@ -37,7 +41,11 @@ class TestRTSPAuth:
37
41
  _rtsp_details: RTSPDetails
38
42
 
39
43
  def __init__(
40
- self, ip_addr: str, username: str | None = None, password: str | None = None, test_uri: str = ""
44
+ self,
45
+ ip_addr: str,
46
+ username: str | None = None,
47
+ password: str | None = None,
48
+ test_uri: str = "",
41
49
  ) -> None:
42
50
  """Initialize RTSP credential test."""
43
51
  self._rtsp_details = RTSPDetails(
@@ -50,7 +58,9 @@ class TestRTSPAuth:
50
58
  defaultPassword=password,
51
59
  )
52
60
 
53
- def generate_auth_string(self, realm: bytes, method: str, uri: str, nonce: bytes) -> str:
61
+ def generate_auth_string(
62
+ self, realm: bytes, method: str, uri: str, nonce: bytes
63
+ ) -> str:
54
64
  """Generate the HTTP Digest Authorization header value."""
55
65
  m_1 = hashlib.md5(
56
66
  f"{self._rtsp_details['defaultUsername']}:{realm.decode()}:{self._rtsp_details['defaultPassword']}".encode()
@@ -60,12 +70,12 @@ class TestRTSPAuth:
60
70
 
61
71
  return (
62
72
  "Digest "
63
- f"username=\"{self._rtsp_details['defaultUsername']}\", "
64
- f"realm=\"{realm.decode()}\", "
73
+ f'username="{self._rtsp_details["defaultUsername"]}", '
74
+ f'realm="{realm.decode()}", '
65
75
  'algorithm="MD5", '
66
- f"nonce=\"{nonce.decode()}\", "
67
- f"uri=\"{uri}\", "
68
- f"response=\"{response}\""
76
+ f'nonce="{nonce.decode()}", '
77
+ f'uri="{uri}", '
78
+ f'response="{response}"'
69
79
  )
70
80
 
71
81
  def main(self) -> None:
@@ -74,7 +84,10 @@ class TestRTSPAuth:
74
84
 
75
85
  try:
76
86
  session.connect(
77
- (self._rtsp_details["defaultServerIp"], self._rtsp_details["defaultServerPort"])
87
+ (
88
+ self._rtsp_details["defaultServerIp"],
89
+ self._rtsp_details["defaultServerPort"],
90
+ )
78
91
  )
79
92
  except TimeoutError as err:
80
93
  raise AuthTestResultFailed("Invalid ip or camera hibernating") from err
@@ -83,24 +96,31 @@ class TestRTSPAuth:
83
96
 
84
97
  seq: int = 1
85
98
 
86
- url: str = "rtsp://" + self._rtsp_details["defaultServerIp"] + self._rtsp_details["defaultTestUri"]
99
+ url: str = (
100
+ "rtsp://"
101
+ + self._rtsp_details["defaultServerIp"]
102
+ + self._rtsp_details["defaultTestUri"]
103
+ )
87
104
 
88
105
  # Basic Authorization header
89
106
  auth_b64: bytes = base64.b64encode(
90
- f"{self._rtsp_details['defaultUsername']}:{self._rtsp_details['defaultPassword']}".encode("ascii")
107
+ f"{self._rtsp_details['defaultUsername']}:{self._rtsp_details['defaultPassword']}".encode(
108
+ "ascii"
109
+ )
91
110
  )
92
111
  auth_seq: str = "Basic " + auth_b64.decode()
93
112
 
94
- describe = genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
95
- print(describe)
113
+ describe = genmsg_describe(
114
+ url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
115
+ )
116
+ _LOGGER.debug("RTSP DESCRIBE (basic):\n%s", describe)
96
117
  session.send(describe.encode())
97
118
  msg1: bytes = session.recv(self._rtsp_details["bufLen"])
98
119
  seq += 1
99
120
 
100
121
  decoded = msg1.decode()
101
122
  if "200 OK" in decoded:
102
- print(f"Basic auth result: {decoded}")
103
- print("Basic Auth test passed. Credentials Valid!")
123
+ _LOGGER.info("Basic auth result: %s", decoded)
104
124
  return
105
125
 
106
126
  if "Unauthorized" in decoded:
@@ -115,21 +135,23 @@ class TestRTSPAuth:
115
135
  end = decoded.find('"', begin + 1)
116
136
  nonce: bytes = msg1[begin + 1 : end]
117
137
 
118
- auth_seq = self.generate_auth_string(realm, "DESCRIBE", self._rtsp_details["defaultTestUri"], nonce)
138
+ auth_seq = self.generate_auth_string(
139
+ realm, "DESCRIBE", self._rtsp_details["defaultTestUri"], nonce
140
+ )
119
141
 
120
- describe = genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
121
- print(describe)
142
+ describe = genmsg_describe(
143
+ url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
144
+ )
145
+ _LOGGER.debug("RTSP DESCRIBE (digest):\n%s", describe)
122
146
  session.send(describe.encode())
123
147
  msg1 = session.recv(self._rtsp_details["bufLen"])
124
148
  decoded = msg1.decode()
125
- print(f"Digest auth result: {decoded}")
149
+ _LOGGER.info("Digest auth result: %s", decoded)
126
150
 
127
151
  if "200 OK" in decoded:
128
- print("Digest Auth test Passed. Credentials Valid!")
129
152
  return
130
153
 
131
154
  if "401 Unauthorized" in decoded:
132
155
  raise AuthTestResultFailed("Credentials not valid!!")
133
156
 
134
- print("Basic Auth test passed. Credentials Valid!")
135
- # ruff: noqa: T201
157
+ _LOGGER.info("Basic Auth test passed. Credentials Valid!")
pyezvizapi/test_mqtt.py CHANGED
@@ -8,6 +8,7 @@ with MFA similar to the main CLI.
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
+ import base64
11
12
  from getpass import getpass
12
13
  import json
13
14
  import logging
@@ -18,19 +19,60 @@ from typing import Any, cast
18
19
 
19
20
  from .client import EzvizClient
20
21
  from .exceptions import EzvizAuthVerificationCode, PyEzvizError
22
+ from .mqtt import MQTTClient
21
23
 
22
24
  logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
25
+ _LOGGER = logging.getLogger(__name__)
23
26
 
24
27
  LOG_FILE = Path("mqtt_messages.jsonl") # JSON Lines format
28
+ RAW_LOG_FILE = Path("mqtt_raw_messages.jsonl")
25
29
 
26
30
 
27
31
  def message_handler(msg: dict[str, Any]) -> None:
28
32
  """Handle new MQTT messages by printing and saving them to a file."""
29
- print("📩 New MQTT message:", msg)
33
+ _LOGGER.info("📩 New MQTT message: %s", msg)
30
34
  with LOG_FILE.open("a", encoding="utf-8") as f:
31
35
  f.write(json.dumps(msg, ensure_ascii=False) + "\n")
32
36
 
33
37
 
38
+ def _log_raw_payload(payload: bytes) -> None:
39
+ """Persist the raw MQTT payload to a log file for debugging."""
40
+ entry: dict[str, Any]
41
+ try:
42
+ decoded = payload.decode("utf-8")
43
+ entry = {"encoding": "utf-8", "payload": decoded}
44
+ except UnicodeDecodeError:
45
+ entry = {
46
+ "encoding": "base64",
47
+ "payload": base64.b64encode(payload).decode("ascii"),
48
+ }
49
+
50
+ entry["timestamp"] = time.time()
51
+ _LOGGER.info("🧾 Raw MQTT payload (%s): %s", entry["encoding"], entry["payload"])
52
+ with RAW_LOG_FILE.open("a", encoding="utf-8") as f:
53
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
54
+
55
+
56
+ def _enable_raw_logging(mqtt_client: MQTTClient) -> None:
57
+ """Wrap the internal paho-mqtt callback to capture raw payloads."""
58
+ if getattr(mqtt_client, "_raw_logging_enabled", False):
59
+ return
60
+ paho_client = getattr(mqtt_client, "mqtt_client", None)
61
+ if paho_client is None:
62
+ _LOGGER.warning("Unable to enable raw logging: MQTT client not configured yet")
63
+ return
64
+
65
+ original_on_message = paho_client.on_message
66
+
67
+ def _raw_logging_wrapper(client: Any, userdata: Any, msg: Any) -> None:
68
+ _log_raw_payload(getattr(msg, "payload", b""))
69
+ if original_on_message:
70
+ original_on_message(client, userdata, msg)
71
+
72
+ paho_client.on_message = _raw_logging_wrapper
73
+ mqtt_client._raw_logging_enabled = True # type: ignore[attr-defined] # noqa: SLF001
74
+
75
+
34
76
  def _load_token_file(path: str | None) -> dict[str, Any] | None:
35
77
  if not path:
36
78
  return None
@@ -40,7 +82,7 @@ def _load_token_file(path: str | None) -> dict[str, Any] | None:
40
82
  try:
41
83
  return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
42
84
  except (OSError, json.JSONDecodeError):
43
- logging.getLogger(__name__).warning("Failed to read token file: %s", p)
85
+ _LOGGER.warning("Failed to read token file: %s", p)
44
86
  return None
45
87
 
46
88
 
@@ -50,9 +92,9 @@ def _save_token_file(path: str | None, token: dict[str, Any]) -> None:
50
92
  p = Path(path)
51
93
  try:
52
94
  p.write_text(json.dumps(token, indent=2), encoding="utf-8")
53
- logging.getLogger(__name__).info("Saved token to %s", p)
95
+ _LOGGER.info("Saved token to %s", p)
54
96
  except OSError:
55
- logging.getLogger(__name__).warning("Failed to save token file: %s", p)
97
+ _LOGGER.warning("Failed to save token file: %s", p)
56
98
 
57
99
 
58
100
  def main(argv: list[str] | None = None) -> int:
@@ -87,7 +129,7 @@ def main(argv: list[str] | None = None) -> int:
87
129
 
88
130
  # If no token and missing username/password, prompt interactively
89
131
  if not token and (not username or not password):
90
- print("No token found. Please enter Ezviz credentials.")
132
+ _LOGGER.info("No token found. Please enter Ezviz credentials")
91
133
  if not username:
92
134
  username = input("Username: ")
93
135
  if not password:
@@ -107,29 +149,29 @@ def main(argv: list[str] | None = None) -> int:
107
149
  code_int = None
108
150
  client.login(sms_code=code_int)
109
151
  except PyEzvizError as exp:
110
- print(f"Login failed: {exp}")
152
+ _LOGGER.error("Login failed: %s", exp)
111
153
  return 1
112
154
 
113
155
  # Start MQTT client
114
156
  mqtt_client = client.get_mqtt_client(on_message_callback=message_handler)
115
157
  mqtt_client.connect()
158
+ _enable_raw_logging(mqtt_client)
116
159
 
117
160
  try:
118
- print("Listening for MQTT messages... (Ctrl+C to quit)")
161
+ _LOGGER.info("Listening for MQTT messages... (Ctrl+C to quit)")
119
162
  while True:
120
163
  time.sleep(1)
121
164
  except KeyboardInterrupt:
122
- print("\nStopping...")
165
+ _LOGGER.info("Stopping listener (keyboard interrupt)")
123
166
  finally:
124
167
  mqtt_client.stop()
125
- print("Stopped.")
168
+ _LOGGER.info("Listener stopped")
126
169
 
127
170
  if args.save_token and args.token_file:
128
- _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
171
+ _save_token_file(args.token_file, client.export_token())
129
172
 
130
173
  return 0
131
174
 
132
175
 
133
176
  if __name__ == "__main__":
134
177
  sys.exit(main())
135
- # ruff: noqa: T201