onvif-python 0.3.0__py3-none-any.whl → 0.4.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 (71) hide show
  1. onvif/__init__.py +7 -3
  2. onvif/cli/helpers/__init__.py +21 -0
  3. onvif/cli/helpers/discovery.py +156 -0
  4. onvif/cli/helpers/execution.py +57 -0
  5. onvif/cli/helpers/files.py +191 -0
  6. onvif/cli/helpers/messages.py +220 -0
  7. onvif/cli/helpers/products.py +294 -0
  8. onvif/cli/helpers/wsdl_parser.py +973 -0
  9. onvif/cli/interactive.py +52 -212
  10. onvif/cli/main.py +32 -679
  11. onvif/cli/utils.py +27 -953
  12. onvif/client.py +211 -138
  13. onvif/meta.py +2 -2
  14. onvif/operator.py +104 -64
  15. onvif/services/accesscontrol.py +8 -6
  16. onvif/services/accessrules.py +8 -6
  17. onvif/services/actionengine.py +8 -6
  18. onvif/services/analytics/__init__.py +1 -0
  19. onvif/services/analytics/analytics.py +8 -6
  20. onvif/services/analytics/ruleengine.py +8 -6
  21. onvif/services/analyticsdevice.py +10 -8
  22. onvif/services/appmgmt.py +9 -7
  23. onvif/services/authenticationbehavior.py +9 -7
  24. onvif/services/credential.py +8 -6
  25. onvif/services/deviceio.py +18 -12
  26. onvif/services/devicemgmt.py +54 -49
  27. onvif/services/display.py +8 -6
  28. onvif/services/doorcontrol.py +8 -6
  29. onvif/services/events/__init__.py +1 -0
  30. onvif/services/events/events.py +8 -6
  31. onvif/services/events/notification.py +9 -24
  32. onvif/services/events/pausable_subscription.py +13 -9
  33. onvif/services/events/pullpoint.py +8 -6
  34. onvif/services/events/subscription.py +10 -9
  35. onvif/services/imaging.py +36 -32
  36. onvif/services/media.py +8 -6
  37. onvif/services/media2.py +9 -6
  38. onvif/services/provisioning.py +9 -7
  39. onvif/services/ptz.py +8 -6
  40. onvif/services/receiver.py +8 -6
  41. onvif/services/recording.py +8 -6
  42. onvif/services/replay.py +8 -6
  43. onvif/services/schedule.py +8 -6
  44. onvif/services/search.py +15 -6
  45. onvif/services/security/__init__.py +1 -0
  46. onvif/services/security/advancedsecurity.py +11 -9
  47. onvif/services/security/authorizationserver.py +9 -7
  48. onvif/services/security/dot1x.py +9 -7
  49. onvif/services/security/jwt.py +9 -7
  50. onvif/services/security/keystore.py +9 -7
  51. onvif/services/security/mediasigning.py +9 -7
  52. onvif/services/security/tlsserver.py +9 -7
  53. onvif/services/thermal.py +8 -6
  54. onvif/services/uplink.py +8 -6
  55. onvif/utils/__init__.py +9 -4
  56. onvif/utils/discovery.py +150 -51
  57. onvif/utils/error_handlers.py +207 -97
  58. onvif/utils/exceptions.py +64 -44
  59. onvif/utils/plugins.py +500 -0
  60. onvif/utils/service.py +106 -87
  61. onvif/utils/wsdl.py +101 -57
  62. onvif/utils/zeep.py +267 -286
  63. onvif_python-0.4.0.dist-info/METADATA +113 -0
  64. {onvif_python-0.3.0.dist-info → onvif_python-0.4.0.dist-info}/RECORD +68 -59
  65. {onvif_python-0.3.0.dist-info → onvif_python-0.4.0.dist-info}/licenses/LICENSE.md +1 -1
  66. onvif/utils/parser.py +0 -139
  67. onvif/utils/xml_capture.py +0 -222
  68. onvif_python-0.3.0.dist-info/METADATA +0 -1219
  69. {onvif_python-0.3.0.dist-info → onvif_python-0.4.0.dist-info}/WHEEL +0 -0
  70. {onvif_python-0.3.0.dist-info → onvif_python-0.4.0.dist-info}/entry_points.txt +0 -0
  71. {onvif_python-0.3.0.dist-info → onvif_python-0.4.0.dist-info}/top_level.txt +0 -0
