pyezvizapi 1.0.3.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/feature.py CHANGED
@@ -2,26 +2,64 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping, MutableMapping
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]:
@@ -78,6 +116,227 @@ def optionals_mapping(camera_data: Mapping[str, Any]) -> dict[str, Any]:
78
116
  return dict(optionals) if isinstance(optionals, Mapping) else {}
79
117
 
80
118
 
119
+ def optionals_dict(camera_data: Mapping[str, Any]) -> dict[str, Any]:
120
+ """Return convenience wrapper for optionals mapping."""
121
+
122
+ return optionals_mapping(camera_data)
123
+
124
+
125
+ def custom_voice_volume_config(camera_data: Mapping[str, Any]) -> dict[str, int] | None:
126
+ """Return current CustomVoice volume configuration."""
127
+
128
+ optionals = optionals_mapping(camera_data)
129
+ config = optionals.get("CustomVoice_Volume")
130
+ config = decode_json(config)
131
+ if not isinstance(config, Mapping):
132
+ return None
133
+
134
+ volume = coerce_int(config.get("volume"))
135
+ mic = coerce_int(config.get("microphone_volume"))
136
+ result: dict[str, int] = {}
137
+ if isinstance(volume, int):
138
+ result["volume"] = volume
139
+ if isinstance(mic, int):
140
+ result["microphone_volume"] = mic
141
+ return result or None
142
+
143
+
144
+ def iter_algorithm_entries(camera_data: Mapping[str, Any]) -> Iterator[dict[str, Any]]:
145
+ """Yield entries from the AlgorithmInfo optionals list."""
146
+
147
+ entries = optionals_dict(camera_data).get("AlgorithmInfo")
148
+ if not isinstance(entries, Iterable):
149
+ return
150
+ for entry in entries:
151
+ if isinstance(entry, Mapping):
152
+ yield dict(entry)
153
+
154
+
155
+ def iter_channel_algorithm_entries(
156
+ camera_data: Mapping[str, Any], channel: int
157
+ ) -> Iterator[dict[str, Any]]:
158
+ """Yield AlgorithmInfo entries filtered by channel."""
159
+
160
+ for entry in iter_algorithm_entries(camera_data):
161
+ entry_channel = coerce_int(entry.get("channel")) or 1
162
+ if entry_channel == channel:
163
+ yield entry
164
+
165
+
166
+ def get_algorithm_value(
167
+ camera_data: Mapping[str, Any], subtype: str, channel: int
168
+ ) -> int | None:
169
+ """Return AlgorithmInfo value for provided subtype/channel."""
170
+
171
+ for entry in iter_channel_algorithm_entries(camera_data, channel):
172
+ if entry.get("SubType") != subtype:
173
+ continue
174
+ return coerce_int(entry.get("Value"))
175
+ return None
176
+
177
+
178
+ def has_algorithm_subtype(
179
+ camera_data: Mapping[str, Any], subtype: str, channel: int = 1
180
+ ) -> bool:
181
+ """Return True when AlgorithmInfo contains subtype for channel."""
182
+
183
+ return get_algorithm_value(camera_data, subtype, channel) is not None
184
+
185
+
186
+ def support_ext_value(camera_data: Mapping[str, Any], ext_key: str) -> str | None:
187
+ """Fetch a supportExt entry as a string when present."""
188
+
189
+ raw = camera_data.get("supportExt")
190
+ if not isinstance(raw, Mapping):
191
+ device_infos = camera_data.get("deviceInfos")
192
+ if isinstance(device_infos, Mapping):
193
+ raw = device_infos.get("supportExt")
194
+
195
+ if not isinstance(raw, Mapping):
196
+ return None
197
+
198
+ value = raw.get(ext_key)
199
+ return str(value) if value is not None else None
200
+
201
+
202
+ def _normalize_port_list(value: Any) -> list[dict[str, Any]] | None:
203
+ """Decode a list of port-security entries."""
204
+
205
+ value = decode_json(value)
206
+ if not isinstance(value, Iterable):
207
+ return None
208
+
209
+ normalized: list[dict[str, Any]] = []
210
+ for raw_entry in value:
211
+ entry = decode_json(raw_entry)
212
+ if not isinstance(entry, Mapping):
213
+ return None
214
+ port = coerce_int(entry.get("portNo"))
215
+ if port is None:
216
+ continue
217
+ normalized.append({"portNo": port, "enabled": bool(entry.get("enabled"))})
218
+
219
+ return normalized
220
+
221
+
222
+ def normalize_port_security(payload: Any) -> dict[str, Any]:
223
+ """Normalize IoT port-security payloads."""
224
+
225
+ seen: set[int] = set()
226
+
227
+ def _apply_hint(
228
+ candidate: dict[str, Any] | None, hint_value: bool | None
229
+ ) -> dict[str, Any] | None:
230
+ if (
231
+ candidate is not None
232
+ and "enabled" not in candidate
233
+ and isinstance(hint_value, bool)
234
+ ):
235
+ candidate["enabled"] = hint_value
236
+ return candidate
237
+
238
+ def _walk_mapping(obj: Mapping[str, Any], hint: bool | None) -> dict[str, Any] | None:
239
+ obj_id = id(obj)
240
+ if obj_id in seen:
241
+ return None
242
+ seen.add(obj_id)
243
+
244
+ enabled_local = obj.get("enabled")
245
+ if isinstance(enabled_local, bool):
246
+ hint = enabled_local
247
+
248
+ ports = _normalize_port_list(obj.get("portSecurityList"))
249
+ if ports is not None:
250
+ return {
251
+ "portSecurityList": ports,
252
+ "enabled": bool(enabled_local)
253
+ if isinstance(enabled_local, bool)
254
+ else bool(hint)
255
+ if isinstance(hint, bool)
256
+ else True,
257
+ }
258
+
259
+ for key in ("PortSecurity", "value", "data", "NetworkSecurityProtection"):
260
+ if key in obj:
261
+ candidate = _apply_hint(_walk(obj[key], hint), hint)
262
+ if candidate:
263
+ return candidate
264
+
265
+ for value in obj.values():
266
+ candidate = _apply_hint(_walk(value, hint), hint)
267
+ if candidate:
268
+ return candidate
269
+
270
+ return None
271
+
272
+ def _walk_iterable(values: Iterable[Any], hint: bool | None) -> dict[str, Any] | None:
273
+ for item in values:
274
+ candidate = _walk(item, hint)
275
+ if candidate:
276
+ return candidate
277
+ return None
278
+
279
+ def _walk(obj: Any, hint: bool | None = None) -> dict[str, Any] | None:
280
+ obj = decode_json(obj)
281
+ if obj is None:
282
+ return None
283
+
284
+ if isinstance(obj, Mapping):
285
+ return _walk_mapping(obj, hint)
286
+
287
+ if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
288
+ return _walk_iterable(obj, hint)
289
+
290
+ return None
291
+
292
+ normalized = _walk(payload)
293
+ if isinstance(normalized, dict):
294
+ normalized.setdefault("enabled", True)
295
+ return normalized
296
+ return {}
297
+
298
+
299
+ def port_security_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
300
+ """Return the normalized port-security mapping for a camera payload."""
301
+
302
+ direct = camera_data.get("NetworkSecurityProtection")
303
+ normalized = normalize_port_security(direct)
304
+ if normalized:
305
+ return normalized
306
+
307
+ feature = camera_data.get("FEATURE_INFO")
308
+ if isinstance(feature, Mapping):
309
+ normalized = normalize_port_security(feature)
310
+ if normalized:
311
+ return normalized
312
+
313
+ return {}
314
+
315
+
316
+ def port_security_has_port(camera_data: Mapping[str, Any], port: int) -> bool:
317
+ """Return True if the normalized config contains the port."""
318
+
319
+ ports = port_security_config(camera_data).get("portSecurityList")
320
+ if not isinstance(ports, Iterable):
321
+ return False
322
+ return any(
323
+ isinstance(entry, Mapping) and coerce_int(entry.get("portNo")) == port
324
+ for entry in ports
325
+ )
326
+
327
+
328
+ def port_security_port_enabled(camera_data: Mapping[str, Any], port: int) -> bool:
329
+ """Return True if the specific port is enabled."""
330
+
331
+ ports = port_security_config(camera_data).get("portSecurityList")
332
+ if not isinstance(ports, Iterable):
333
+ return False
334
+ for entry in ports:
335
+ if isinstance(entry, Mapping) and coerce_int(entry.get("portNo")) == port:
336
+ return bool(entry.get("enabled"))
337
+ return False
338
+
339
+
81
340
  def display_mode_value(camera_data: Mapping[str, Any]) -> int:
