Skip to content

Commit c845840

Browse files
committed
code(example): Simplify function docstrings and improve output formatting in device_discovery.py
1 parent b181b8a commit c845840

File tree

1 file changed

+28
-205
lines changed

1 file changed

+28
-205
lines changed

examples/device_discovery.py

Lines changed: 28 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import xml.etree.ElementTree as ET
2323
import sys
2424
import struct
25-
import time
2625

2726

2827
# WS-Discovery constants
@@ -48,12 +47,7 @@
4847

4948

5049
def get_network_interface():
51-
"""
52-
Get the local network interface IP address.
53-
54-
Returns:
55-
str: Local IP address
56-
"""
50+
"""Get the local network interface IP address."""
5751
try:
5852
# Create a socket to determine the local IP
5953
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -89,10 +83,10 @@ def send_probe_and_get_responses(network_interface=None, timeout=WS_DISCOVERY_TI
8983
if network_interface is None:
9084
network_interface = get_network_interface()
9185

92-
print(f"Using network interface: {network_interface}")
86+
print(f"Network interface: {network_interface}")
9387
print(f"Probe UUID: {probe_uuid}")
9488
print(f"Sending Probe to: {WS_DISCOVERY_ADDRESS_IPv4}:{WS_DISCOVERY_PORT}")
95-
print("-" * 70)
89+
print("-" * 55)
9690

9791
responses = []
9892

@@ -135,7 +129,7 @@ def send_probe_and_get_responses(network_interface=None, timeout=WS_DISCOVERY_TI
135129
responses.append(
136130
{"xml": response, "address": addr[0], "port": addr[1]}
137131
)
138-
print(f"Received ProbeMatch from {addr[0]}:{addr[1]}")
132+
print(f"Received ProbeMatch {addr[0]}:{addr[1]}")
139133
# else: silently ignore non-XML responses
140134
# else: silently ignore empty/too short responses
141135

@@ -160,15 +154,7 @@ def send_probe_and_get_responses(network_interface=None, timeout=WS_DISCOVERY_TI
160154

161155

162156
def parse_probe_match(xml_response):
163-
"""
164-
Parse SOAP ProbeMatch XML response to extract device information.
165-
166-
Args:
167-
xml_response (str): SOAP XML response string
168-
169-
Returns:
170-
dict: Parsed device information or None if parsing fails
171-
"""
157+
"""Parse SOAP ProbeMatch XML response to extract device information."""
172158
try:
173159
# Clean up the XML response (remove null bytes and whitespace)
174160
xml_response = xml_response.strip()
@@ -255,20 +241,11 @@ def parse_probe_match(xml_response):
255241

256242

257243
def discover_onvif_devices(network_interface=None, timeout=WS_DISCOVERY_TIMEOUT):
258-
"""
259-
Discover ONVIF devices on the network using WS-Discovery.
260-
261-
Args:
262-
network_interface (str): Network interface IP to bind to (None for auto-detect)
263-
timeout (int): Discovery timeout in seconds
264-
265-
Returns:
266-
list: List of discovered ONVIF devices with parsed information
267-
"""
244+
"""Discover ONVIF devices on the network using WS-Discovery."""
268245
# Send probe and collect responses
269246
responses = send_probe_and_get_responses(network_interface, timeout)
270247

271-
print(f"\n{'='*70}")
248+
print(f"\n{'-'*55}")
272249
print(f"Total responses received: {len(responses)}")
273250

274251
discovered_devices = []
@@ -284,7 +261,7 @@ def discover_onvif_devices(network_interface=None, timeout=WS_DISCOVERY_TIMEOUT)
284261
discovered_devices.append(device_info)
285262

286263
print(f"Valid ONVIF devices found: {len(discovered_devices)}")
287-
print(f"{'='*70}\n")
264+
print(f"{'-'*55}\n")
288265

289266
# Print device information
290267
for device in discovered_devices:
@@ -294,31 +271,20 @@ def discover_onvif_devices(network_interface=None, timeout=WS_DISCOVERY_TIMEOUT)
294271

295272

296273
def print_device_info(device, is_onvif=True):
297-
"""
298-
Print formatted device information from ProbeMatch response.
299-
300-
Args:
301-
device (dict): Device information dictionary
302-
is_onvif (bool): Whether this is confirmed as an ONVIF device
303-
"""
304-
print(f"━━━ ONVIF Device #{device['index']} ━━━")
305-
print(f" Response from: {device.get('response_from', 'Unknown')}")
306-
print(f" EndpointReference (EPR):")
307-
print(f" {device['epr']}")
274+
"""Print formatted device information from ProbeMatch response."""
275+
print(f"[#{device['index']}] - {device.get('response_from', 'Unknown')}")
276+
print(f"EndpointReference (EPR):")
277+
print(f" • {device['epr']}")
308278

309279
if device["types"]:
310-
print(f" Types (from ProbeMatch):")
280+
print(f"Types (from ProbeMatch):")
311281
for type_info in device["types"]:
312-
print(f" • {type_info}")
313-
if "NetworkVideoTransmitter" in type_info:
314-
print(f" → Camera/Video Encoder device")
315-
elif "Device" in type_info:
316-
print(f" → ONVIF Device Management Service")
282+
print(f" • {type_info}")
317283

318284
if device["xaddrs"]:
319-
print(f" Service Addresses (XAddrs):")
285+
print(f"Service Addresses (XAddrs):")
320286
for xaddr in device["xaddrs"]:
321-
print(f" {xaddr}")
287+
print(f" • {xaddr}")
322288
# Extract and display IP address and port
323289
if "://" in xaddr:
324290
try:
@@ -329,179 +295,36 @@ def print_device_info(device, is_onvif=True):
329295
if ":" in rest.split("/")[0]:
330296
port_part = rest.split(":")[1].split("/")[0]
331297
print(
332-
f" → IP: {ip_part}, Port: {port_part}, Protocol: {protocol}"
298+
f" → IP: {ip_part}, Port: {port_part}, Protocol: {protocol}"
333299
)
334300
else:
335301
default_port = "80" if protocol == "http" else "443"
336302
print(
337-
f" → IP: {ip_part}, Port: {default_port}, Protocol: {protocol}"
303+
f" → IP: {ip_part}, Port: {default_port}, Protocol: {protocol}"
338304
)
339305
except:
340306
pass
341307

342308
if device["scopes"]:
343-
print(f" Scopes (ONVIF Metadata):")
309+
print(f"Scopes (ONVIF Metadata):")
344310
for scope in device["scopes"]:
345-
print(f" • {scope}")
346-
# Parse common ONVIF scope patterns
347-
if "onvif://www.onvif.org/MAC/" in scope:
348-
mac = scope.split("MAC/")[1] if "MAC/" in scope else ""
349-
print(f" → MAC Address: {mac}")
350-
elif "onvif://www.onvif.org/hardware/" in scope:
351-
hw = scope.split("hardware/")[1] if "hardware/" in scope else ""
352-
print(f" → Hardware Model: {hw}")
353-
elif "onvif://www.onvif.org/name/" in scope:
354-
name = scope.split("name/")[1] if "name/" in scope else ""
355-
print(f" → Device Name: {name}")
356-
elif "onvif://www.onvif.org/location/" in scope:
357-
loc = scope.split("location/")[1] if "location/" in scope else ""
358-
print(f" → Location: {loc}")
359-
elif "onvif://www.onvif.org/type/" in scope:
360-
dtype = scope.split("type/")[1] if "type/" in scope else ""
361-
print(f" → Device Type: {dtype}")
362-
elif "onvif://www.onvif.org/Profile/" in scope:
363-
profile = scope.split("Profile/")[1] if "Profile/" in scope else ""
364-
print(f" → ONVIF Profile: {profile}")
311+
# Remove the prefix "onvif://www.onvif.org/" if present
312+
if scope.startswith("onvif://www.onvif.org/"):
313+
simplified = scope.replace("onvif://www.onvif.org/", "")
314+
print(f" • [{simplified}]")
315+
else:
316+
# Keep other scopes as-is
317+
print(f" • [{scope}]")
365318

366319
if device.get("metadata_version"):
367-
print(f" Metadata Version: {device['metadata_version']}")
368-
369-
print("-" * 70)
370-
371-
372-
def extract_device_credentials(device):
373-
"""
374-
Extract useful connection information from discovered device.
375-
376-
Args:
377-
device (dict): Device information dictionary
378-
379-
Returns:
380-
dict: Connection parameters for ONVIFClient
381-
"""
382-
connection_info = {"host": None, "port": 80, "xaddrs": device["xaddrs"]}
383-
384-
# Try to extract host and port from XAddrs
385-
if device["xaddrs"]:
386-
first_xaddr = device["xaddrs"][0]
387-
try:
388-
# Parse URL like http://192.168.1.100:80/onvif/device_service
389-
if "://" in first_xaddr:
390-
parts = first_xaddr.split("://")[1]
391-
host_port = parts.split("/")[0]
392-
393-
if ":" in host_port:
394-
connection_info["host"] = host_port.split(":")[0]
395-
connection_info["port"] = int(host_port.split(":")[1])
396-
else:
397-
connection_info["host"] = host_port
398-
connection_info["port"] = 80
399-
except Exception as e:
400-
print(f"Warning: Could not parse XAddr: {e}")
401-
402-
return connection_info
403-
404-
405-
def main():
406-
"""Main function to run ONVIF device discovery"""
407-
print("\n" + "═" * 70)
408-
print(" ONVIF Device Discovery using WS-Discovery Protocol")
409-
print(" (Raw SOAP/UDP Implementation - No External Dependencies)")
410-
print("═" * 70)
411-
print("\nThis script sends a WS-Discovery Probe message via UDP multicast")
412-
print("to discover ONVIF-compliant devices on the local network.")
413-
print("\nProbe Message Details:")
414-
print(" • Multicast Address: 239.255.255.250")
415-
print(" • Port: 3702 (UDP)")
416-
print(" • Action: http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe")
417-
print(" • Types: tds:Device (ONVIF Device constraint)")
418-
print(" • Each probe has unique urn:uuid")
419-
print()
420-
421-
# Discover devices (auto-detect network interface)
422-
devices = discover_onvif_devices(
423-
network_interface=None, timeout=WS_DISCOVERY_TIMEOUT
424-
)
425-
426-
# Print summary
427-
print("\n" + "═" * 70)
428-
print(f" Discovery Complete - Found {len(devices)} ONVIF Device(s)")
429-
print("═" * 70)
430-
431-
if devices:
432-
print("\n📋 Connection Information Summary:")
433-
print("-" * 70)
434-
for device in devices:
435-
conn_info = extract_device_credentials(device)
436-
if conn_info["host"]:
437-
print(f"\n🎥 Device #{device['index']}:")
438-
print(f" Host: {conn_info['host']}")
439-
print(f" Port: {conn_info['port']}")
440-
441-
# Extract device info from scopes
442-
device_name = "Unknown"
443-
mac_address = "Unknown"
444-
hardware = "Unknown"
445-
446-
for scope in device.get("scopes", []):
447-
if "onvif://www.onvif.org/name/" in scope:
448-
device_name = (
449-
scope.split("name/")[1] if "name/" in scope else "Unknown"
450-
)
451-
elif "onvif://www.onvif.org/MAC/" in scope:
452-
mac_address = (
453-
scope.split("MAC/")[1] if "MAC/" in scope else "Unknown"
454-
)
455-
elif "onvif://www.onvif.org/hardware/" in scope:
456-
hardware = (
457-
scope.split("hardware/")[1]
458-
if "hardware/" in scope
459-
else "Unknown"
460-
)
461-
462-
if device_name != "Unknown":
463-
print(f" Name: {device_name}")
464-
if hardware != "Unknown":
465-
print(f" Model: {hardware}")
466-
if mac_address != "Unknown":
467-
print(f" MAC: {mac_address}")
468-
469-
print(
470-
f" XAddr: {conn_info['xaddrs'][0] if conn_info['xaddrs'] else 'N/A'}"
471-
)
472-
print(f"\n 💻 Python Code to Connect:")
473-
print(f" from onvif import ONVIFClient")
474-
print(
475-
f' client = ONVIFClient("{conn_info["host"]}", {conn_info["port"]}, "admin", "password")'
476-
)
477-
print(f" device = client.devicemgmt()")
478-
print(f" print(device.GetDeviceInformation())")
479-
print("-" * 70)
480-
print("\n💡 Tips:")
481-
print(" • Replace 'admin' and 'password' with your actual credentials")
482-
print(" • Most ONVIF cameras use default credentials like admin/admin")
483-
print(" • Check your camera's manual for default username/password")
484-
else:
485-
print("\n❌ No ONVIF devices were discovered.")
486-
print("\n🔍 Possible reasons:")
487-
print(" • No ONVIF devices are powered on or connected to the network")
488-
print(" • Devices have WS-Discovery disabled in their settings")
489-
print(" • Firewall is blocking multicast UDP traffic (port 3702)")
490-
print(" • Network segmentation prevents multicast discovery")
491-
print(" • Devices are on a different subnet/VLAN")
492-
print("\n💡 Troubleshooting:")
493-
print(" • Check if devices respond to ping")
494-
print(" • Verify devices are on the same network segment")
495-
print(" • Try disabling firewall temporarily")
496-
print(" • Check camera settings for WS-Discovery/ONVIF support")
497-
print(" • Run script with administrator/root privileges")
320+
print(f"Metadata Version: {device['metadata_version']}")
498321

499322
print()
500323

501324

502325
if __name__ == "__main__":
503326
try:
504-
main()
327+
discover_onvif_devices(network_interface=None, timeout=WS_DISCOVERY_TIMEOUT)
505328
except KeyboardInterrupt:
506329
print("\n\nDiscovery interrupted by user.")
507330
sys.exit(0)

0 commit comments

Comments
 (0)