onvif/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- """ONVIF Python"""
1
+ """ONVIF Python."""
2
2
 
3
3
  from onvif.cli import main as ONVIFCLI
4
4
  from onvif.client import ONVIFClient
@@ -6,10 +6,12 @@ from onvif.operator import CacheMode
6
6
  from onvif.utils import (
7
7
  ONVIFWSDL,
8
8
  ONVIFDiscovery,
9
- ONVIFErrorHandler,
10
9
  ONVIFOperationException,
11
10
  ONVIFParser,
12
11
  ZeepPatcher,
12
+ ignore_unsupported,
13
+ is_action_not_supported,
14
+ safe_call,
13
15
  )
14
16
 
15
17
  __all__ = [
@@ -17,9 +19,11 @@ __all__ = [
17
19
  "CacheMode",
18
20
  "ONVIFWSDL",
19
21
  "ONVIFOperationException",
20
- "ONVIFErrorHandler",
21
22
  "ZeepPatcher",
22
23
  "ONVIFCLI",
23
24
  "ONVIFDiscovery",
24
25
  "ONVIFParser",
26
+ "ignore_unsupported",
27
+ "is_action_not_supported",
28
+ "safe_call",
25
29
  ]
@@ -0,0 +1,21 @@
1
+ """ONVIF CLI helpers."""
2
+
3
+ from onvif.cli.helpers.discovery import discover_devices, select_device_interactive
4
+ from onvif.cli.helpers.execution import execute_command, setup_warning_format
5
+ from onvif.cli.helpers.files import save_output_to_file
6
+ from onvif.cli.helpers.products import search_products
7
+ from onvif.cli.helpers.wsdl_parser import (
8
+ get_method_documentation,
9
+ get_operation_type_info,
10
+ )
11
+
12
+ __all__ = [
13
+ "search_products",
14
+ "save_output_to_file",
15
+ "discover_devices",
16
+ "select_device_interactive",
17
+ "execute_command",
18
+ "setup_warning_format",
19
+ "get_method_documentation",
20
+ "get_operation_type_info",
21
+ ]
@@ -0,0 +1,156 @@
1
+ """ONVIF discovery helpers."""
2
+
3
+ from datetime import datetime
4
+
5
+ from onvif.cli.utils import colorize
6
+ from onvif.utils import ONVIFDiscovery
7
+
8
+
9
+ def discover_devices(
10
+ timeout: int = 4,
11
+ interface: str | None = None,
12
+ prefer_https: bool = False,
13
+ filter_term: str | None = None,
14
+ ) -> list:
15
+ """Discover ONVIF devices on the network using WS-Discovery.
16
+
17
+ Args:
18
+ timeout (int): Discovery timeout in seconds
19
+ interface (str | None): Network interface to use for discovery
20
+ prefer_https (bool): If True, prioritize HTTPS XAddrs when available
21
+ filter_term (str | None): Optional search term to filter devices by types or scopes (case-insensitive)
22
+
23
+ Returns:
24
+ List of discovered devices with connection info
25
+ """
26
+ # Use ONVIFDiscovery class
27
+ discovery = ONVIFDiscovery(timeout=timeout, interface=interface)
28
+
29
+ print(f"\n{colorize('Discovering ONVIF devices on network...', 'yellow')}")
30
+ print(f"Network interface: {colorize(discovery.get_local_ip(), 'white')}")
31
+ print(f"Timeout: {timeout}s")
32
+ if filter_term:
33
+ print(f"Filter: {colorize(filter_term, 'yellow')}")
34
+ print()
35
+
36
+ devices = discovery.discover(prefer_https=prefer_https, search=filter_term)
37
+
38
+ return devices
39
+
40
+
41
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
42
+ def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
43
+ """Display devices and allow user to select one interactively.
44
+
45
+ Args:
46
+ devices (list): Discovered devices
47
+
48
+ Returns:
49
+ Tuple of (host, port, use_https) or None if cancelled
50
+ """
51
+ if not devices:
52
+ print(f"\n{colorize('No ONVIF devices found.', 'red')}")
53
+ return None
54
+
55
+ print(f"{colorize(f'Found {len(devices)} ONVIF device(s):', 'green')}")
56
+
57
+ for idx, device in enumerate(devices, 1):
58
+ idx_str = colorize(f"[{idx}]", "yellow")
59
+ protocol = "https" if device.get("use_https", False) else "http"
60
+ host_port = f"{device['host']}:{device['port']}"
61
+ protocol_indicator = (
62
+ colorize("🔒 HTTPS", "green")
63
+ if device.get("use_https", False)
64
+ else colorize("HTTP", "white")
65
+ )
66
+ print(f"\n{idx_str} {colorize(host_port, 'yellow')} ({protocol_indicator})")
67
+
68
+ # Remove uuid: or urn:uuid: prefix from EPR
69
+ epr_display = device["epr"]
70
+ if epr_display.startswith("urn:uuid:"):
71
+ epr_display = epr_display.replace("urn:uuid:", "")
72
+ elif epr_display.startswith("uuid:"):
73
+ epr_display = epr_display.replace("uuid:", "")
74
+ print(f" [uuid] {epr_display}")
75
+
76
+ if device.get("hostname"):
77
+ print(f" [hostname] {device["hostname"]}")
78
+
79
+ if device.get("xaddrs"):
80
+ xaddrs_parts = [f"[{xaddr}]" for xaddr in device["xaddrs"]]
81
+ print(f" [xaddrs] {' '.join(xaddrs_parts)}")
82
+
83
+ if device.get("date_time"):
84
+ utc = (
85
+ datetime.fromisoformat(device["date_time"].get("utc")).strftime(
86
+ "%H:%M:%S %d-%m-%Y"
87
+ )
88
+ if device["date_time"].get("utc")
89
+ else ""
90
+ )
91
+ local = (
92
+ datetime.fromisoformat(device["date_time"].get("local")).strftime(
93
+ "%H:%M:%S %d-%m-%Y"
94
+ )
95
+ if device["date_time"].get("local")
96
+ else ""
97
+ )
98
+ print(
99
+ f" [date_time]"
100
+ f'{" [UTC: " + utc + "]" if utc else ""}'
101
+ f'{" [Local: " + local + "]" if local else ""}'
102
+ )
103
+
104
+ if device.get("types"):
105
+ types_parts = [f"[{t}]" for t in device["types"]]
106
+ print(f" [types] {' '.join(types_parts)}")
107
+
108
+ if device.get("services"):
109
+ service = [colorize(f"[{t}]", "cyan") for t in device["services"]]
110
+ print(f" [services] {' '.join(service)}")
111
+
112
+ if device.get("scopes"):
113
+ scope_parts = []
114
+ for scope in device["scopes"]:
115
+ # Remove the prefix "onvif://www.onvif.org/" if present
116
+ if scope.startswith("onvif://www.onvif.org/"):
117
+ simplified = scope.replace("onvif://www.onvif.org/", "")
118
+ scope_parts.append(f"[{simplified}]")
119
+ else:
120
+ # Keep other scopes as-is (e.g., http:123)
121
+ scope_parts.append(f"[{scope}]")
122
+
123
+ if scope_parts:
124
+ print(f" [scopes] {' '.join(scope_parts)}")
125
+
126
+ # Simple selection (without arrow keys for cross-platform compatibility)
127
+ while True:
128
+ try:
129
+ selection = input(
130
+ f"\nSelect device number {colorize(f'1-{len(devices)}', 'white')} or {colorize('q', 'white')} to quit: "
131
+ )
132
+
133
+ if selection.lower() == "q":
134
+ return None
135
+
136
+ idx = int(selection)
137
+ if 1 <= idx <= len(devices):
138
+ selected = devices[idx - 1]
139
+ protocol = "https" if selected.get("use_https", False) else "http"
140
+ host_port = f"{selected['host']}:{selected['port']}"
141
+ print(
142
+ f"\n{colorize('Selected:', 'green')} {colorize(protocol, 'cyan')}://{colorize(host_port, 'yellow')}"
143
+ )
144
+
145
+ return (
146
+ selected["host"],
147
+ selected["port"],
148
+ selected.get("use_https", False),
149
+ )
150
+
151
+ print(colorize("Invalid selection. Please try again.", "red"))
152
+
153
+ except ValueError:
154
+ print(colorize("Invalid input. Please enter a number.", "red"))
155
+ except (EOFError, KeyboardInterrupt):
156
+ return None
@@ -0,0 +1,57 @@
1
+ """ONVIF command execution helpers."""
2
+
3
+ import warnings
4
+ from typing import Any
5
+
6
+ from onvif.cli.utils import colorize, parse_json_params
7
+ from onvif.client import ONVIFClient
8
+
9
+
10
+ def execute_command(
11
+ client: ONVIFClient,
12
+ service_name: str,
13
+ method_name: str,
14
+ params_str: str | None = None,
15
+ ) -> Any:
16
+ """Execute a single ONVIF command.
17
+
18
+ Args:
19
+ client (ONVIFClient): ONVIFClient instance
20
+ service_name (str): Selected service for command execution
21
+ method_name (str): Method to execute by service
22
+ params_str (str | None): Provided params for method execution
23
+
24
+ Returns:
25
+ The operation response printed in terminal
26
+ """
27
+ # Get service instance
28
+ try:
29
+ service = getattr(client, service_name.lower())()
30
+ except AttributeError as e:
31
+ raise ValueError(f"{colorize('Unknown service:', 'red')} {service_name}") from e
32
+
33
+ # Get method
34
+ try:
35
+ method = getattr(service, method_name)
36
+ except AttributeError as e:
37
+ raise ValueError(
38
+ f"{colorize('Unknown method', 'red')} '{method_name}' for service '{service_name}'"
39
+ ) from e
40
+
41
+ # Parse parameters
42
+ params = parse_json_params(params_str) if params_str else {}
43
+
44
+ # Execute method
45
+ return method(**params)
46
+
47
+
48
+ def setup_warning_format() -> None:
49
+ """Setup custom warning format to show clean, concise warnings."""
50
+
51
+ def custom_warning_format(
52
+ message, category, filename, lineno, line=None
53
+ ): # pylint: disable=unused-argument
54
+ # Show only the warning message without file path and line number
55
+ return f"{category.__name__}: {message}\n"
56
+
57
+ warnings.formatwarning = custom_warning_format
@@ -0,0 +1,191 @@
1
+ """File processing helpers."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from onvif.cli.utils import colorize
10
+ from onvif.client import ONVIFClient
11
+
12
+
13
+ def save_output_to_file(
14
+ result: Any, output_path: str, debug_mode: bool, client: ONVIFClient
15
+ ) -> None:
16
+ """Save command output to file in appropriate format based on file extension.
17
+
18
+ Args:
19
+ result (Any): The ONVIF command result
20
+ output_path (str): Path to output file
21
+ debug_mode (bool): Whether debug mode is enabled (for XML capture)
22
+ client (ONVIFClient): ONVIFClient instance (for accessing XML plugin)
23
+ """
24
+ try:
25
+ # Determine output format based on file extension
26
+ _, ext = os.path.splitext(output_path.lower())
27
+
28
+ if ext == ".json":
29
+ # Prepare output data
30
+ output_data = {}
31
+
32
+ # JSON format
33
+ output_data["result"] = _serialize_for_json(result)
34
+ output_data["timestamp"] = datetime.now(timezone.utc).isoformat()
35
+ output_data["raw_result"] = str(result) # Add raw string as fallback
36
+
37
+ # Add XML data if debug mode is enabled and XML plugin is available
38
+ if debug_mode and client.xml_plugin:
39
+ output_data["debug"] = {
40
+ "last_request_xml": client.xml_plugin.last_sent_xml,
41
+ "last_response_xml": client.xml_plugin.last_received_xml,
42
+ "last_operation": client.xml_plugin.last_operation,
43
+ }
44
+
45
+ with open(output_path, "w", encoding="utf-8") as f:
46
+ json.dump(output_data, f, indent=2, ensure_ascii=False)
47
+
48
+ elif ext == ".xml":
49
+ # XML format - prioritize raw SOAP XML over parsed result
50
+ if client.xml_plugin and client.xml_plugin.last_received_xml:
51
+ # Save the raw SOAP response XML with minimal wrapper
52
+ content = f"""<?xml version="1.0" encoding="UTF-8"?>
53
+ <!-- ONVIF SOAP Response -->
54
+ <!-- Timestamp: {datetime.now(timezone.utc).isoformat()} -->
55
+ <!-- Operation: {client.xml_plugin.last_operation or 'Unknown'} -->
56
+
57
+ {client.xml_plugin.last_received_xml}
58
+ """
59
+ else:
60
+ # Fallback: Simple XML wrapper for the parsed result
61
+ content = f"""<?xml version="1.0" encoding="UTF-8"?>
62
+ <!-- ONVIF Command Output (Parsed Result) -->
63
+ <!-- Timestamp: {datetime.now(timezone.utc).isoformat()} -->
64
+ <!-- Note: Raw SOAP XML not available. Enable --debug for full SOAP capture. -->
65
+ <onvif_result>
66
+ <![CDATA[
67
+ {result!s}
68
+ ]]>
69
+ </onvif_result>
70
+ """
71
+
72
+ with open(output_path, "w", encoding="utf-8") as f:
73
+ f.write(content)
74
+
75
+ else:
76
+ # Plain text format (default)
77
+ content = "ONVIF Command Output\n"
78
+ content += f"Timestamp: {datetime.now(timezone.utc).isoformat()}\n"
79
+ content += f"{'='*50}\n\n"
80
+ content += str(result)
81
+
82
+ # Add debug information if available
83
+ if debug_mode and client.xml_plugin:
84
+ content += f"\n\n{'='*50}\n"
85
+ content += "DEBUG INFORMATION\n"
86
+ content += f"{'='*50}\n"
87
+ if client.xml_plugin.last_operation:
88
+ content += f"Operation: {client.xml_plugin.last_operation}\n\n"
89
+ if client.xml_plugin.last_sent_xml:
90
+ content += "SOAP Request:\n"
91
+ content += client.xml_plugin.last_sent_xml + "\n\n"
92
+ if client.xml_plugin.last_received_xml:
93
+ content += "SOAP Response:\n"
94
+ content += client.xml_plugin.last_received_xml + "\n"
95
+
96
+ with open(output_path, "w", encoding="utf-8") as f:
97
+ f.write(content)
98
+
99
+ except (AttributeError, ValueError, OSError) as e:
100
+ print(f"{colorize('Error saving output:', 'red')} {e}", file=sys.stderr)
101
+ # Still print the result to console if file save fails
102
+ print(str(result))
103
+
104
+
105
+ # pylint: disable=too-many-return-statements,too-many-branches,too-many-nested-blocks
106
+ def _serialize_for_json(obj: Any) -> Any:
107
+ """Recursively serialize ONVIF objects for JSON output.
108
+
109
+ Args:
110
+ obj: Object to serialize
111
+
112
+ Returns:
113
+ JSON-serializable representation of the object
114
+ """
115
+ if obj is None:
116
+ return None
117
+ if isinstance(obj, (str, int, float, bool)):
118
+ return obj
119
+ if isinstance(obj, datetime):
120
+ return obj.isoformat()
121
+ if isinstance(obj, (list, tuple)):
122
+ return [_serialize_for_json(item) for item in obj]
123
+ if isinstance(obj, dict):
124
+ return {key: _serialize_for_json(value) for key, value in obj.items()}
125
+
126
+ # Check if this is a Zeep object (has _xsd_type attribute)
127
+ if hasattr(obj, "_xsd_type"):
128
+ result = {}
129
+ # Try to get all elements from XSD type
130
+ if hasattr(obj._xsd_type, "elements"): # pylint: disable=protected-access
131
+ for (
132
+ elem_name,
133
+ _,
134
+ ) in obj._xsd_type.elements: # pylint: disable=protected-access
135
+ try:
136
+ value = getattr(obj, elem_name, None)
137
+ if value is not None:
138
+ result[elem_name] = _serialize_for_json(value)
139
+ except (AttributeError, TypeError):
140
+ # Skip elements that can't be accessed or have type issues
141
+ pass
142
+
143
+ # Also try regular attributes
144
+ for attr_name in dir(obj):
145
+ if not attr_name.startswith("_") and not callable(
146
+ getattr(obj, attr_name, None)
147
+ ):
148
+ try:
149
+ attr_value = getattr(obj, attr_name)
150
+ if attr_value is not None and attr_name not in result:
151
+ result[attr_name] = _serialize_for_json(attr_value)
152
+ except (AttributeError, TypeError):
153
+ # Skip attributes that can't be accessed or have type issues
154
+ pass
155
+
156
+ return result
157
+
158
+ if hasattr(obj, "__dict__"):
159
+ # Handle regular objects with attributes
160
+ result = {}
161
+ for key, value in obj.__dict__.items():
162
+ if not key.startswith("_"): # Skip private attributes
163
+ result[key] = _serialize_for_json(value)
164
+
165
+ # If result is empty, try to get attributes using dir()
166
+ if not result:
167
+ for attr_name in dir(obj):
168
+ if not attr_name.startswith("_") and not callable(
169
+ getattr(obj, attr_name, None)
170
+ ):
171
+ try:
172
+ attr_value = getattr(obj, attr_name)
173
+ if attr_value is not None:
174
+ result[attr_name] = _serialize_for_json(attr_value)
175
+ except (AttributeError, TypeError):
176
+ # Skip attributes that can't be accessed or have type issues
177
+ pass
178
+
179
+ return result
180
+
181
+ if hasattr(obj, "_value_1"):
182
+ # Handle zeep objects with special structure
183
+ return _serialize_for_json(obj._value_1) # pylint: disable=protected-access
184
+
185
+ # Try to convert to dict using vars() if available
186
+ try:
187
+ obj_dict = vars(obj)
188
+ return _serialize_for_json(obj_dict)
189
+ except TypeError:
190
+ # Fallback to string representation
191
+ return str(obj)
@@ -0,0 +1,220 @@
1
+ """CLI messages."""
2
+
3
+ from onvif.cli.utils import (
4
+ colorize,
5
+ )
6
+ from onvif.meta import __repository__, __version__
7
+
8
+
9
+ def print_interactive_intro(args, device_info_text) -> str:
10
+ """Print interactive shell introduction."""
11
+ # Welcome message with enhanced info
12
+ banner_lines = [
13
+ " ____ _ ___ ____________",
14
+ " / __ \\/ | / / | / / _/ ____/",
15
+ " / / / / |/ /| | / // // /_ ",
16
+ "/ /_/ / /| / | |/ // // __/ ",
17
+ f"\\____/_/ |_/ |___/___/_/ v{__version__}",
18
+ " ",
19
+ ]
20
+
21
+ banner = "\n".join(colorize(line, "cyan") for line in banner_lines)
22
+ repo_info = "\n".join(
23
+ [
24
+ colorize("Star ⭐ this repo", "white"),
25
+ colorize(__repository__, "white"),
26
+ ]
27
+ )
28
+
29
+ # Build connection and CLI options info
30
+ options_info = []
31
+
32
+ # Connection options
33
+ if hasattr(args, "https") and args.https:
34
+ options_info.append(f" Use HTTPS : {colorize('True', 'green')}")
35
+
36
+ if hasattr(args, "no_verify_ssl") and args.no_verify_ssl:
37
+ options_info.append(f" Verify SSL : {colorize('False', 'red')}")
38
+
39
+ if hasattr(args, "timeout") and args.timeout != 10: # 10 is default
40
+ options_info.append(
41
+ f" Timeout : {colorize(f'{args.timeout}s', 'yellow')}"
42
+ )
43
+
44
+ # CLI options
45
+ if hasattr(args, "debug") and args.debug:
46
+ options_info.append(f" Debug Mode : {colorize('True', 'green')}")
47
+
48
+ if hasattr(args, "no_patch") and args.no_patch:
49
+ options_info.append(f" ZeepPatcher : {colorize('Disabled', 'red')}")
50
+
51
+ if hasattr(args, "wsdl") and args.wsdl:
52
+ options_info.append(f" Custom WSDL : {colorize(args.wsdl, 'yellow')}")
53
+
54
+ if (
55
+ hasattr(args, "health_check_interval") and args.health_check_interval != 10
56
+ ): # 10 is default
57
+ options_info.append(
58
+ f" Health Check : every {colorize(f'{args.health_check_interval}s', 'yellow')}"
59
+ )
60
+
61
+ # Format options info
62
+ options_display = ""
63
+ if options_info:
64
+ options_display = "\n" + "\n".join(options_info)
65
+
66
+ terminal_header = colorize("\n[ONVIF Terminal Client]", "yellow")
67
+
68
+ intro = (
69
+ f"{banner}\n"
70
+ f"{repo_info}\n"
71
+ f"{terminal_header}\n"
72
+ f" Connected to : {colorize(f'{args.host}:{args.port}', 'yellow')}\n"
73
+ f" Auth Method : {colorize(f'{'HTTP Digest' if args.digest else 'WS-UsernameToken'}', 'yellow')}"
74
+ f"{options_display}{device_info_text}\n\n"
75
+ f"{colorize('[Quick Start]', 'green')}\n"
76
+ f" - Type {colorize('dev', 'yellow')} + {colorize('TAB', 'yellow')} to see `devicemgmt` suggestion\n"
77
+ f" - Type {colorize('devicemgmt', 'yellow')} to enter device management service\n"
78
+ f" - Use {colorize('TAB', 'yellow')} completion for commands and methods\n\n"
79
+ f"{colorize('[Typical Commands]', 'magenta')}\n"
80
+ f" - help : Show help information\n"
81
+ f" - ls : List commands/services/methods in grid format\n"
82
+ f" - <service> : Enter service mode (e.g., devicemgmt)\n"
83
+ f" - up : Exit service mode (go up one level)\n"
84
+ f" - info : Show current device and connection info\n"
85
+ f" - exit : Exit shell\n\n"
86
+ f"Use {colorize('TAB', 'yellow')} for auto-completion. "
87
+ f"Type partial commands to see suggestions.\n"
88
+ )
89
+
90
+ return intro
91
+
92
+
93
+ INTERACTIVE_HELP = f"""
94
+ {colorize(f'ONVIF Interactive Shell — v{__version__}', 'cyan')}\n{colorize(__repository__, 'white')}
95
+
96
+ {colorize('Basic Commands:', 'yellow')}
97
+ capabilities, caps - Show device capabilities
98
+ services - Show available services with details
99
+ info - Show connection and device information
100
+ exit - Exit the shell
101
+ shortcuts - Show available shortcuts
102
+
103
+ {colorize('Navigation Commands:', 'yellow')}
104
+ <service> - Enter service mode (e.g., devicemgmt, media)
105
+ <service> <argument> - Enter service mode with argument (e.g. pullpoint SubscriptionRef=<value>)
106
+ cd <service> - Enter service mode (alias)
107
+ ls - List commands/services/methods in grid format
108
+ up - Exit current service mode (go up one level)
109
+ clear - Clear terminal screen
110
+ help <command> - Show help for a specific command
111
+
112
+ {colorize('Service Mode Commands:', 'yellow')}
113
+ desc <method> - Show method documentation
114
+ type <method> - Show input/output types from WSDL
115
+
116
+ {colorize('Method Execution:', 'yellow')}
117
+ <method> - Execute method without parameters
118
+ <method> {{"param": "value"}} - Execute method with JSON parameters
119
+ <method> param=value - Execute method with simple parameters
120
+
121
+ {colorize('Data Management:', 'yellow')}
122
+ store <name> - Store last result with a name
123
+ show <name> - Show stored data
124
+ show <name>[0] - Show element at index (for lists)
125
+ show <name>.attribute - Show specific attribute
126
+ show - List all stored data
127
+ rm <name> - Remove stored data by name
128
+ cls - Clear all stored data
129
+
130
+ {colorize('Using Stored Data in Methods:', 'yellow')}
131
+ Use $variable syntax to reference stored data in method parameters:
132
+ - $profiles[0].token - Access list element and attribute
133
+ - $profiles[0].VideoSourceConfiguration.SourceToken
134
+
135
+ Example:
136
+ GetProfiles - Get profiles
137
+ store profiles - Store result
138
+ show profiles[0].token - Show first profile token
139
+ GetImagingSettings VideoSourceToken=$profiles[0].VideoSourceConfiguration.SourceToken
140
+
141
+ {colorize('Debug Commands:', 'yellow')}
142
+ debug - Show last SOAP request & response (if --debug enabled)
143
+
144
+ {colorize('Tab Completion:', 'yellow')}
145
+ Use {colorize('TAB', 'yellow')} key for auto-completion of commands, services, and methods
146
+ Type partial commands to see suggestions
147
+
148
+ {colorize('Examples:', 'yellow')}
149
+ 192.168.1.17:8000 > caps # Show capabilities
150
+ 192.168.1.17:8000 > dev<TAB> # Completes to 'devicemgmt'
151
+ 192.168.1.17:8000 > cd devicemgmt # Enter device management
152
+ 192.168.1.17:8000/devicemgmt > Get<TAB> # Show methods starting with 'Get'
153
+ 192.168.1.17:8000/devicemgmt > GetServices {{"IncludeCapability": true}}
154
+ 192.168.1.17:8000/devicemgmt > GetServices IncludeCapability=True
155
+ 192.168.1.17:8000/devicemgmt > store services_info
156
+ 192.168.1.17:8000/devicemgmt > up # Exit service mode
157
+ 192.168.1.17:8000 > # Back to root context
158
+ """
159
+
160
+ INTERACTIVE_SHORTCUTS = f"""
161
+ {colorize('Available Shortcuts:', 'cyan')}
162
+
163
+ {colorize('Navigation:', 'yellow')}
164
+ <service> - Enter service mode (e.g., devicemgmt, media)
165
+ cd <service> - Enter service (same as '<service>')
166
+ ls - List commands/services in grid format (like TAB)
167
+ up - Go up one level
168
+ clear - Clear terminal screen
169
+ help <command> - Show help for a command
170
+
171
+ {colorize('Service Mode Commands:', 'yellow')}
172
+ desc <method> - Show method documentation
173
+ type <method> - Show input/output types from WSDL
174
+
175
+ {colorize('Quick Access:', 'yellow')}
176
+ caps - Show capabilities (same as 'capabilities')
177
+
178
+ {colorize('Tab Completion Examples:', 'yellow')}
179
+ dev<TAB> - Completes to 'devicemgmt'
180
+ med<TAB> - Completes to 'media'
181
+ Get<TAB> - Shows all methods starting with 'Get'
182
+ """
183
+
184
+ # pylint: disable=line-too-long
185
+ CLI_EPILOG = f"""
186
+ Examples:
187
+ # Product search
188
+ {colorize('onvif', 'yellow')} --search c210
189
+ {colorize('onvif', 'yellow')} -s "axis camera"
190
+ {colorize('onvif', 'yellow')} --search hikvision --page 2 --per-page 5
191
+
192
+ # Discover ONVIF devices on network
193
+ {colorize('onvif', 'yellow')} --discover --username admin --password admin123 --interactive
194
+ {colorize('onvif', 'yellow')} media GetProfiles --discover --username admin
195
+ {colorize('onvif', 'yellow')} -d --interface 192.168.1.77 -i
196
+
197
+ # Discover with filtering
198
+ {colorize('onvif', 'yellow')} --discover --filter ptz --interactive
199
+ {colorize('onvif', 'yellow')} -d -f "C210" -i
200
+ {colorize('onvif', 'yellow')} -d -f "audio_encoder" -u admin -p admin123 -i
201
+
202
+ # Direct command execution
203
+ {colorize('onvif', 'yellow')} devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
204
+ {colorize('onvif', 'yellow')} ptz ContinuousMove ProfileToken=Profile_1 Velocity={{'PanTilt': {{'x': -0.1, 'y': 0}}}} -H 192.168.1.17 -P 8000 -u admin -p admin123
205
+
206
+ # Save output to file
207
+ {colorize('onvif', 'yellow')} devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
208
+ {colorize('onvif', 'yellow')} media GetProfiles --host 192.168.1.17 --port 8000 --username admin --password admin123 --output profiles.xml
209
+ {colorize('onvif', 'yellow')} ptz GetConfigurations --host 192.168.1.17 --port 8000 --username admin --password admin123 --output ptz_config.txt --debug
210
+
211
+ # Interactive mode
212
+ {colorize('onvif', 'yellow')} --host 192.168.1.17 --port 8000 --username admin --password admin123 --interactive
213
+
214
+ # Prompting for username and password
215
+ # (if not provided)
216
+ {colorize('onvif', 'yellow')} -H 192.168.1.17 -P 8000 -i
217
+
218
+ # Using HTTPS
219
+ {colorize('onvif', 'yellow')} media GetProfiles --host camera.example.com --port 443 --username admin --password admin123 --https
220
+ """