onvif-python 0.3.0__py3-none-any.whl → 0.3.1__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 (70) hide show
  1. onvif/__init__.py +1 -1
  2. onvif/cli/helpers/__init__.py +21 -0
  3. onvif/cli/helpers/discovery.py +124 -0
  4. onvif/cli/helpers/execution.py +57 -0
  5. onvif/cli/helpers/files.py +189 -0
  6. onvif/cli/helpers/messages.py +221 -0
  7. onvif/cli/helpers/products.py +292 -0
  8. onvif/cli/helpers/wsdl_parser.py +968 -0
  9. onvif/cli/interactive.py +45 -199
  10. onvif/cli/main.py +22 -675
  11. onvif/cli/utils.py +25 -953
  12. onvif/client.py +185 -122
  13. onvif/meta.py +1 -1
  14. onvif/operator.py +34 -26
  15. onvif/services/accesscontrol.py +6 -5
  16. onvif/services/accessrules.py +6 -5
  17. onvif/services/actionengine.py +6 -5
  18. onvif/services/analytics/__init__.py +0 -0
  19. onvif/services/analytics/analytics.py +6 -5
  20. onvif/services/analytics/ruleengine.py +6 -5
  21. onvif/services/analyticsdevice.py +8 -7
  22. onvif/services/appmgmt.py +7 -6
  23. onvif/services/authenticationbehavior.py +6 -5
  24. onvif/services/credential.py +6 -5
  25. onvif/services/deviceio.py +15 -11
  26. onvif/services/devicemgmt.py +49 -48
  27. onvif/services/display.py +6 -5
  28. onvif/services/doorcontrol.py +6 -5
  29. onvif/services/events/__init__.py +0 -0
  30. onvif/services/events/events.py +6 -5
  31. onvif/services/events/notification.py +4 -5
  32. onvif/services/events/pausable_subscription.py +4 -4
  33. onvif/services/events/pullpoint.py +6 -5
  34. onvif/services/events/subscription.py +4 -4
  35. onvif/services/imaging.py +30 -29
  36. onvif/services/media.py +6 -5
  37. onvif/services/media2.py +6 -5
  38. onvif/services/provisioning.py +6 -5
  39. onvif/services/ptz.py +6 -5
  40. onvif/services/receiver.py +6 -5
  41. onvif/services/recording.py +6 -5
  42. onvif/services/replay.py +6 -5
  43. onvif/services/schedule.py +6 -5
  44. onvif/services/search.py +6 -5
  45. onvif/services/security/__init__.py +0 -0
  46. onvif/services/security/advancedsecurity.py +8 -7
  47. onvif/services/security/authorizationserver.py +6 -5
  48. onvif/services/security/dot1x.py +6 -5
  49. onvif/services/security/jwt.py +6 -5
  50. onvif/services/security/keystore.py +6 -5
  51. onvif/services/security/mediasigning.py +6 -5
  52. onvif/services/security/tlsserver.py +6 -5
  53. onvif/services/thermal.py +6 -5
  54. onvif/services/uplink.py +6 -5
  55. onvif/utils/__init__.py +1 -1
  56. onvif/utils/discovery.py +29 -44
  57. onvif/utils/error_handlers.py +8 -5
  58. onvif/utils/exceptions.py +33 -28
  59. onvif/utils/{parser.py → plugins.py} +41 -8
  60. onvif/utils/service.py +7 -7
  61. onvif/utils/wsdl.py +48 -33
  62. onvif/utils/xml_capture.py +15 -18
  63. onvif/utils/zeep.py +9 -16
  64. onvif_python-0.3.1.dist-info/METADATA +103 -0
  65. {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/RECORD +69 -59
  66. {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/licenses/LICENSE.md +1 -1
  67. onvif_python-0.3.0.dist-info/METADATA +0 -1219
  68. {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/WHEEL +0 -0
  69. {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/entry_points.txt +0 -0
  70. {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.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
@@ -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,124 @@
1
+ """ONVIF discovery helpers."""
2
+
3
+ from onvif.cli.utils import colorize
4
+ from onvif.utils import ONVIFDiscovery
5
+
6
+
7
+ def discover_devices(
8
+ timeout: int = 4,
9
+ interface: str | None = None,
10
+ prefer_https: bool = False,
11
+ filter_term: str | None = None,
12
+ ) -> list:
13
+ """Discover ONVIF devices on the network using WS-Discovery.
14
+
15
+ Args:
16
+ timeout (int): Discovery timeout in seconds
17
+ interface (str | None): Network interface to use for discovery
18
+ prefer_https (bool): If True, prioritize HTTPS XAddrs when available
19
+ filter_term (str | None): Optional search term to filter devices by types or scopes
20
+
21
+ Returns:
22
+ List of discovered devices with connection info
23
+ """
24
+ # Use ONVIFDiscovery class
25
+ discovery = ONVIFDiscovery(timeout=timeout, interface=interface)
26
+
27
+ print(f"\n{colorize('Discovering ONVIF devices on network...', 'yellow')}")
28
+ print(f"Network interface: {colorize(discovery.get_local_ip(), 'white')}")
29
+ print(f"Timeout: {timeout}s")
30
+ if filter_term:
31
+ print(f"Filter: {colorize(filter_term, 'yellow')}")
32
+ print()
33
+
34
+ devices = discovery.discover(prefer_https=prefer_https, search=filter_term)
35
+
36
+ return devices
37
+
38
+
39
+ def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
40
+ """Display devices and allow user to select one interactively.
41
+
42
+ Args:
43
+ devices (list): Discovered devices
44
+
45
+ Returns:
46
+ Tuple of (host, port, use_https) or None if cancelled
47
+ """
48
+ if not devices:
49
+ print(f"\n{colorize('No ONVIF devices found.', 'red')}")
50
+ return None
51
+
52
+ print(f"{colorize(f'Found {len(devices)} ONVIF device(s):', 'green')}")
53
+
54
+ for idx, device in enumerate(devices, 1):
55
+ idx_str = colorize(f"[{idx}]", "yellow")
56
+ protocol = "https" if device.get("use_https", False) else "http"
57
+ host_port = f"{device['host']}:{device['port']}"
58
+ protocol_indicator = (
59
+ colorize("🔒 HTTPS", "green")
60
+ if device.get("use_https", False)
61
+ else colorize("HTTP", "white")
62
+ )
63
+ print(f"\n{idx_str} {colorize(host_port, 'yellow')} ({protocol_indicator})")
64
+
65
+ # Remove uuid: or urn:uuid: prefix from EPR
66
+ epr_display = device["epr"]
67
+ if epr_display.startswith("urn:uuid:"):
68
+ epr_display = epr_display.replace("urn:uuid:", "")
69
+ elif epr_display.startswith("uuid:"):
70
+ epr_display = epr_display.replace("uuid:", "")
71
+ print(f" [uuid] {epr_display}")
72
+
73
+ if device["xaddrs"]:
74
+ xaddrs_parts = [f"[{xaddr}]" for xaddr in device["xaddrs"]]
75
+ print(f" [xaddrs] {' '.join(xaddrs_parts)}")
76
+
77
+ if device["types"]:
78
+ types_parts = [f"[{t}]" for t in device["types"]]
79
+ print(f" [types] {' '.join(types_parts)}")
80
+
81
+ if device["scopes"]:
82
+ scope_parts = []
83
+ for scope in device["scopes"]:
84
+ # Remove the prefix "onvif://www.onvif.org/" if present
85
+ if scope.startswith("onvif://www.onvif.org/"):
86
+ simplified = scope.replace("onvif://www.onvif.org/", "")
87
+ scope_parts.append(f"[{simplified}]")
88
+ else:
89
+ # Keep other scopes as-is (e.g., http:123)
90
+ scope_parts.append(f"[{scope}]")
91
+
92
+ if scope_parts:
93
+ print(f" [scopes] {' '.join(scope_parts)}")
94
+
95
+ # Simple selection (without arrow keys for cross-platform compatibility)
96
+ while True:
97
+ try:
98
+ selection = input(
99
+ f"\nSelect device number {colorize(f'1-{len(devices)}', 'white')} or {colorize('q', 'white')} to quit: "
100
+ )
101
+
102
+ if selection.lower() == "q":
103
+ return None
104
+
105
+ idx = int(selection)
106
+ if 1 <= idx <= len(devices):
107
+ selected = devices[idx - 1]
108
+ protocol = "https" if selected.get("use_https", False) else "http"
109
+ host_port = f"{selected['host']}:{selected['port']}"
110
+ print(
111
+ f"\n{colorize('Selected:', 'green')} {colorize(protocol, 'cyan')}://{colorize(host_port, 'yellow')}"
112
+ )
113
+ return (
114
+ selected["host"],
115
+ selected["port"],
116
+ selected.get("use_https", False),
117
+ )
118
+ else:
119
+ print(colorize("Invalid selection. Please try again.", "red"))
120
+
121
+ except ValueError:
122
+ print(colorize("Invalid input. Please enter a number.", "red"))
123
+ except (EOFError, KeyboardInterrupt):
124
+ 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,189 @@
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
+ def _serialize_for_json(obj: Any) -> Any:
106
+ """Recursively serialize ONVIF objects for JSON output.
107
+
108
+ Args:
109
+ obj: Object to serialize
110
+
111
+ Returns:
112
+ JSON-serializable representation of the object
113
+ """
114
+ if obj is None:
115
+ return None
116
+ elif isinstance(obj, (str, int, float, bool)):
117
+ return obj
118
+ elif isinstance(obj, datetime):
119
+ return obj.isoformat()
120
+ elif isinstance(obj, (list, tuple)):
121
+ return [_serialize_for_json(item) for item in obj]
122
+ elif isinstance(obj, dict):
123
+ return {key: _serialize_for_json(value) for key, value in obj.items()}
124
+
125
+ # Check if this is a Zeep object (has _xsd_type attribute)
126
+ elif hasattr(obj, "_xsd_type"):
127
+ result = {}
128
+ # Try to get all elements from XSD type
129
+ if hasattr(obj._xsd_type, "elements"): # pylint: disable=protected-access
130
+ for (
131
+ elem_name,
132
+ _,
133
+ ) in obj._xsd_type.elements: # pylint: disable=protected-access
134
+ try:
135
+ value = getattr(obj, elem_name, None)
136
+ if value is not None:
137
+ result[elem_name] = _serialize_for_json(value)
138
+ except (AttributeError, TypeError):
139
+ # Skip elements that can't be accessed or have type issues
140
+ pass
141
+
142
+ # Also try regular attributes
143
+ for attr_name in dir(obj):
144
+ if not attr_name.startswith("_") and not callable(
145
+ getattr(obj, attr_name, None)
146
+ ):
147
+ try:
148
+ attr_value = getattr(obj, attr_name)
149
+ if attr_value is not None and attr_name not in result:
150
+ result[attr_name] = _serialize_for_json(attr_value)
151
+ except (AttributeError, TypeError):
152
+ # Skip attributes that can't be accessed or have type issues
153
+ pass
154
+
155
+ return result
156
+
157
+ elif hasattr(obj, "__dict__"):
158
+ # Handle regular objects with attributes
159
+ result = {}
160
+ for key, value in obj.__dict__.items():
161
+ if not key.startswith("_"): # Skip private attributes
162
+ result[key] = _serialize_for_json(value)
163
+
164
+ # If result is empty, try to get attributes using dir()
165
+ if not result:
166
+ for attr_name in dir(obj):
167
+ if not attr_name.startswith("_") and not callable(
168
+ getattr(obj, attr_name, None)
169
+ ):
170
+ try:
171
+ attr_value = getattr(obj, attr_name)
172
+ if attr_value is not None:
173
+ result[attr_name] = _serialize_for_json(attr_value)
174
+ except (AttributeError, TypeError):
175
+ # Skip attributes that can't be accessed or have type issues
176
+ pass
177
+
178
+ return result
179
+ elif hasattr(obj, "_value_1"):
180
+ # Handle zeep objects with special structure
181
+ return _serialize_for_json(obj._value_1) # pylint: disable=protected-access
182
+ else:
183
+ # Try to convert to dict using vars() if available
184
+ try:
185
+ obj_dict = vars(obj)
186
+ return _serialize_for_json(obj_dict)
187
+ except TypeError:
188
+ # Fallback to string representation
189
+ return str(obj)
@@ -0,0 +1,221 @@
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')}"
73
+ f"{options_display}{device_info_text}\n\n"
74
+ f"{colorize('[Quick Start]', 'green')}\n"
75
+ f" - Type {colorize('dev', 'yellow')} + {colorize('TAB', 'yellow')} to see `devicemgmt` suggestion\n"
76
+ f" - Type {colorize('devicemgmt', 'yellow')} to enter device management service\n"
77
+ f" - Use {colorize('TAB', 'yellow')} completion for commands and methods\n\n"
78
+ f"{colorize('[Typical Commands]', 'magenta')}\n"
79
+ f" - help : Show help information\n"
80
+ f" - ls : List commands/services/methods in grid format\n"
81
+ f" - <service> : Enter service mode (e.g., devicemgmt)\n"
82
+ f" - up : Exit service mode (go up one level)\n"
83
+ f" - info : Show current device and connection info\n"
84
+ f" - exit : Exit shell\n\n"
85
+ f"Use {colorize('TAB', 'yellow')} for auto-completion. "
86
+ f"Type partial commands to see suggestions.\n"
87
+ )
88
+
89
+ return intro
90
+
91
+
92
+ INTERACTIVE_HELP = f"""
93
+ {colorize(f'ONVIF Interactive Shell — v{__version__}', 'cyan')}\n{colorize(__repository__, 'white')}
94
+
95
+ {colorize('Basic Commands:', 'yellow')}
96
+ capabilities, caps - Show device capabilities
97
+ services - Show available services with details
98
+ info - Show connection and device information
99
+ exit - Exit the shell
100
+ shortcuts - Show available shortcuts
101
+
102
+ {colorize('Navigation Commands:', 'yellow')}
103
+ <service> - Enter service mode (e.g., devicemgmt, media)
104
+ <service> <argument> - Enter service mode with argument (e.g. pullpoint SubscriptionRef=<value>)
105
+ cd <service> - Enter service mode (alias)
106
+ ls - List commands/services/methods in grid format
107
+ up - Exit current service mode (go up one level)
108
+ pwd - Show current service context
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
+ pwd - Show current context
169
+ clear - Clear terminal screen
170
+ help <command> - Show help for a command
171
+
172
+ {colorize('Service Mode Commands:', 'yellow')}
173
+ desc <method> - Show method documentation
174
+ type <method> - Show input/output types from WSDL
175
+
176
+ {colorize('Quick Access:', 'yellow')}
177
+ caps - Show capabilities (same as 'capabilities')
178
+
179
+ {colorize('Tab Completion Examples:', 'yellow')}
180
+ dev<TAB> - Completes to 'devicemgmt'
181
+ med<TAB> - Completes to 'media'
182
+ Get<TAB> - Shows all methods starting with 'Get'
183
+ """
184
+
185
+ # pylint: disable=line-too-long
186
+ CLI_EPILOG = f"""
187
+ Examples:
188
+ # Product search
189
+ {colorize('onvif', 'yellow')} --search c210
190
+ {colorize('onvif', 'yellow')} -s "axis camera"
191
+ {colorize('onvif', 'yellow')} --search hikvision --page 2 --per-page 5
192
+
193
+ # Discover ONVIF devices on network
194
+ {colorize('onvif', 'yellow')} --discover --username admin --password admin123 --interactive
195
+ {colorize('onvif', 'yellow')} media GetProfiles --discover --username admin
196
+ {colorize('onvif', 'yellow')} -d -i
197
+
198
+ # Discover with filtering
199
+ {colorize('onvif', 'yellow')} --discover --filter ptz --interactive
200
+ {colorize('onvif', 'yellow')} -d -f "C210" -i
201
+ {colorize('onvif', 'yellow')} -d -f "audio_encoder" -u admin -p admin123 -i
202
+
203
+ # Direct command execution
204
+ {colorize('onvif', 'yellow')} devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
205
+ {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
206
+
207
+ # Save output to file
208
+ {colorize('onvif', 'yellow')} devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
209
+ {colorize('onvif', 'yellow')} media GetProfiles --host 192.168.1.17 --port 8000 --username admin --password admin123 --output profiles.xml
210
+ {colorize('onvif', 'yellow')} ptz GetConfigurations --host 192.168.1.17 --port 8000 --username admin --password admin123 --output ptz_config.txt --debug
211
+
212
+ # Interactive mode
213
+ {colorize('onvif', 'yellow')} --host 192.168.1.17 --port 8000 --username admin --password admin123 --interactive
214
+
215
+ # Prompting for username and password
216
+ # (if not provided)
217
+ {colorize('onvif', 'yellow')} -H 192.168.1.17 -P 8000 -i
218
+
219
+ # Using HTTPS
220
+ {colorize('onvif', 'yellow')} media GetProfiles --host camera.example.com --port 443 --username admin --password admin123 --https
221
+ """