onvif-python 0.2.11__py3-none-any.whl → 0.3.0__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.
Files changed (60) hide show
  1. onvif/__init__.py +5 -13
  2. onvif/cli/__init__.py +2 -2
  3. onvif/cli/__main__.py +2 -2
  4. onvif/cli/interactive.py +53 -45
  5. onvif/cli/main.py +28 -16
  6. onvif/cli/utils.py +32 -139
  7. onvif/client.py +116 -94
  8. onvif/meta.py +6 -0
  9. onvif/operator.py +80 -30
  10. onvif/services/__init__.py +38 -38
  11. onvif/services/accesscontrol.py +177 -8
  12. onvif/services/accessrules.py +82 -8
  13. onvif/services/actionengine.py +64 -8
  14. onvif/services/analytics/analytics.py +65 -8
  15. onvif/services/analytics/ruleengine.py +35 -8
  16. onvif/services/analyticsdevice.py +57 -23
  17. onvif/services/appmgmt.py +35 -8
  18. onvif/services/authenticationbehavior.py +137 -8
  19. onvif/services/credential.py +165 -10
  20. onvif/services/deviceio.py +160 -8
  21. onvif/services/devicemgmt.py +569 -29
  22. onvif/services/display.py +71 -8
  23. onvif/services/doorcontrol.py +175 -8
  24. onvif/services/events/events.py +47 -8
  25. onvif/services/events/notification.py +41 -7
  26. onvif/services/events/pausable_subscription.py +48 -7
  27. onvif/services/events/pullpoint.py +52 -7
  28. onvif/services/events/subscription.py +34 -7
  29. onvif/services/imaging.py +80 -8
  30. onvif/services/media.py +463 -8
  31. onvif/services/media2.py +332 -8
  32. onvif/services/provisioning.py +21 -8
  33. onvif/services/ptz.py +201 -15
  34. onvif/services/receiver.py +49 -8
  35. onvif/services/recording.py +124 -8
  36. onvif/services/replay.py +31 -8
  37. onvif/services/schedule.py +101 -8
  38. onvif/services/search.py +191 -8
  39. onvif/services/security/advancedsecurity.py +17 -8
  40. onvif/services/security/authorizationserver.py +23 -8
  41. onvif/services/security/dot1x.py +120 -8
  42. onvif/services/security/jwt.py +17 -8
  43. onvif/services/security/keystore.py +405 -8
  44. onvif/services/security/mediasigning.py +49 -8
  45. onvif/services/security/tlsserver.py +181 -8
  46. onvif/services/thermal.py +35 -8
  47. onvif/services/uplink.py +29 -8
  48. onvif/utils/__init__.py +8 -8
  49. onvif/utils/discovery.py +1 -0
  50. onvif/utils/error_handlers.py +22 -16
  51. onvif/utils/exceptions.py +1 -0
  52. onvif/utils/service.py +5 -8
  53. onvif/utils/wsdl.py +4 -3
  54. onvif/utils/zeep.py +4 -3
  55. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/METADATA +23 -23
  56. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/RECORD +60 -59
  57. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/WHEEL +0 -0
  58. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/entry_points.txt +0 -0
  59. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
  60. {onvif_python-0.2.11.dist-info → onvif_python-0.3.0.dist-info}/top_level.txt +0 -0
onvif/__init__.py CHANGED
@@ -1,15 +1,9 @@
1
- """onvif-python: A Python library for interacting with ONVIF-compliant devices."""
1
+ """ONVIF Python"""
2
2
 
