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.
- onvif/__init__.py +1 -1
- onvif/cli/helpers/__init__.py +21 -0
- onvif/cli/helpers/discovery.py +124 -0
- onvif/cli/helpers/execution.py +57 -0
- onvif/cli/helpers/files.py +189 -0
- onvif/cli/helpers/messages.py +221 -0
- onvif/cli/helpers/products.py +292 -0
- onvif/cli/helpers/wsdl_parser.py +968 -0
- onvif/cli/interactive.py +45 -199
- onvif/cli/main.py +22 -675
- onvif/cli/utils.py +25 -953
- onvif/client.py +185 -122
- onvif/meta.py +1 -1
- onvif/operator.py +34 -26
- onvif/services/accesscontrol.py +6 -5
- onvif/services/accessrules.py +6 -5
- onvif/services/actionengine.py +6 -5
- onvif/services/analytics/__init__.py +0 -0
- onvif/services/analytics/analytics.py +6 -5
- onvif/services/analytics/ruleengine.py +6 -5
- onvif/services/analyticsdevice.py +8 -7
- onvif/services/appmgmt.py +7 -6
- onvif/services/authenticationbehavior.py +6 -5
- onvif/services/credential.py +6 -5
- onvif/services/deviceio.py +15 -11
- onvif/services/devicemgmt.py +49 -48
- onvif/services/display.py +6 -5
- onvif/services/doorcontrol.py +6 -5
- onvif/services/events/__init__.py +0 -0
- onvif/services/events/events.py +6 -5
- onvif/services/events/notification.py +4 -5
- onvif/services/events/pausable_subscription.py +4 -4
- onvif/services/events/pullpoint.py +6 -5
- onvif/services/events/subscription.py +4 -4
- onvif/services/imaging.py +30 -29
- onvif/services/media.py +6 -5
- onvif/services/media2.py +6 -5
- onvif/services/provisioning.py +6 -5
- onvif/services/ptz.py +6 -5
- onvif/services/receiver.py +6 -5
- onvif/services/recording.py +6 -5
- onvif/services/replay.py +6 -5
- onvif/services/schedule.py +6 -5
- onvif/services/search.py +6 -5
- onvif/services/security/__init__.py +0 -0
- onvif/services/security/advancedsecurity.py +8 -7
- onvif/services/security/authorizationserver.py +6 -5
- onvif/services/security/dot1x.py +6 -5
- onvif/services/security/jwt.py +6 -5
- onvif/services/security/keystore.py +6 -5
- onvif/services/security/mediasigning.py +6 -5
- onvif/services/security/tlsserver.py +6 -5
- onvif/services/thermal.py +6 -5
- onvif/services/uplink.py +6 -5
- onvif/utils/__init__.py +1 -1
- onvif/utils/discovery.py +29 -44
- onvif/utils/error_handlers.py +8 -5
- onvif/utils/exceptions.py +33 -28
- onvif/utils/{parser.py → plugins.py} +41 -8
- onvif/utils/service.py +7 -7
- onvif/utils/wsdl.py +48 -33
- onvif/utils/xml_capture.py +15 -18
- onvif/utils/zeep.py +9 -16
- onvif_python-0.3.1.dist-info/METADATA +103 -0
- {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/RECORD +69 -59
- {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/licenses/LICENSE.md +1 -1
- onvif_python-0.3.0.dist-info/METADATA +0 -1219
- {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/WHEEL +0 -0
- {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/entry_points.txt +0 -0
- {onvif_python-0.3.0.dist-info → onvif_python-0.3.1.dist-info}/top_level.txt +0 -0
onvif/__init__.py
CHANGED
|
@@ -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
|
+
"""
|