2222import xml .etree .ElementTree as ET
2323import sys
2424import struct
25- import time
2625
2726
2827# WS-Discovery constants
4847
4948
5049def 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
162156def 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
257243def 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
296273def 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 ("\n This script sends a WS-Discovery Probe message via UDP multicast" )
412- print ("to discover ONVIF-compliant devices on the local network." )
413- print ("\n Probe 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
502325if __name__ == "__main__" :
503326 try :
504- main ( )
327+ discover_onvif_devices ( network_interface = None , timeout = WS_DISCOVERY_TIMEOUT )
505328 except KeyboardInterrupt :
506329 print ("\n \n Discovery interrupted by user." )
507330 sys .exit (0 )
0 commit comments