3
- from typing import Final
4
-
5
-
6
- __version__: Final[str] = "0.2.11"
7
- __repository__: Final[str] = "https://github.com/nirsimetri/onvif-python"
8
-
9
- from .cli import main as ONVIFCLI
10
- from .client import ONVIFClient
11
- from .operator import CacheMode
12
- from .utils import (
3
+ from onvif.cli import main as ONVIFCLI
4
+ from onvif.client import ONVIFClient
5
+ from onvif.operator import CacheMode
6
+ from onvif.utils import (
13
7
  ONVIFWSDL,
14
8
  ONVIFDiscovery,
15
9
  ONVIFErrorHandler,
@@ -28,6 +22,4 @@ __all__ = [
28
22
  "ONVIFCLI",
29
23
  "ONVIFDiscovery",
30
24
  "ONVIFParser",
31
- "__version__",
32
- "__repository__",
33
25
  ]
onvif/cli/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
- """ONVIF CLI"""
1
+ """ONVIF CLI."""
2
2
 
3
- from .main import main
3
+ from onvif.cli.main import main
4
4
 
5
5
  __all__ = ["main"]
onvif/cli/__main__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """ONVIF CLI entry point for python -m onvif.cli"""
2
+ """ONVIF CLI entry point for python -m onvif.cli."""
3
3
 
4
- from .main import main
4
+ from onvif.cli.main import main
5
5
 
6
6
  if __name__ == "__main__":
7
7
  main()
onvif/cli/interactive.py CHANGED
@@ -17,10 +17,7 @@ from typing import Any
17
17
  from requests.exceptions import RequestException
18
18
  from zeep.exceptions import Fault, TransportError
19
19
 
20
- from .. import __repository__, __version__
21
- from ..client import ONVIFClient
22
- from ..utils.exceptions import ONVIFOperationException
23
- from .utils import (
20
+ from onvif.cli.utils import (
24
21
  colorize,
25
22
  format_capabilities_as_services,
26
23
  format_services_list,
@@ -31,10 +28,13 @@ from .utils import (
31
28
  get_service_required_args,
32
29
  parse_json_params,
33
30
  )
31
+ from onvif.client import ONVIFClient
32
+ from onvif.meta import __repository__, __version__
33
+ from onvif.utils.exceptions import ONVIFOperationException
34
34
 
35
35
 
36
36
  class InteractiveShell(cmd.Cmd):
37
- """Interactive ONVIF CLI shell"""
37
+ """Interactive ONVIF CLI shell."""
38
38
 
39
39
  def __init__(self, client: ONVIFClient, args):
40
40
  super().__init__()
@@ -432,7 +432,7 @@ class InteractiveShell(cmd.Cmd):
432
432
  print(line)
433
433
 
434
434
  def _resolve_stored_reference(self, reference: str):
435
- """Resolve stored data reference like 'profiles[0].token' or 'profiles.Token'
435
+ """Resolve stored data reference like 'profiles[0].token' or 'profiles.Token'.
436
436
 
437
437
  Args:
438
438
  reference: String reference like 'profiles[0]' or 'profiles.Token'
@@ -440,7 +440,6 @@ class InteractiveShell(cmd.Cmd):
440
440
  Returns:
441
441
  Resolved value or None if not found
442
442
  """
443
-
444
443
  # Parse the reference - e.g., "profiles[0].token" or "services.Namespace"
445
444
  # Split by dots and brackets
446
445
  parts = re.split(r"\.|\[|\]", reference)
@@ -484,7 +483,7 @@ class InteractiveShell(cmd.Cmd):
484
483
  return current
485
484
 
486
485
  def _substitute_stored_references(self, params_str: str) -> str:
487
- """Substitute $variable references in parameter string with stored data
486
+ """Substitute $variable references in parameter string with stored data.
488
487
 
489
488
  Args:
490
489
  params_str: Parameter string that may contain $variable references
@@ -492,7 +491,6 @@ class InteractiveShell(cmd.Cmd):
492
491
  Returns:
493
492
  Parameter string with substituted values
494
493
  """
495
-
496
494
  # Find all $variable references (e.g., $profiles[0].token)
497
495
  pattern = (
498
496
  r"\$([a-zA-Z_][a-zA-Z0-9_]*(?:\[[0-9]+\])?(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)"
@@ -518,14 +516,15 @@ class InteractiveShell(cmd.Cmd):
518
516
  return re.sub(pattern, replace_reference, params_str)
519
517
 
520
518
  def update_prompt(self):
521
- """Update command prompt based on current context"""
519
+ """Update command prompt based on current context."""
522
520
  if self.current_service_name:
523
521
  self.prompt = f"{self.args.username}@{self.args.host}:{self.args.port}/{self.current_service_name} > "
524
522
  else:
525
523
  self.prompt = f"{self.args.username}@{self.args.host}:{self.args.port} > "
526
524
 
527
525
  def onecmd(self, line):
528
- """Override onecmd to handle service method calls without showing cmd.Cmd traceback"""
526
+ """Override onecmd to handle service method calls without showing cmd.Cmd
527
+ traceback."""
529
528
  line = line.strip()
530
529
  if not line:
531
530
  return self.emptyline()
@@ -560,7 +559,7 @@ class InteractiveShell(cmd.Cmd):
560
559
  return super().onecmd(line)
561
560
 
562
561
  def default(self, line):
563
- """Handle unknown commands"""
562
+ """Handle unknown commands."""
564
563
  available_services = get_device_available_services(self.client)
565
564
 
566
565
  # Check if it's a service call (with or without arguments)
@@ -594,7 +593,7 @@ class InteractiveShell(cmd.Cmd):
594
593
  print(f"Type {colorize('help', 'white')} for available commands")
595
594
 
596
595
  def get_suggestions(self, partial_cmd: str) -> list[str]:
597
- """Get command suggestions based on partial input"""
596
+ """Get command suggestions based on partial input."""
598
597
  suggestions = []
599
598
 
600
599
  if self.current_service:
@@ -623,7 +622,7 @@ class InteractiveShell(cmd.Cmd):
623
622
  return suggestions[:5] # Limit to 5 suggestions
624
623
 
625
624
  def completenames(self, text: str, *ignored) -> list[str]:
626
- """Tab completion for command names"""
625
+ """Tab completion for command names."""
627
626
  if self.current_service:
628
627
  # Complete method names in service mode
629
628
  methods = get_service_methods(self.current_service)
@@ -646,7 +645,7 @@ class InteractiveShell(cmd.Cmd):
646
645
  return completions
647
646
 
648
647
  def cmdloop(self, intro=None):
649
- """Override cmdloop to handle TAB completion manually"""
648
+ """Override cmdloop to handle TAB completion manually."""
650
649
  if intro is not None:
651
650
  self.intro = intro
652
651
  if self.intro:
@@ -670,7 +669,7 @@ class InteractiveShell(cmd.Cmd):
670
669
  stop = self.postcmd(stop, line)
671
670
 
672
671
  def columnize(self, list, displaywidth=80):
673
- """Override columnize to use grid format for TAB completion"""
672
+ """Override columnize to use grid format for TAB completion."""
674
673
  if not list:
675
674
  return
676
675
 
@@ -678,7 +677,7 @@ class InteractiveShell(cmd.Cmd):
678
677
  self._display_grid(list)
679
678
 
680
679
  def print_topics(self, header, cmds, cmdlen, maxcol):
681
- """Override print_topics to use grid format for TAB completion"""
680
+ """Override print_topics to use grid format for TAB completion."""
682
681
  if not cmds:
683
682
  return
684
683
 
@@ -690,7 +689,7 @@ class InteractiveShell(cmd.Cmd):
690
689
  self._display_grid(cmds)
691
690
 
692
691
  def do_capabilities(self, line): # pylint: disable=unused-argument
693
- """Show device capabilities in service format"""
692
+ """Show device capabilities in service format."""
694
693
  try:
695
694
  if hasattr(self.client, "capabilities") and self.client.capabilities:
696
695
  result = self.client.capabilities
@@ -713,7 +712,7 @@ class InteractiveShell(cmd.Cmd):
713
712
  print(f"{colorize('Error:', 'red')} {e}")
714
713
 
715
714
  def do_services(self, line): # pylint: disable=unused-argument
716
- """Show available services in service format"""
715
+ """Show available services in service format."""
717
716
  try:
718
717
  if hasattr(self.client, "services") and self.client.services:
719
718
  result = self.client.services
@@ -736,7 +735,7 @@ class InteractiveShell(cmd.Cmd):
736
735
  print(f"{colorize('Error:', 'red')} {e}")
737
736
 
738
737
  def do_enter_service(self, line):
739
- """Enter service mode with optional arguments for services that require them"""
738
+ """Enter service mode with optional arguments for services that require them."""
740
739
  # Parse service name and arguments
741
740
  parts = line.split(None, 1)
742
741
  service_name = parts[0] if parts else ""
@@ -835,7 +834,7 @@ class InteractiveShell(cmd.Cmd):
835
834
  traceback.print_exc()
836
835
 
837
836
  def do_ls(self, line): # pylint: disable=unused-argument
838
- """List available commands/services like TAB completion"""
837
+ """List available commands/services like TAB completion."""
839
838
  if self.current_service:
840
839
  # In service mode - show available methods
841
840
  methods = get_service_methods(self.current_service)
@@ -859,7 +858,10 @@ class InteractiveShell(cmd.Cmd):
859
858
  print("No commands available")
860
859
 
861
860
  def do_desc(self, line):
862
- """Describes a method from its WSDL documentation. Usage: desc <method_name>"""
861
+ """Describes a method from its WSDL documentation.
862
+
863
+ Usage: desc <method_name>
864
+ """
863
865
  method_name = line.strip()
864
866
 
865
867
  if not self.current_service:
@@ -911,14 +913,17 @@ class InteractiveShell(cmd.Cmd):
911
913
  def complete_desc(
912
914
  self, text, line, begidx, endidx
913
915
  ): # pylint: disable=unused-argument
914
- """Autocomplete method names for desc command"""
916
+ """Autocomplete method names for desc command."""
915
917
  if not self.current_service:
916
918
  return []
917
919
  methods = get_service_methods(self.current_service)
918
920
  return [m for m in methods if m.lower().startswith(text.lower())]
919
921
 
920
922
  def do_type(self, line):
921
- """Show input and output types for a method. Usage: type <method_name>"""
923
+ """Show input and output types for a method.
924
+
925
+ Usage: type <method_name>
926
+ """
922
927
  method_name = line.strip()
923
928
 
924
929
  if not self.current_service:
@@ -944,8 +949,7 @@ class InteractiveShell(cmd.Cmd):
944
949
  if type_info:
945
950
  # Helper function to display parameters recursively with tree-style indentation
946
951
  def display_params(params, prefix_lines=None, is_root_level=False):
947
- """
948
- Display parameters with tree-style formatting.
952
+ """Display parameters with tree-style formatting.
949
953
 
950
954
  Args:
951
955
  params: List of parameter dictionaries
@@ -1003,7 +1007,10 @@ class InteractiveShell(cmd.Cmd):
1003
1007
  # Display parameter name with tree structure, prefix, occurrence, and type
1004
1008
  # Only add semicolon separator if there's an occurrence label
1005
1009
  separator = ";" if occurs else ""
1006
- param_line = f"{current_prefix}{tree_branch}{prefix_symbol}{colorize(param['name'], 'white')}{occurs}{separator} {colorize(type_str, 'cyan')}"
1010
+ param_line = (
1011
+ f"{current_prefix}{tree_branch}{prefix_symbol}{colorize(param['name'], 'white')}"
1012
+ f"{occurs}{separator} {colorize(type_str, 'cyan')}"
1013
+ )
1007
1014
  print(param_line)
1008
1015
 
1009
1016
  # Display documentation if available
@@ -1076,7 +1083,7 @@ class InteractiveShell(cmd.Cmd):
1076
1083
  def complete_type(
1077
1084
  self, text, line, begidx, endidx
1078
1085
  ): # pylint: disable=unused-argument
1079
- """Autocomplete method names for type command"""
1086
+ """Autocomplete method names for type command."""
1080
1087
  if not self.current_service:
1081
1088
  return []
1082
1089
  methods = get_service_methods(self.current_service)
@@ -1095,12 +1102,12 @@ class InteractiveShell(cmd.Cmd):
1095
1102
  def complete_cd(
1096
1103
  self, text, line, begidx, endidx
1097
1104
  ): # pylint: disable=unused-argument
1098
- """Autocomplete service names for cd command"""
1105
+ """Autocomplete service names for cd command."""
1099
1106
  services = get_device_available_services(self.client)
1100
1107
  return [s for s in services if s.lower().startswith(text.lower())]
1101
1108
 
1102
1109
  def do_pwd(self, line): # pylint: disable=unused-argument
1103
- """Show current service context"""
1110
+ """Show current service context."""
1104
1111
  if self.current_service_name:
1105
1112
  print(
1106
1113
  f"{colorize('Current service:', 'yellow')} {colorize(self.current_service_name, 'cyan')}"
@@ -1111,7 +1118,7 @@ class InteractiveShell(cmd.Cmd):
1111
1118
  )
1112
1119
 
1113
1120
  def do_shortcuts(self, line): # pylint: disable=unused-argument
1114
- """Show available shortcuts"""
1121
+ """Show available shortcuts."""
1115
1122
  shortcuts = f"""
1116
1123
  {colorize('Available Shortcuts:', 'cyan')}
1117
1124
 
@@ -1160,14 +1167,14 @@ class InteractiveShell(cmd.Cmd):
1160
1167
 
1161
1168
  def do_store(self, line):
1162
1169
  """Store last result with a name: store <name>"""
1163
-
1164
1170
  if not line:
1165
1171
  print("Usage: store <name>")
1166
1172
  return
1167
1173
  # Only allow valid Python variable names
1168
1174
  if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", line):
1169
1175
  print(
1170
- f"{colorize('Error:', 'red')} Invalid name '{line}'. Only letters, numbers, and underscores allowed, and must not start with a number."
1176
+ f"{colorize('Error:', 'red')} Invalid name '{line}'. "
1177
+ "Only letters, numbers, and underscores allowed, and must not start with a number."
1171
1178
  )
1172
1179
  return
1173
1180
  # Must be unique
@@ -1209,11 +1216,12 @@ class InteractiveShell(cmd.Cmd):
1209
1216
  def complete_rm(
1210
1217
  self, text, line, begidx, endidx
1211
1218
  ): # pylint: disable=unused-argument
1212
- """Autocomplete stored variable names for rm command"""
1219
+ """Autocomplete stored variable names for rm command."""
1213
1220
  return [name for name in self.stored_data if name.startswith(text)]
1214
1221
 
1215
1222
  def do_show(self, line):
1216
- """Show stored data: show <name> or show <name>.<attribute> or show <name>[index]"""
1223
+ """Show stored data: show <name> or show <name>.<attribute> or show
1224
+ <name>[index]"""
1217
1225
  if not line:
1218
1226
  print(colorize("Stored data:", "green"))
1219
1227
  for name in self.stored_data:
@@ -1235,7 +1243,7 @@ class InteractiveShell(cmd.Cmd):
1235
1243
  print(f"{colorize('Error:', 'red')} Cannot resolve '{line}'")
1236
1244
 
1237
1245
  def complete_show(self, text, line, begidx, endidx):
1238
- """Autocomplete stored variable names for show command"""
1246
+ """Autocomplete stored variable names for show command."""
1239
1247
  # Get the part being completed
1240
1248
  parts = line.split()
1241
1249
  if len(parts) <= 1 or (len(parts) == 2 and not line.endswith(" ")):
@@ -1244,18 +1252,18 @@ class InteractiveShell(cmd.Cmd):
1244
1252
  return []
1245
1253
 
1246
1254
  def do_clear(self, line): # pylint: disable=unused-argument
1247
- """Clear terminal screen"""
1255
+ """Clear terminal screen."""
1248
1256
  # Clear screen for both Windows and Unix-like systems
1249
1257
  os.system("cls" if os.name == "nt" else "clear")
1250
1258
 
1251
1259
  def do_cls(self, line): # pylint: disable=unused-argument
1252
- """Clear stored data"""
1260
+ """Clear stored data."""
1253
1261
  self.stored_data.clear()
1254
1262
  self.stored_metadata.clear()
1255
1263
  print(f"{colorize('Cleared all stored data', 'yellow')}")
1256
1264
 
1257
1265
  def do_info(self, line): # pylint: disable=unused-argument
1258
- """Show connection and device information"""
1266
+ """Show connection and device information."""
1259
1267
  # Build connection and CLI options info
1260
1268
  options_info = []
1261
1269
 
@@ -1305,7 +1313,7 @@ class InteractiveShell(cmd.Cmd):
1305
1313
  print() # Extra newline for spacing
1306
1314
 
1307
1315
  def execute_service_method(self, method_name, params_str):
1308
- """Execute a method on the current service"""
1316
+ """Execute a method on the current service."""
1309
1317
  if not self.current_service:
1310
1318
  print(f"{colorize('Error:', 'red')} Not in service mode")
1311
1319
  return
@@ -1359,7 +1367,7 @@ class InteractiveShell(cmd.Cmd):
1359
1367
  traceback.print_exc()
1360
1368
 
1361
1369
  def do_debug(self, line): # pylint: disable=unused-argument
1362
- """Show debug information"""
1370
+ """Show debug information."""
1363
1371
  if self.client.xml_plugin:
1364
1372
  if self._last_method and self._last_operation_timestamp:
1365
1373
  print(f"{colorize('Last Operation:', 'cyan')}")
@@ -1379,16 +1387,16 @@ class InteractiveShell(cmd.Cmd):
1379
1387
  )
1380
1388
 
1381
1389
  def do_exit(self, line): # pylint: disable=unused-argument
1382
- """Exit the shell"""
1390
+ """Exit the shell."""
1383
1391
  self._stop_health_check.set()
1384
1392
  print(colorize("Goodbye!", "cyan"))
1385
1393
  return True
1386
1394
 
1387
1395
  def emptyline(self):
1388
- """Handle empty line"""
1396
+ """Handle empty line."""
1389
1397
 
1390
1398
  def do_help(self, line):
1391
- """Show help information"""
1399
+ """Show help information."""
1392
1400
  if line:
1393
1401
  super().do_help(line)
1394
1402
  else:
@@ -1462,7 +1470,7 @@ class InteractiveShell(cmd.Cmd):
1462
1470
  print(help_text)
1463
1471
 
1464
1472
  def run(self):
1465
- """Run the interactive shell"""
1473
+ """Run the interactive shell."""
1466
1474
  try:
1467
1475
  self.cmdloop()
1468
1476
  except KeyboardInterrupt:
onvif/cli/main.py CHANGED
@@ -14,16 +14,17 @@ import warnings
14
14
  from datetime import datetime, timezone
15
15
  from typing import Any
16
16
 
17
- from .. import __repository__, __version__
18
- from ..client import ONVIFClient
19
- from ..operator import CacheMode
20
- from ..utils import ONVIFOperationException, ONVIFDiscovery
21
- from .interactive import InteractiveShell
22
- from .utils import colorize, parse_json_params
17
+ from onvif.cli.interactive import InteractiveShell
18
+ from onvif.cli.utils import colorize, parse_json_params
19
+ from onvif.client import ONVIFClient
20
+ from onvif.meta import __repository__, __version__
21
+ from onvif.operator import CacheMode
22
+ from onvif.utils import ONVIFDiscovery, ONVIFOperationException
23
23
 
24
24
 
25
+ # pylint: disable=line-too-long
25
26
  def create_parser():
26
- """Create argument parser for ONVIF CLI"""
27
+ """Create argument parser for ONVIF CLI."""
27
28
  parser = argparse.ArgumentParser(
28
29
  prog="onvif",
29
30
  description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v{__version__}\n{__repository__}",
@@ -95,6 +96,13 @@ Examples:
95
96
  "-if",
96
97
  help="Specify network interface IP for discovery (default: auto-detect)",
97
98
  )
99
+ parser.add_argument(
100
+ "--discovery-timeout",
101
+ "-dt",
102
+ type=int,
103
+ default=4,
104
+ help="Discovery timeout in seconds (default: 4)",
105
+ )
98
106
 
99
107
  # Product search
100
108
  parser.add_argument(
@@ -120,7 +128,7 @@ Examples:
120
128
  "--timeout",
121
129
  type=int,
122
130
  default=10,
123
- help="Connection timeout in seconds (default: 10)",
131
+ help="ONVIF connection timeout in seconds (default: 10)",
124
132
  )
125
133
  parser.add_argument(
126
134
  "--https", action="store_true", help="Use HTTPS instead of HTTP"
@@ -157,7 +165,10 @@ Examples:
157
165
  parser.add_argument(
158
166
  "--output",
159
167
  "-o",
160
- help="Save command output to file. Supports .json, .xml extensions for format detection, or plain text. XML format automatically enables debug mode for SOAP capture.",
168
+ help=(
169
+ "Save command output to file. Supports .json, .xml extensions for format detection, "
170
+ "or plain text. XML format automatically enables debug mode for SOAP capture."
171
+ ),
161
172
  )
162
173
 
163
174
  # Service and method (for direct command execution)
@@ -182,7 +193,7 @@ Examples:
182
193
 
183
194
 
184
195
  def main():
185
- """Main CLI entry point"""
196
+ """Main CLI entry point."""
186
197
  # Setup custom warning format for cleaner output
187
198
  setup_warning_format()
188
199
 
@@ -213,7 +224,8 @@ def main():
213
224
  and (not args.service or not args.method)
214
225
  ):
215
226
  parser.error(
216
- f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} mode or {colorize('service/method', 'white')} must be specified"
227
+ f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} "
228
+ f"mode or {colorize('service/method', 'white')} must be specified"
217
229
  )
218
230
 
219
231
  # Validate output argument
@@ -231,7 +243,7 @@ def main():
231
243
 
232
244
  # Discover devices (pass --https flag to prioritize HTTPS XAddrs and filter term)
233
245
  devices = discover_devices(
234
- timeout=4,
246
+ timeout=args.discovery_timeout,
235
247
  interface=args.interface if args.interface else None,
236
248
  prefer_https=args.https,
237
249
  filter_term=args.filter,
@@ -358,7 +370,7 @@ def execute_command(
358
370
  method_name: str,
359
371
  params_str: str | None = None,
360
372
  ) -> Any:
361
- """Execute a single ONVIF command"""
373
+ """Execute a single ONVIF command."""
362
374
  # Get service instance
363
375
  try:
364
376
  service = getattr(client, service_name.lower())()
@@ -576,7 +588,6 @@ def discover_devices(
576
588
  Returns:
577
589
  List of discovered devices with connection info
578
590
  """
579
-
580
591
  # Use ONVIFDiscovery class
581
592
  discovery = ONVIFDiscovery(timeout=timeout, interface=interface)
582
593
 
@@ -678,7 +689,8 @@ def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
678
689
 
679
690
 
680
691
  def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None:
681
- """Search ONVIF products database and display results in table format with pagination.
692
+ """Search ONVIF products database and display results in table format with
693
+ pagination.
682
694
 
683
695
  Args:
684
696
  search_term: Search term to match against model, post_title, and company_name fields
@@ -962,7 +974,7 @@ def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None
962
974
 
963
975
 
964
976
  def setup_warning_format():
965
- """Setup custom warning format to show clean, concise warnings"""
977
+ """Setup custom warning format to show clean, concise warnings."""
966
978
 
967
979
  def custom_warning_format(
968
980
  message, category, filename, lineno, line=None