82
341
  """Return display mode value (1..3) from camera data."""
83
342
 
@@ -85,10 +344,9 @@ def display_mode_value(camera_data: Mapping[str, Any]) -> int:
85
344
  display_mode = optionals.get("display_mode")
86
345
  display_mode = decode_json(display_mode)
87
346
 
88
- if isinstance(display_mode, Mapping):
89
- mode = display_mode.get("mode")
90
- else:
91
- mode = display_mode
347
+ mode = (
348
+ display_mode.get("mode") if isinstance(display_mode, Mapping) else display_mode
349
+ )
92
350
 
93
351
  if isinstance(mode, int) and mode in (1, 2, 3):
94
352
  return mode
@@ -96,6 +354,33 @@ def display_mode_value(camera_data: Mapping[str, Any]) -> int:
96
354
  return 1
97
355
 
98
356
 
357
+ def blc_current_value(camera_data: Mapping[str, Any]) -> int:
358
+ """Return BLC position (0..5) from camera data. 0 = Off."""
359
+ optionals = optionals_mapping(camera_data)
360
+ inverse_mode = optionals.get("inverse_mode")
361
+ inverse_mode = decode_json(inverse_mode)
362
+
363
+ # Expected: {"mode": int, "enable": 0|1, "position": 0..5}
364
+ if isinstance(inverse_mode, Mapping):
365
+ enable = inverse_mode.get("enable", 0)
366
+ position = inverse_mode.get("position", 0)
367
+ if (
368
+ isinstance(enable, int)
369
+ and enable == 1
370
+ and isinstance(position, int)
371
+ and position in (1, 2, 3, 4, 5)
372
+ ):
373
+ return position
374
+ return 0
375
+
376
+ # Fallbacks if backend ever returns a bare int (position) instead of the object
377
+ if isinstance(inverse_mode, int) and inverse_mode in (0, 1, 2, 3, 4, 5):
378
+ return inverse_mode
379
+
380
+ # Default to Off
381
+ return 0
382
+
383
+
99
384
  def device_icr_dss_config(camera_data: Mapping[str, Any]) -> dict[str, Any]:
100
385
  """Decode and return the device_ICR_DSS configuration."""
101
386
 
pyezvizapi/light_bulb.py CHANGED
@@ -176,7 +176,7 @@ class EzvizLightBulb:
176
176
  def set_brightness(self, value: int) -> bool:
177
177
  """Set the light bulb brightness.
178
178
 
179
- The value must be in range 1100. Returns True on success.
179
+ The value must be in range 1-100. Returns True on success.
180
180
 
181
181
  Raises:
182
182
  PyEzvizError: On API failures.
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."""
@@ -110,15 +113,14 @@ class TestRTSPAuth:
110
113
  describe = genmsg_describe(
111
114
  url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
112
115
  )
113
- print(describe)
116
+ _LOGGER.debug("RTSP DESCRIBE (basic):\n%s", describe)
114
117
  session.send(describe.encode())
115
118
  msg1: bytes = session.recv(self._rtsp_details["bufLen"])
116
119
  seq += 1
117
120
 
118
121
  decoded = msg1.decode()
119
122
  if "200 OK" in decoded:
120
- print(f"Basic auth result: {decoded}")
121
- print("Basic Auth test passed. Credentials Valid!")
123
+ _LOGGER.info("Basic auth result: %s", decoded)
122
124
  return
123
125
 
124
126
  if "Unauthorized" in decoded:
@@ -140,20 +142,16 @@ class TestRTSPAuth:
140
142
  describe = genmsg_describe(
141
143
  url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
142
144
  )
143
- print(describe)
145
+ _LOGGER.debug("RTSP DESCRIBE (digest):\n%s", describe)
144
146
  session.send(describe.encode())
145
147
  msg1 = session.recv(self._rtsp_details["bufLen"])
146
148
  decoded = msg1.decode()
147
- print(f"Digest auth result: {decoded}")
149
+ _LOGGER.info("Digest auth result: %s", decoded)
148
150
 
149
151
  if "200 OK" in decoded:
150
- print("Digest Auth test Passed. Credentials Valid!")
151
152
  return
152
153
 
153
154
  if "401 Unauthorized" in decoded:
154
155
  raise AuthTestResultFailed("Credentials not valid!!")
155
156
 
156
- print("Basic Auth test passed. Credentials Valid!")
157
-
158
-
159
- # 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