onvif-python 0.1.8__py3-none-any.whl → 0.2.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.
onvif/cli/interactive.py CHANGED
@@ -173,6 +173,9 @@ class InteractiveShell(cmd.Cmd):
173
173
  if hasattr(args, "no_patch") and args.no_patch:
174
174
  options_info.append(f" ZeepPatcher : {colorize('Disabled', 'red')}")
175
175
 
176
+ if hasattr(args, "wsdl") and args.wsdl:
177
+ options_info.append(f" Custom WSDL : {colorize(args.wsdl, 'yellow')}")
178
+
176
179
  if (
177
180
  hasattr(args, "health_check_interval") and args.health_check_interval != 10
178
181
  ): # 10 is default
@@ -191,7 +194,7 @@ class InteractiveShell(cmd.Cmd):
191
194
  " / __ \\/ | / / | / / _/ ____/",
192
195
  " / / / / |/ /| | / // // /_ ",
193
196
  "/ /_/ / /| / | |/ // // __/ ",
194
- "\\____/_/ |_/ |___/___/_/ v0.1.8",
197
+ "\\____/_/ |_/ |___/___/_/ v0.2.0",
195
198
  " ",
196
199
  ]
197
200
 
@@ -602,6 +605,8 @@ class InteractiveShell(cmd.Cmd):
602
605
  if self.current_service:
603
606
  # In service mode - suggest methods
604
607
  methods = get_service_methods(self.current_service)
608
+ # Add helper commands in service mode
609
+ methods.extend([colorize("type", "yellow"), colorize("desc", "yellow")])
605
610
  for method in methods:
606
611
  if method.lower().startswith(partial_cmd.lower()):
607
612
  suggestions.append(method)
@@ -627,6 +632,8 @@ class InteractiveShell(cmd.Cmd):
627
632
  if self.current_service:
628
633
  # Complete method names in service mode
629
634
  methods = get_service_methods(self.current_service)
635
+ # Add helper commands in service mode
636
+ methods.extend([colorize("type", "yellow"), colorize("desc", "yellow")])
630
637
  completions = [
631
638
  method for method in methods if method.lower().startswith(text.lower())
632
639
  ]
@@ -839,6 +846,8 @@ class InteractiveShell(cmd.Cmd):
839
846
  if self.current_service:
840
847
  # In service mode - show available methods
841
848
  methods = get_service_methods(self.current_service)
849
+ # Add helper commands in service mode
850
+ methods.extend([colorize("type", "yellow"), colorize("desc", "yellow")])
842
851
  if methods:
843
852
  # Use the same display format as TAB completion
844
853
  self._display_grid(methods)
@@ -875,6 +884,7 @@ class InteractiveShell(cmd.Cmd):
875
884
  print(
876
885
  f"{colorize('Error:', 'red')} Method '{method_name}' not found in service '{self.current_service_name}'."
877
886
  )
887
+ print(f"Use {colorize('ls', 'yellow')} to see available methods.")
878
888
  return
879
889
 
880
890
  doc_info = get_method_documentation(self.current_service, method_name)
@@ -913,8 +923,8 @@ class InteractiveShell(cmd.Cmd):
913
923
  return [m for m in methods if m.lower().startswith(text.lower())]
914
924
 
915
925
  def do_type(self, line):
916
- """Show input and output types for an operation. Usage: type <operation_name>"""
917
- operation_name = line.strip()
926
+ """Show input and output types for a method. Usage: type <method_name>"""
927
+ method_name = line.strip()
918
928
 
919
929
  if not self.current_service:
920
930
  print(
@@ -923,18 +933,18 @@ class InteractiveShell(cmd.Cmd):
923
933
  print(f"Enter a service first (e.g., {colorize('devicemgmt', 'yellow')})")
924
934
  return
925
935
 
926
- if not operation_name:
927
- print("Usage: type <operation_name>")
936
+ if not method_name:
937
+ print("Usage: type <method_name>")
928
938
  return
929
939
 
930
- if not hasattr(self.current_service, operation_name):
940
+ if not hasattr(self.current_service, method_name):
931
941
  print(
932
- f"{colorize('Error:', 'red')} Operation '{operation_name}' not found in service '{self.current_service_name}'."
942
+ f"{colorize('Error:', 'red')} Method '{method_name}' not found in service '{self.current_service_name}'."
933
943
  )
934
- print(f"Use {colorize('ls', 'yellow')} to see available operations.")
944
+ print(f"Use {colorize('ls', 'yellow')} to see available methods.")
935
945
  return
936
946
 
937
- type_info = get_operation_type_info(self.current_service, operation_name)
947
+ type_info = get_operation_type_info(self.current_service, method_name)
938
948
 
939
949
  if type_info:
940
950
  # Helper function to display parameters recursively with tree-style indentation
@@ -1065,7 +1075,7 @@ class InteractiveShell(cmd.Cmd):
1065
1075
  print() # Add newline for spacing
1066
1076
  else:
1067
1077
  print(
1068
- f"{colorize('Error:', 'red')} Could not retrieve type information for '{operation_name}'."
1078
+ f"{colorize('Error:', 'red')} Could not retrieve type information for '{method_name}'."
1069
1079
  )
1070
1080
 
1071
1081
  def complete_type(self, text, line, begidx, endidx):
@@ -1268,6 +1278,11 @@ class InteractiveShell(cmd.Cmd):
1268
1278
  if hasattr(self.args, "no_patch") and self.args.no_patch:
1269
1279
  options_info.append(f" ZeepPatcher : {colorize('Disabled', 'red')}")
1270
1280
 
1281
+ if hasattr(self.args, "wsdl") and self.args.wsdl:
1282
+ options_info.append(
1283
+ f" Custom WSDL : {colorize(self.args.wsdl, 'yellow')}"
1284
+ )
1285
+
1271
1286
  if (
1272
1287
  hasattr(self.args, "health_check_interval")
1273
1288
  and self.args.health_check_interval != 10
@@ -1385,7 +1400,7 @@ class InteractiveShell(cmd.Cmd):
1385
1400
  super().do_help(line)
1386
1401
  else:
1387
1402
  help_text = f"""
1388
- {colorize('ONVIF Interactive Shell — v0.1.8', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
1403
+ {colorize('ONVIF Interactive Shell — v0.2.0', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
1389
1404
 
1390
1405
  {colorize('Basic Commands:', 'yellow')}
1391
1406
  capabilities, caps - Show device capabilities
onvif/cli/main.py CHANGED
@@ -4,6 +4,10 @@ import argparse
4
4
  import sys
5
5
  import warnings
6
6
  import getpass
7
+ import sqlite3
8
+ import os
9
+ import shutil
10
+ from datetime import datetime
7
11
  from typing import Any, Optional, Tuple
8
12
 
9
13
  from ..client import ONVIFClient
@@ -17,19 +21,24 @@ def create_parser():
17
21
  """Create argument parser for ONVIF CLI"""
18
22
  parser = argparse.ArgumentParser(
19
23
  prog="onvif",
20
- description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.1.8\nhttps://github.com/nirsimetri/onvif-python",
24
+ description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.0\nhttps://github.com/nirsimetri/onvif-python",
21
25
  formatter_class=argparse.RawDescriptionHelpFormatter,
22
26
  epilog=f"""
23
27
  Examples:
28
+ # Product search
29
+ {colorize('onvif', 'yellow')} --search c210
30
+ {colorize('onvif', 'yellow')} -s "axis camera"
31
+ {colorize('onvif', 'yellow')} --search hikvision --page 2 --per-page 5
32
+
24
33
  # Discover ONVIF devices on network
25
34
  {colorize('onvif', 'yellow')} --discover --username admin --password admin123 --interactive
26
35
  {colorize('onvif', 'yellow')} media GetProfiles --discover --username admin
27
36
  {colorize('onvif', 'yellow')} -d -i
28
37
 
29
38
  # Discover with filtering
30
- {colorize('onvif', 'yellow')} --discover --search ptz --interactive
31
- {colorize('onvif', 'yellow')} -d -s "C210" -i
32
- {colorize('onvif', 'yellow')} -d -s "audio_encoder" -u admin -p admin123 -i
39
+ {colorize('onvif', 'yellow')} --discover --filter ptz --interactive
40
+ {colorize('onvif', 'yellow')} -d -f "C210" -i
41
+ {colorize('onvif', 'yellow')} -d -f "audio_encoder" -u admin -p admin123 -i
33
42
 
34
43
  # Direct command execution
35
44
  {colorize('onvif', 'yellow')} devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
@@ -66,10 +75,29 @@ Examples:
66
75
  action="store_true",
67
76
  help="Discover ONVIF devices on the network using WS-Discovery",
68
77
  )
78
+ parser.add_argument(
79
+ "--filter",
80
+ "-f",
81
+ help="Filter discovered devices by types or scopes (case-insensitive substring match)",
82
+ )
83
+
84
+ # Product search
69
85
  parser.add_argument(
70
86
  "--search",
71
87
  "-s",
72
- help="Filter discovered devices by types or scopes (case-insensitive substring match)",
88
+ help="Search ONVIF products database by model or company (e.g., 'c210', 'hikvision')",
89
+ )
90
+ parser.add_argument(
91
+ "--page",
92
+ type=int,
93
+ default=1,
94
+ help="Page number for search results (default: 1)",
95
+ )
96
+ parser.add_argument(
97
+ "--per-page",
98
+ type=int,
99
+ default=20,
100
+ help="Number of results per page (default: 20)",
73
101
  )
74
102
 
75
103
  # Connection options
@@ -133,6 +161,277 @@ Examples:
133
161
  return parser
134
162
 
135
163
 
164
+ def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None:
165
+ """Search ONVIF products database and display results in table format with pagination.
166
+
167
+ Args:
168
+ search_term: Search term to match against model, post_title, and company_name fields
169
+ page: Page number (1-based)
170
+ per_page: Number of results per page
171
+ """
172
+ # Get the database path relative to the script location
173
+ current_dir = os.path.dirname(os.path.abspath(__file__))
174
+ db_path = os.path.join(current_dir, "..", "db", "products.db")
175
+ db_path = os.path.normpath(db_path)
176
+
177
+ if not os.path.exists(db_path):
178
+ print(f"{colorize('Error:', 'red')} Products database not found at {db_path}")
179
+ sys.exit(1)
180
+
181
+ # Validate pagination parameters
182
+ if page < 1:
183
+ print(f"{colorize('Error:', 'red')} Page number must be 1 or greater")
184
+ sys.exit(1)
185
+
186
+ if per_page < 1 or per_page > 100:
187
+ print(f"{colorize('Error:', 'red')} Per-page must be between 1 and 100")
188
+ sys.exit(1)
189
+
190
+ try:
191
+ conn = sqlite3.connect(db_path)
192
+ cursor = conn.cursor()
193
+
194
+ # First, get total count for pagination info
195
+ count_query = """
196
+ SELECT COUNT(*)
197
+ FROM onvif_products
198
+ WHERE LOWER(model) LIKE LOWER(?)
199
+ OR LOWER(post_title) LIKE LOWER(?)
200
+ OR LOWER(company_name) LIKE LOWER(?)
201
+ """
202
+
203
+ search_pattern = f"%{search_term}%"
204
+ cursor.execute(count_query, (search_pattern, search_pattern, search_pattern))
205
+ total_count = cursor.fetchone()[0]
206
+
207
+ if total_count == 0:
208
+ print(
209
+ f"{colorize('No products found matching:', 'yellow')} {colorize(search_term, 'white')}"
210
+ )
211
+ return
212
+
213
+ # Calculate pagination
214
+ total_pages = (total_count + per_page - 1) // per_page # Ceiling division
215
+ if page > total_pages:
216
+ print(
217
+ f"{colorize('Error:', 'red')} Page {page} does not exist. Total pages: {total_pages}"
218
+ )
219
+ return
220
+
221
+ offset = (page - 1) * per_page
222
+
223
+ # Search query with pagination
224
+ query = """
225
+ SELECT ID, test_date, post_title, product_firmware_version,
226
+ product_profiles, product_category, type,
227
+ company_name
228
+ FROM onvif_products
229
+ WHERE LOWER(model) LIKE LOWER(?)
230
+ OR LOWER(post_title) LIKE LOWER(?)
231
+ OR LOWER(company_name) LIKE LOWER(?)
232
+ ORDER BY test_date DESC
233
+ LIMIT ? OFFSET ?
234
+ """
235
+
236
+ cursor.execute(
237
+ query, (search_pattern, search_pattern, search_pattern, per_page, offset)
238
+ )
239
+ results = cursor.fetchall()
240
+
241
+ # Display results in table format
242
+ start_result = offset + 1
243
+ end_result = min(offset + per_page, total_count)
244
+ print(
245
+ f"\n{colorize(f'Found {total_count} product(s) matching:', 'green')} {colorize(search_term, 'white')}"
246
+ )
247
+ print(
248
+ f"{colorize(f'Showing {start_result}-{end_result} of {total_count} results', 'cyan')}"
249
+ )
250
+ print()
251
+
252
+ # Table headers
253
+ headers = [
254
+ "ID",
255
+ "Test Date",
256
+ "Model",
257
+ "Firmware",
258
+ "Profiles",
259
+ "Category",
260
+ "Type",
261
+ "Company",
262
+ ]
263
+
264
+ # Get terminal width for adaptive formatting
265
+ try:
266
+ terminal_width = shutil.get_terminal_size().columns
267
+ except Exception:
268
+ terminal_width = 120 # fallback width
269
+
270
+ # Calculate minimum column widths
271
+ min_col_widths = [max(len(str(header)), 8) for header in headers]
272
+
273
+ # Calculate actual content widths (without truncation first)
274
+ actual_widths = [0] * len(headers)
275
+ for row in results:
276
+ for i, value in enumerate(row):
277
+ if value:
278
+ if i == 1: # Date column - calculate formatted date width
279
+ str_value = str(value)
280
+ if "T" in str_value:
281
+ date_part = str_value.split("T")[0]
282
+ time_part = str_value.split("T")[1].split(".")[0]
283
+ if "+" in time_part:
284
+ time_part = time_part.split("+")[0]
285
+ elif "Z" in time_part:
286
+ time_part = time_part.replace("Z", "")
287
+ formatted_date = f"{date_part} {time_part}"
288
+ actual_widths[i] = max(
289
+ actual_widths[i], len(formatted_date)
290
+ )
291
+ else:
292
+ actual_widths[i] = max(actual_widths[i], len(str_value))
293
+ else:
294
+ actual_widths[i] = max(actual_widths[i], len(str(value)))
295
+
296
+ # Combine minimum widths with actual content widths
297
+ col_widths = [
298
+ max(min_col_widths[i], actual_widths[i]) for i in range(len(headers))
299
+ ]
300
+
301
+ # Calculate space needed for separators (3 chars per separator: " | ")
302
+ separator_space = (len(headers) - 1) * 3
303
+ total_content_width = sum(col_widths)
304
+ total_needed_width = total_content_width + separator_space
305
+
306
+ # If table is too wide for terminal, apply smart truncation
307
+ if total_needed_width > terminal_width:
308
+ available_width = terminal_width - separator_space
309
+
310
+ # Priority columns that should not be truncated (ID, Date)
311
+ protected_cols = {0, 1} # ID and Date columns
312
+ protected_width = sum(col_widths[i] for i in protected_cols)
313
+
314
+ # Width available for other columns
315
+ remaining_width = available_width - protected_width
316
+
317
+ # Columns that can be truncated
318
+ truncatable_cols = [
319
+ i for i in range(len(headers)) if i not in protected_cols
320
+ ]
321
+
322
+ if remaining_width > 0 and truncatable_cols:
323
+ # Calculate proportional allocation for truncatable columns
324
+ current_truncatable_width = sum(col_widths[i] for i in truncatable_cols)
325
+
326
+ for i in truncatable_cols:
327
+ if current_truncatable_width > 0:
328
+ # Proportional allocation
329
+ proportion = col_widths[i] / current_truncatable_width
330
+ new_width = int(remaining_width * proportion)
331
+
332
+ # Ensure minimum width
333
+ col_widths[i] = max(new_width, min_col_widths[i])
334
+ else:
335
+ col_widths[i] = min_col_widths[i]
336
+
337
+ # Print header
338
+ header_line = " | ".join(
339
+ header.ljust(col_widths[i]) for i, header in enumerate(headers)
340
+ )
341
+ print(colorize(header_line, "yellow"))
342
+ print(colorize("-" * len(header_line), "white"))
343
+
344
+ # Print data rows
345
+ for row in results:
346
+ formatted_row = []
347
+ for i, value in enumerate(row):
348
+ if value is None:
349
+ formatted_value = ""
350
+ else:
351
+ str_value = str(value)
352
+
353
+ # Special formatting for date column (index 1)
354
+ if i == 1 and value: # Date column
355
+ try:
356
+ # Handle ISO format with timezone
357
+ if "T" in str_value:
358
+ # Parse ISO format: 2024-08-15T17:53:12.9154121+08:00
359
+ # Extract just the date and time part before timezone
360
+ date_part = str_value.split("T")[0]
361
+ time_part = str_value.split("T")[1].split(".")[
362
+ 0
363
+ ] # Remove microseconds
364
+ if "+" in time_part:
365
+ time_part = time_part.split("+")[0]
366
+ elif "Z" in time_part:
367
+ time_part = time_part.replace("Z", "")
368
+ formatted_value = f"{date_part} {time_part}"
369
+ elif (
370
+ len(str_value) == 19 and " " in str_value
371
+ ): # Already in correct format
372
+ formatted_value = str_value
373
+ elif len(str_value) == 10: # Just date, add time
374
+ formatted_value = f"{str_value} 00:00:00"
375
+ else:
376
+ # Try to parse common formats
377
+ try:
378
+ parsed_date = datetime.strptime(
379
+ str_value, "%Y-%m-%d %H:%M:%S"
380
+ )
381
+ formatted_value = parsed_date.strftime(
382
+ "%Y-%m-%d %H:%M:%S"
383
+ )
384
+ except ValueError:
385
+ try:
386
+ parsed_date = datetime.strptime(
387
+ str_value, "%Y-%m-%d"
388
+ )
389
+ formatted_value = parsed_date.strftime(
390
+ "%Y-%m-%d 00:00:00"
391
+ )
392
+ except ValueError:
393
+ formatted_value = (
394
+ str_value # Keep original if parsing fails
395
+ )
396
+ except Exception:
397
+ formatted_value = str_value # Keep original if any error
398
+ else:
399
+ # Apply truncation based on calculated column width
400
+ max_width = col_widths[i]
401
+ if len(str_value) > max_width:
402
+ formatted_value = str_value[: max_width - 3] + "..."
403
+ else:
404
+ formatted_value = str_value
405
+
406
+ formatted_row.append(formatted_value.ljust(col_widths[i]))
407
+
408
+ print(" | ".join(formatted_row))
409
+
410
+ # Display pagination information
411
+ print()
412
+ newline = "\n" if total_pages == 1 else ""
413
+ print(f"{colorize(f'Page {page} of {total_pages}', 'cyan')} {newline}")
414
+
415
+ # Show navigation hints
416
+ nav_hints = []
417
+ if page > 1:
418
+ nav_hints.append(f"Previous: --page {page - 1}")
419
+ if page < total_pages:
420
+ nav_hints.append(f"Next: --page {page + 1}")
421
+
422
+ if nav_hints:
423
+ print(f"{colorize('Navigation:', 'white')} {' | '.join(nav_hints)}\n")
424
+
425
+ conn.close()
426
+
427
+ except sqlite3.Error as e:
428
+ print(f"{colorize('Database error:', 'red')} {e}")
429
+ sys.exit(1)
430
+ except Exception as e:
431
+ print(f"{colorize('Error:', 'red')} {e}")
432
+ sys.exit(1)
433
+
434
+
136
435
  def main():
137
436
  """Main CLI entry point"""
138
437
  # Setup custom warning format for cleaner output
@@ -149,11 +448,21 @@ def main():
149
448
 
150
449
  # Show ONVIF CLI version
151
450
  if args.version:
152
- print(colorize("0.1.8", "yellow"))
451
+ print(colorize("0.2.0", "yellow"))
452
+ sys.exit(0)
453
+
454
+ # Handle product search
455
+ if args.search:
456
+ search_products(args.search, args.page, args.per_page)
153
457
  sys.exit(0)
154
458
 
155
459
  # Validate arguments early (before discovery)
156
- if not args.interactive and (not args.service or not args.method):
460
+ # Skip validation if search mode is active
461
+ if (
462
+ not args.search
463
+ and not args.interactive
464
+ and (not args.service or not args.method)
465
+ ):
157
466
  parser.error(
158
467
  f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} mode or {colorize('service/method', 'white')} must be specified"
159
468
  )
