pyezvizapi 1.0.1.7__py3-none-any.whl → 1.0.1.9__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/__main__.py CHANGED
@@ -1,25 +1,47 @@
1
- """pyezvizapi command line."""
1
+ """pyezvizapi command line.
2
+
3
+ Small utility CLI for testing and scripting Ezviz operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
2
8
  import argparse
3
9
  import json
4
10
  import logging
11
+ from pathlib import Path
5
12
  import sys
6
- from typing import Any
13
+ from typing import Any, cast
7
14
 
8
15
  import pandas as pd
9
16
 
10
17
  from .camera import EzvizCamera
11
18
  from .client import EzvizClient
12
- from .constants import BatteryCameraWorkMode, DefenseModeType
13
- from .exceptions import EzvizAuthVerificationCode
19
+ from .constants import BatteryCameraWorkMode, DefenseModeType, DeviceSwitchType
20
+ from .exceptions import EzvizAuthVerificationCode, PyEzvizError
14
21
  from .light_bulb import EzvizLightBulb
15
- from .mqtt import MQTTClient
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ def _setup_logging(debug: bool) -> None:
27
+ """Configure root logger for CLI usage."""
28
+ level = logging.DEBUG if debug else logging.INFO
29
+ logging.basicConfig(level=level, stream=sys.stderr, format="%(levelname)s: %(message)s")
30
+ if debug:
31
+ # Verbose requests logging in debug mode
32
+ requests_log = logging.getLogger("requests.packages.urllib3")
33
+ requests_log.setLevel(logging.DEBUG)
34
+ requests_log.propagate = True
16
35
 
17
36
 
18
- def main() -> Any:
19
- """Initiate arg parser."""
37
+ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
38
+ """Build and parse CLI arguments.
39
+
40
+ Returns a populated `argparse.Namespace`. Pass `argv` for testing.
41
+ """
20
42
  parser = argparse.ArgumentParser(prog="pyezvizapi")
21
- parser.add_argument("-u", "--username", required=True, help="Ezviz username")
22
- parser.add_argument("-p", "--password", required=True, help="Ezviz Password")
43
+ parser.add_argument("-u", "--username", required=False, help="Ezviz username")
44
+ parser.add_argument("-p", "--password", required=False, help="Ezviz Password")
23
45
  parser.add_argument(
24
46
  "-r",
25
47
  "--region",
@@ -30,6 +52,20 @@ def main() -> Any:
30
52
  parser.add_argument(
31
53
  "--debug", "-d", action="store_true", help="Print debug messages to stderr"
32
54
  )
55
+ parser.add_argument(
56
+ "--json", action="store_true", help="Force JSON output when possible"
57
+ )
58
+ parser.add_argument(
59
+ "--token-file",
60
+ type=str,
61
+ default="ezviz_token.json",
62
+ help="Path to JSON token file in the current directory (default: ezviz_token.json)",
63
+ )
64
+ parser.add_argument(
65
+ "--save-token",
66
+ action="store_true",
67
+ help="Save token to --token-file after successful login",
68
+ )
33
69
 
34
70
  subparsers = parser.add_subparsers(dest="action")
35
71
 
@@ -43,6 +79,12 @@ def main() -> Any:
43
79
  help="Device action to perform",
44
80
  choices=["device", "status", "switch", "connection"],
45
81
  )
82
+ parser_device.add_argument(
83
+ "--refresh",
84
+ action=argparse.BooleanOptionalAction,
85
+ default=True,
86
+ help="Refresh alarm info before composing status (default: on)",
87
+ )
46
88
 
47
89
  parser_device_lights = subparsers.add_parser(
48
90
  "devices_light", help="Get all the light bulbs"
@@ -54,6 +96,12 @@ def main() -> Any:
54
96
  help="Light bulbs action to perform",
55
97
  choices=["status"]
56
98
  )
99
+ parser_device_lights.add_argument(
100
+ "--refresh",
101
+ action=argparse.BooleanOptionalAction,
102
+ default=True,
103
+ help="Refresh device data before composing status (default: on)",
104
+ )
57
105
 
58
106
  parser_light = subparsers.add_parser("light", help="Light actions")
59
107
  parser_light.add_argument("--serial", required=True, help="light bulb SERIAL")
@@ -77,7 +125,13 @@ def main() -> Any:
77
125
 
78
126
  subparsers_camera = parser_camera.add_subparsers(dest="camera_action")
79
127
 
80
- subparsers_camera.add_parser("status", help="Get the status of the camera")
128
+ parser_camera_status = subparsers_camera.add_parser("status", help="Get the status of the camera")
129
+ parser_camera_status.add_argument(
130
+ "--refresh",
131
+ action=argparse.BooleanOptionalAction,
132
+ default=True,
133
+ help="Refresh alarm info before composing status (default: on)",
134
+ )
81
135
  subparsers_camera.add_parser("unlock-door", help="Unlock the door lock")
82
136
  subparsers_camera.add_parser("unlock-gate", help="Unlock the gate lock")
83
137
  parser_camera_move = subparsers_camera.add_parser("move", help="Move the camera")
@@ -161,12 +215,11 @@ def main() -> Any:
161
215
  parser_camera_alarm.add_argument(
162
216
  "--do_not_disturb",
163
217
  required=False,
164
- help="\
165
- If alarm notifications are enabled in the EZViz app then movement normally causes a notification to be sent. \
166
- Enabling this feature stops these notifications, i.e. you are not to be disturbed even if movement occurs. \
167
- Care must be taken because do-not-disturb can not be reset using the mobile app. \
168
- No new notifications will be sent until do-not-disturb is disabled. \
169
- Movement is still recorded even if do-not-disturb is enabled.",
218
+ help=(
219
+ "Enable/disable push notifications for motion events. "
220
+ "Some camera models expose this setting in the EZVIZ app, but not all. "
221
+ "Motion alarms are still recorded and available even when push notifications are disabled."
222
+ ),
170
223
  default=None,
171
224
  type=int,
172
225
  choices=[0, 1],
@@ -186,293 +239,363 @@ Movement is still recorded even if do-not-disturb is enabled.",
186
239
  choices=[mode.name for mode in BatteryCameraWorkMode if mode is not BatteryCameraWorkMode.UNKNOWN],
187
240
  )
188
241
 
189
- args = parser.parse_args()
190
-
191
- # print("--------------args")
192
- # print("--------------args: %s",args)
193
- # print("--------------args")
194
-
195
- client = EzvizClient(args.username, args.password, args.region)
196
- try:
197
- client.login()
198
-
199
- except EzvizAuthVerificationCode:
200
- mfa_code = input("MFA code required, please input MFA code.\n")
201
- client.login(sms_code=mfa_code)
202
-
203
- except Exception as exp: # pylint: disable=broad-except
204
- print(exp)
205
-
206
- if args.debug:
207
- # You must initialize logging, otherwise you'll not see debug output.
208
- logging.basicConfig()
209
- logging.getLogger().setLevel(logging.DEBUG)
210
- requests_log = logging.getLogger("requests.packages.urllib3")
211
- requests_log.setLevel(logging.DEBUG)
212
- requests_log.propagate = True
213
-
214
- if args.action == "devices":
215
-
216
- if args.device_action == "device":
217
- try:
218
- print(json.dumps(client.get_device(), indent=2))
219
- except Exception as exp: # pylint: disable=broad-except
220
- print(exp)
221
- finally:
222
- client.close_session()
242
+ # Dump full pagelist for exploration
243
+ subparsers.add_parser("pagelist", help="Output full pagelist as JSON")
223
244
 
224
- elif args.device_action == "status":
225
- try:
226
- print(
227
- pd.DataFrame.from_dict(
228
- data=client.load_cameras(),
229
- orient="index",
230
- columns=[
231
- "name",
232
- # version,
233
- # upgrade_available,
234
- "status",
235
- "device_category",
236
- "device_sub_category",
237
- "sleep",
238
- "privacy",
239
- "audio",
240
- "ir_led",
241
- "state_led",
242
- # follow_move,
243
- # alarm_notify,
244
- # alarm_schedules_enabled,
245
- # alarm_sound_mod,
246
- # encrypted,
247
- "local_ip",
248
- "local_rtsp_port",
249
- "detection_sensibility",
250
- "battery_level",
251
- "alarm_schedules_enabled",
252
- "alarm_notify",
253
- "Motion_Trigger",
254
- # last_alarm_time,
255
- # last_alarm_pic
256
- ],
257
- )
258
- )
259
- except Exception as exp: # pylint: disable=broad-except
260
- print(exp)
261
- finally:
262
- client.close_session()
263
-
264
- elif args.device_action == "switch":
265
- try:
266
- print(json.dumps(client.get_switch(), indent=2))
267
- except Exception as exp: # pylint: disable=broad-except
268
- print(exp)
269
- finally:
270
- client.close_session()
245
+ # Dump device infos mapping (optionally for a single serial)
246
+ parser_device_infos = subparsers.add_parser(
247
+ "device_infos", help="Output device infos (raw JSON), optionally filtered by serial"
248
+ )
249
+ parser_device_infos.add_argument(
250
+ "--serial", required=False, help="Optional serial to filter a single device"
251
+ )
271
252
 
272
- elif args.device_action == "connection":
273
- try:
274
- print(json.dumps(client.get_connection(), indent=2))
275
- except Exception as exp: # pylint: disable=broad-except
276
- print(exp)
277
- finally:
278
- client.close_session()
253
+ return parser.parse_args(argv)
279
254
 
280
- else:
281
- print("Action not implemented: %s", args.device_action)
282
255
 
283
- elif args.action == "devices_light":
284
- if args.devices_light_action == "status":
285
- try:
286
- print(
287
- pd.DataFrame.from_dict(
288
- data=client.load_light_bulbs(),
289
- orient="index",
290
- columns=[
291
- "name",
292
- # "version",
293
- # "upgrade_available",
294
- "status",
295
- "device_category",
296
- "device_sub_category",
297
- "local_ip",
298
- "productId",
299
- "is_on",
300
- "brightness",
301
- "color_temperature",
302
- ],
303
- )
304
- )
305
- except Exception as exp: # pylint: disable=broad-except
306
- print(exp)
307
- finally:
308
- client.close_session()
309
-
310
- elif args.action == "light":
311
- # load light bulb object
256
+ def _login(client: EzvizClient) -> None:
257
+ """Login if credentials are configured; skip when only a token is used."""
258
+ if client.account and client.password:
312
259
  try:
313
- light_bulb = EzvizLightBulb(client, args.serial)
314
- logging.debug("Light bulb loaded")
315
- except Exception as exp: # pylint: disable=broad-except
316
- print(exp)
317
- client.close_session()
318
-
319
- if args.light_action == "toggle":
260
+ client.login()
261
+ except EzvizAuthVerificationCode:
262
+ mfa_code = input("MFA code required, please input MFA code.\n")
320
263
  try:
321
- light_bulb.toggle_switch()
322
- except Exception as exp: # pylint: disable=broad-except
323
- print(exp)
324
- finally:
325
- client.close_session()
326
-
327
- elif args.light_action == "status":
328
- try:
329
- print(json.dumps(light_bulb.status(), indent=2))
330
-
331
- except Exception as exp: # pylint: disable=broad-except
332
- print(exp)
333
- finally:
334
- client.close_session()
335
-
336
- elif args.action == "home_defence_mode":
337
-
338
- if args.mode:
339
- try:
340
- print(
341
- json.dumps(
342
- client.api_set_defence_mode(
343
- getattr(DefenseModeType, args.mode).value
344
- ),
345
- indent=2,
346
- )
347
- )
348
- except Exception as exp: # pylint: disable=broad-except
349
- print(exp)
350
- finally:
351
- client.close_session()
352
-
353
- elif args.action == "mqtt":
354
-
355
- logging.basicConfig()
356
- logging.getLogger().setLevel(logging.DEBUG)
357
-
358
- try:
359
- token = client.login()
360
- mqtt = MQTTClient(token)
361
- mqtt.start()
362
-
363
- except Exception as exp: # pylint: disable=broad-except
364
- print(exp)
365
- finally:
366
- client.close_session()
264
+ code_int = int(mfa_code.strip())
265
+ except ValueError:
266
+ code_int = None
267
+ client.login(sms_code=code_int)
367
268
 
368
- elif args.action == "camera":
369
269
 
370
- # load camera object
371
- try:
372
- camera = EzvizCamera(client, args.serial)
373
- logging.debug("Camera loaded")
374
- except Exception as exp: # pylint: disable=broad-except
375
- print(exp)
376
- client.close_session()
270
+ def _write_json(obj: Any) -> None:
271
+ """Write an object to stdout as pretty JSON."""
272
+ sys.stdout.write(json.dumps(obj, indent=2) + "\n")
377
273
 
378
- if args.camera_action == "move":
379
- try:
380
- camera.move(args.direction, args.speed)
381
- except Exception as exp: # pylint: disable=broad-except
382
- print(exp)
383
- finally:
384
- client.close_session()
385
274
 
386
- elif args.camera_action == "move_coords":
387
- try:
388
- camera.move_coordinates(args.x, args.y)
389
- except Exception as exp: # pylint: disable=broad-except
390
- print(exp)
391
- finally:
392
- client.close_session()
275
+ def _write_df(df: pd.DataFrame) -> None:
276
+ """Write a DataFrame to stdout as a formatted table."""
277
+ sys.stdout.write(df.to_string() + "\n")
393
278
 
394
- elif args.camera_action == "status":
395
- try:
396
- print(json.dumps(camera.status(), indent=2))
397
279
 
398
- except Exception as exp: # pylint: disable=broad-except
399
- print(exp)
400
- finally:
401
- client.close_session()
280
+ def _handle_devices(args: argparse.Namespace, client: EzvizClient) -> int:
281
+ """Handle `devices` subcommands (device/status/switch/connection)."""
282
+ if args.device_action == "device":
283
+ _write_json(client.get_device())
284
+ return 0
402
285
 
403
- elif args.camera_action == "unlock-door":
404
- try:
405
- camera.door_unlock()
406
-
407
- except Exception as exp: # pylint: disable=broad-except
408
- print(exp)
409
- finally:
410
- client.close_session()
286
+ if args.device_action == "status":
287
+ data = client.load_cameras(refresh=getattr(args, "refresh", True))
288
+ if args.json:
289
+ _write_json(data)
290
+ else:
291
+ # Enrich with common switch flags when available
292
+ def _flag_from_switch(sw: Any, code: int) -> bool | None:
293
+ # Accept list[{type, enable}] or dict-like {type: enable}
294
+ if isinstance(sw, list):
295
+ for item in sw:
296
+ if isinstance(item, dict) and item.get("type") == code:
297
+ val = item.get("enable")
298
+ return bool(val) if isinstance(val, (bool, int)) else None
299
+ return None
300
+ if isinstance(sw, dict):
301
+ val = sw.get(code) if code in sw else sw.get(str(code))
302
+ return bool(val) if isinstance(val, (bool, int)) else None
303
+ return None
304
+
305
+ for payload in data.values():
306
+ sw = payload.get("SWITCH")
307
+ if sw is None:
308
+ continue
309
+
310
+ # Compute all switch flags present on the device
311
+ flags: dict[str, bool] = {}
312
+ if isinstance(sw, list):
313
+ for item in sw:
314
+ if not isinstance(item, dict):
315
+ continue
316
+ t = item.get("type")
317
+ en = item.get("enable")
318
+ if not isinstance(t, int) or not isinstance(en, (bool, int)):
319
+ continue
320
+ try:
321
+ name = DeviceSwitchType(t).name.lower()
322
+ except ValueError:
323
+ name = f"switch_{t}"
324
+ flags[name] = bool(en)
325
+ elif isinstance(sw, dict):
326
+ for k, v in sw.items():
327
+ try:
328
+ t = int(k)
329
+ except (TypeError, ValueError):
330
+ continue
331
+ if not isinstance(v, (bool, int)):
332
+ continue
333
+ try:
334
+ name = DeviceSwitchType(t).name.lower()
335
+ except ValueError:
336
+ name = f"switch_{t}"
337
+ flags[name] = bool(v)
338
+
339
+ if flags:
340
+ payload["switch_flags"] = flags
341
+
342
+ # Keep legacy-friendly individual columns
343
+ payload["sleep"] = flags.get("sleep")
344
+ payload["privacy"] = flags.get("privacy")
345
+ payload["audio"] = flags.get("sound")
346
+ payload["ir_led"] = flags.get("infrared_light")
347
+ payload["state_led"] = flags.get("light")
348
+
349
+ df = pd.DataFrame.from_dict(
350
+ data=data,
351
+ orient="index",
352
+ columns=[
353
+ "name",
354
+ "status",
355
+ "device_category",
356
+ "device_sub_category",
357
+ "sleep",
358
+ "privacy",
359
+ "audio",
360
+ "ir_led",
361
+ "state_led",
362
+ "local_ip",
363
+ "local_rtsp_port",
364
+ "battery_level",
365
+ "alarm_schedules_enabled",
366
+ "alarm_notify",
367
+ "Motion_Trigger",
368
+ ],
369
+ )
370
+ _write_df(df)
371
+ return 0
372
+
373
+ if args.device_action == "switch":
374
+ _write_json(client.get_switch())
375
+ return 0
376
+
377
+ if args.device_action == "connection":
378
+ _write_json(client.get_connection())
379
+ return 0
380
+
381
+ _LOGGER.error("Action not implemented: %s", args.device_action)
382
+ return 2
383
+
384
+
385
+ def _handle_devices_light(args: argparse.Namespace, client: EzvizClient) -> int:
386
+ """Handle `devices_light` subcommands (status)."""
387
+ if args.devices_light_action == "status":
388
+ data = client.load_light_bulbs(refresh=getattr(args, "refresh", True))
389
+ if args.json:
390
+ _write_json(data)
391
+ else:
392
+ df = pd.DataFrame.from_dict(
393
+ data=data,
394
+ orient="index",
395
+ columns=[
396
+ "name",
397
+ "status",
398
+ "device_category",
399
+ "device_sub_category",
400
+ "local_ip",
401
+ "productId",
402
+ "is_on",
403
+ "brightness",
404
+ "color_temperature",
405
+ ],
406
+ )
407
+ _write_df(df)
408
+ return 0
409
+ return 2
410
+
411
+
412
+ def _handle_pagelist(client: EzvizClient) -> int:
413
+ """Output full pagelist (raw JSON) for exploration in editors like Notepad++."""
414
+ data = client._get_page_list() # noqa: SLF001
415
+ _write_json(data)
416
+ return 0
417
+
418
+
419
+ def _handle_device_infos(args: argparse.Namespace, client: EzvizClient) -> int:
420
+ """Output device infos mapping (raw JSON), optionally filtered by serial."""
421
+ data = client.get_device_infos(args.serial) if args.serial else client.get_device_infos()
422
+ _write_json(data)
423
+ return 0
424
+
425
+
426
+ def _handle_light(args: argparse.Namespace, client: EzvizClient) -> int:
427
+ """Handle `light` subcommands (toggle/status)."""
428
+ light_bulb = EzvizLightBulb(client, args.serial)
429
+ _LOGGER.debug("Light bulb loaded")
430
+ if args.light_action == "toggle":
431
+ light_bulb.toggle_switch()
432
+ return 0
433
+ if args.light_action == "status":
434
+ _write_json(light_bulb.status())
435
+ return 0
436
+ _LOGGER.error("Action not implemented for light: %s", args.light_action)
437
+ return 2
438
+
439
+
440
+ def _handle_home_defence_mode(args: argparse.Namespace, client: EzvizClient) -> int:
441
+ """Handle `home_defence_mode` subcommands (set mode)."""
442
+ if args.mode:
443
+ res = client.api_set_defence_mode(getattr(DefenseModeType, args.mode).value)
444
+ _write_json(res)
445
+ return 0
446
+ return 2
447
+
448
+
449
+ def _handle_mqtt(_: argparse.Namespace, client: EzvizClient) -> int:
450
+ """Connect to MQTT push notifications using current session token."""
451
+ logging.getLogger().setLevel(logging.DEBUG)
452
+ client.login()
453
+ mqtt = client.get_mqtt_client()
454
+ mqtt.connect()
455
+ return 0
456
+
457
+
458
+ def _handle_camera(args: argparse.Namespace, client: EzvizClient) -> int:
459
+ """Handle `camera` subcommands (status/move/unlock/switch/alarm/select)."""
460
+ camera = EzvizCamera(client, args.serial)
461
+ _LOGGER.debug("Camera loaded")
462
+
463
+ if args.camera_action == "move":
464
+ camera.move(args.direction, args.speed)
465
+ return 0
466
+
467
+ if args.camera_action == "move_coords":
468
+ camera.move_coordinates(args.x, args.y)
469
+ return 0
470
+
471
+ if args.camera_action == "status":
472
+ _write_json(camera.status(refresh=getattr(args, "refresh", True)))
473
+ return 0
474
+
475
+ if args.camera_action == "unlock-door":
476
+ camera.door_unlock()
477
+ return 0
478
+
479
+ if args.camera_action == "unlock-gate":
480
+ camera.gate_unlock()
481
+ return 0
482
+
483
+ if args.camera_action == "switch":
484
+ if args.switch == "ir":
485
+ camera.switch_device_ir_led(args.enable)
486
+ elif args.switch == "state":
487
+ sys.stdout.write(str(args.enable) + "\n")
488
+ camera.switch_device_state_led(args.enable)
489
+ elif args.switch == "audio":
490
+ camera.switch_device_audio(args.enable)
491
+ elif args.switch == "privacy":
492
+ camera.switch_privacy_mode(args.enable)
493
+ elif args.switch == "sleep":
494
+ camera.switch_sleep_mode(args.enable)
495
+ elif args.switch == "follow_move":
496
+ camera.switch_follow_move(args.enable)
497
+ elif args.switch == "sound_alarm":
498
+ camera.switch_sound_alarm(args.enable + 1)
499
+ else:
500
+ _LOGGER.error("Unknown switch: %s", args.switch)
501
+ return 2
502
+ return 0
503
+
504
+ if args.camera_action == "alarm":
505
+ if args.sound is not None:
506
+ camera.alarm_sound(args.sound)
507
+ if args.notify is not None:
508
+ camera.alarm_notify(args.notify)
509
+ if args.sensibility is not None:
510
+ camera.alarm_detection_sensibility(args.sensibility)
511
+ if args.do_not_disturb is not None:
512
+ camera.do_not_disturb(args.do_not_disturb)
513
+ if args.schedule is not None:
514
+ camera.change_defence_schedule(args.schedule)
515
+ return 0
516
+
517
+ if args.camera_action == "select":
518
+ if args.battery_work_mode is not None:
519
+ camera.set_battery_camera_work_mode(
520
+ getattr(BatteryCameraWorkMode, args.battery_work_mode)
521
+ )
522
+ return 0
523
+ return 2
524
+
525
+ _LOGGER.error("Action not implemented, try running with -h switch for help")
526
+ return 2
527
+
528
+
529
+ def _load_token_file(path: str | None) -> dict[str, Any] | None:
530
+ """Load a token dictionary from `path` if it exists; else return None."""
531
+ if not path:
532
+ return None
533
+ p = Path(path)
534
+ if not p.exists():
535
+ return None
536
+ try:
537
+ return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
538
+ except (OSError, json.JSONDecodeError): # pragma: no cover - tolerate malformed file
539
+ _LOGGER.warning("Failed to read token file: %s", p)
540
+ return None
411
541
 
412
- elif args.camera_action == "unlock-gate":
413
- try:
414
- camera.gate_unlock()
415
542
 
416
- except Exception as exp: # pylint: disable=broad-except
417
- print(exp)
418
- finally:
419
- client.close_session()
543
+ def _save_token_file(path: str | None, token: dict[str, Any]) -> None:
544
+ """Persist the token dictionary to `path` in JSON format."""
545
+ if not path:
546
+ return
547
+ p = Path(path)
548
+ try:
549
+ p.write_text(json.dumps(token, indent=2), encoding="utf-8")
550
+ _LOGGER.info("Saved token to %s", p)
551
+ except OSError: # pragma: no cover - filesystem issues
552
+ _LOGGER.warning("Failed to save token file: %s", p)
420
553
 
421
- elif args.camera_action == "switch":
422
- try:
423
- if args.switch == "ir":
424
- camera.switch_device_ir_led(args.enable)
425
- elif args.switch == "state":
426
- print(args.enable)
427
- camera.switch_device_state_led(args.enable)
428
- elif args.switch == "audio":
429
- camera.switch_device_audio(args.enable)
430
- elif args.switch == "privacy":
431
- camera.switch_privacy_mode(args.enable)
432
- elif args.switch == "sleep":
433
- camera.switch_sleep_mode(args.enable)
434
- elif args.switch == "follow_move":
435
- camera.switch_follow_move(args.enable)
436
- elif args.switch == "sound_alarm":
437
- # Map 0|1 enable flog to operation type: 1 for off and 2 for on.
438
- camera.switch_sound_alarm(args.enable + 1)
439
- except Exception as exp: # pylint: disable=broad-except
440
- print(exp)
441
- finally:
442
- client.close_session()
443
-
444
- elif args.camera_action == "alarm":
445
- try:
446
- if args.sound is not None:
447
- camera.alarm_sound(args.sound)
448
- if args.notify is not None:
449
- camera.alarm_notify(args.notify)
450
- if args.sensibility is not None:
451
- camera.alarm_detection_sensibility(args.sensibility)
452
- if args.do_not_disturb is not None:
453
- camera.do_not_disturb(args.do_not_disturb)
454
- if args.schedule is not None:
455
- camera.change_defence_schedule(args.schedule)
456
- except Exception as exp: # pylint: disable=broad-except
457
- print(exp)
458
- finally:
459
- client.close_session()
460
-
461
- elif args.camera_action == "select":
462
- try:
463
- if args.battery_work_mode is not None:
464
- camera.set_battery_camera_work_mode(getattr(BatteryCameraWorkMode, args.battery_work_mode))
465
554
 
466
- except Exception as exp: # pylint: disable=broad-except
467
- print(exp)
468
- finally:
469
- client.close_session()
555
+ def main(argv: list[str] | None = None) -> int:
556
+ """CLI entry point."""
557
+ args = _parse_args(argv)
558
+ _setup_logging(args.debug)
470
559
 
471
- else:
472
- print("Action not implemented, try running with -h switch for help")
560
+ token = _load_token_file(args.token_file)
561
+ if not token and (not args.username or not args.password):
562
+ _LOGGER.error("Provide --token-file (existing) or --username/--password")
563
+ return 2
473
564
 
565
+ client = EzvizClient(args.username, args.password, args.region, token=token)
566
+ try:
567
+ _login(client)
568
+
569
+ if args.action == "devices":
570
+ return _handle_devices(args, client)
571
+ if args.action == "devices_light":
572
+ return _handle_devices_light(args, client)
573
+ if args.action == "light":
574
+ return _handle_light(args, client)
575
+ if args.action == "home_defence_mode":
576
+ return _handle_home_defence_mode(args, client)
577
+ if args.action == "mqtt":
578
+ return _handle_mqtt(args, client)
579
+ if args.action == "camera":
580
+ return _handle_camera(args, client)
581
+ if args.action == "pagelist":
582
+ return _handle_pagelist(client)
583
+ if args.action == "device_infos":
584
+ return _handle_device_infos(args, client)
585
+
586
+ except PyEzvizError as exp:
587
+ _LOGGER.error("%s", exp)
588
+ return 1
589
+ except KeyboardInterrupt:
590
+ _LOGGER.error("Interrupted")
591
+ return 130
474
592
  else:
475
- print("Action not implemented: %s", args.action)
593
+ _LOGGER.error("Action not implemented: %s", args.action)
594
+ return 2
595
+ finally:
596
+ if args.save_token and args.token_file:
597
+ _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
598
+ client.close_session()
476
599
 
477
600
 
478
601
  if __name__ == "__main__":