@@ -165,15 +474,15 @@ def main():
165
474
  f"{colorize('--discover', 'white')} cannot be used with {colorize('--host', 'white')}"
166
475
  )
167
476
 
168
- # Discover devices (pass --https flag to prioritize HTTPS XAddrs and search term)
477
+ # Discover devices (pass --https flag to prioritize HTTPS XAddrs and filter term)
169
478
  devices = discover_devices(
170
- timeout=4, prefer_https=args.https, search=args.search
479
+ timeout=4, prefer_https=args.https, filter_term=args.filter
171
480
  )
172
481
 
173
482
  if not devices:
174
- if args.search:
483
+ if args.filter:
175
484
  print(
176
- f"{colorize('No devices found matching search:', 'red')} {colorize(args.search, 'white')}"
485
+ f"{colorize('No devices found matching filter:', 'red')} {colorize(args.filter, 'white')}"
177
486
  )
178
487
  else:
179
488
  print(colorize("No ONVIF devices discovered. Exiting.", "red"))
@@ -192,22 +501,22 @@ def main():
192
501
  # Use device's detected protocol (already filtered by prefer_https in discover_devices)
193
502
  # No need to override - device info already has correct protocol based on --https flag
194
503
 
195
- # Validate that host is provided (either via --host or --discover)
196
- if not args.host:
504
+ # Validate that host is provided (either via --host or --discover) unless using --search
505
+ if not args.search and not args.host:
197
506
  parser.error(
198
507
  f"Either {colorize('--host', 'white')} or {colorize('--discover', 'white')} must be specified"
199
508
  )
200
509
 
201
- # Handle username prompt
202
- if not args.username:
510
+ # Handle username prompt (skip for search mode)
511
+ if not args.search and not args.username:
203
512
  try:
204
513
  args.username = input("Enter username: ")
205
514
  except (EOFError, KeyboardInterrupt):
206
515
  print("\nUsername entry cancelled.")
207
516
  sys.exit(1)
208
517
 
209
- # Handle password securely if not provided
210
- if not args.password:
518
+ # Handle password securely if not provided (skip for search mode)
519
+ if not args.search and not args.password:
211
520
  try:
212
521
  args.password = getpass.getpass(
213
522
  f"Enter password for {colorize(f'{args.username}@{args.host}', 'yellow')}: "
@@ -216,6 +525,10 @@ def main():
216
525
  print("\nPassword entry cancelled.")
217
526
  sys.exit(1)
218
527
 
528
+ # Skip ONVIF client creation for search mode
529
+ if args.search:
530
+ return
531
+
219
532
  try:
220
533
  # Create ONVIF client
221
534
  client = ONVIFClient(
@@ -296,14 +609,14 @@ def execute_command(
296
609
 
297
610
 
298
611
  def discover_devices(
299
- timeout: int = 4, prefer_https: bool = False, search: Optional[str] = None
612
+ timeout: int = 4, prefer_https: bool = False, filter_term: Optional[str] = None
300
613
  ) -> list:
301
614
  """Discover ONVIF devices on the network using WS-Discovery.
302
615
 
303
616
  Args:
304
617
  timeout: Discovery timeout in seconds
305
618
  prefer_https: If True, prioritize HTTPS XAddrs when available
306
- search: Optional search term to filter devices by types or scopes
619
+ filter_term: Optional search term to filter devices by types or scopes
307
620
 
308
621
  Returns:
309
622
  List of discovered devices with connection info
@@ -315,11 +628,11 @@ def discover_devices(
315
628
  print(f"\n{colorize('Discovering ONVIF devices on network...', 'yellow')}")
316
629
  print(f"Network interface: {colorize(discovery._get_local_ip(), 'white')}")
317
630
  print(f"Timeout: {timeout}s")
318
- if search:
319
- print(f"Filter: {colorize(search, 'yellow')}")
631
+ if filter_term:
632
+ print(f"Filter: {colorize(filter_term, 'yellow')}")
320
633
  print()
321
634
 
322
- devices = discovery.discover(prefer_https=prefer_https, search=search)
635
+ devices = discovery.discover(prefer_https=prefer_https, search=filter_term)
323
636
 
324
637
  return devices
325
638
 
@@ -346,15 +659,22 @@ def select_device_interactive(devices: list) -> Optional[Tuple[str, int, bool]]:
346
659
  else colorize("HTTP", "white")
347
660
  )
348
661
  print(f"\n{idx_str} {colorize(host_port, 'yellow')} ({protocol_indicator})")
349
- print(f" [id] {device['epr']}")
662
+
663
+ # Remove uuid: or urn:uuid: prefix from EPR
664
+ epr_display = device["epr"]
665
+ if epr_display.startswith("urn:uuid:"):
666
+ epr_display = epr_display.replace("urn:uuid:", "")
667
+ elif epr_display.startswith("uuid:"):
668
+ epr_display = epr_display.replace("uuid:", "")
669
+ print(f" [id] {epr_display}")
350
670
 
351
671
  if device["xaddrs"]:
352
- xaddrs_str = " ".join(device["xaddrs"])
353
- print(f" [xaddrs] {xaddrs_str}")
672
+ xaddrs_parts = [f"[{xaddr}]" for xaddr in device["xaddrs"]]
673
+ print(f" [xaddrs] {' '.join(xaddrs_parts)}")
354
674
 
355
675
  if device["types"]:
356
- types_str = " ".join(device["types"])
357
- print(f" [types] {types_str}")
676
+ types_parts = [f"[{t}]" for t in device["types"]]
677
+ print(f" [types] {' '.join(types_parts)}")
358
678
 
359
679
  if device["scopes"]:
360
680
  scope_parts = []
onvif/cli/utils.py CHANGED
@@ -213,8 +213,8 @@ def get_service_methods(service_obj) -> list:
213
213
  methods = []
214
214
  for attr_name in dir(service_obj):
215
215
  if not attr_name.startswith("_") and callable(getattr(service_obj, attr_name)):
216
- # Skip operator attribute
217
- if attr_name != "operator":
216
+ # Skip helper methods
217
+ if attr_name not in ["type", "desc", "operations"]:
218
218
  methods.append(attr_name)
219
219
  return sorted(methods)
220
220
 
@@ -354,13 +354,6 @@ def get_method_documentation(service_obj, method_name: str) -> Optional[Dict[str
354
354
  return None
355
355
 
356
356
 
357
- def truncate_output(text: str, max_length: int = 1000) -> str:
358
- """Truncate output if too long"""
359
- if len(text) <= max_length:
360
- return text
361
- return text[:max_length] + f"\n... (truncated, {len(text)} total chars)"
362
-
363
-
364
357
  def colorize(text: str, color: str) -> str:
365
358
  """Add color to text for terminal output"""
366
359
  # Enable ANSI colors on Windows