unifi-network-maps 1.4.11__py3-none-any.whl → 1.4.12__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.
@@ -1 +1 @@
1
- __version__ = "1.4.11"
1
+ __version__ = "1.4.12"
@@ -62,7 +62,12 @@ def render_mermaid_output(
62
62
  direction=args.direction,
63
63
  groups=groups,
64
64
  group_order=group_order,
65
- node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
65
+ node_types=build_node_type_map(
66
+ devices,
67
+ clients,
68
+ client_mode=args.client_scope,
69
+ only_unifi=args.only_unifi,
70
+ ),
66
71
  theme=mermaid_theme,
67
72
  )
68
73
  if args.markdown:
@@ -97,13 +102,23 @@ def render_svg_output(
97
102
 
98
103
  return render_svg_isometric(
99
104
  edges,
100
- node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
105
+ node_types=build_node_type_map(
106
+ devices,
107
+ clients,
108
+ client_mode=args.client_scope,
109
+ only_unifi=args.only_unifi,
110
+ ),
101
111
  options=options,
102
112
  theme=svg_theme,
103
113
  )
104
114
  return render_svg(
105
115
  edges,
106
- node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
116
+ node_types=build_node_type_map(
117
+ devices,
118
+ clients,
119
+ client_mode=args.client_scope,
120
+ only_unifi=args.only_unifi,
121
+ ),
107
122
  options=options,
108
123
  theme=svg_theme,
109
124
  )
@@ -190,6 +205,7 @@ def render_lldp_format(
190
205
  include_ports=args.include_ports,
191
206
  show_clients=args.include_clients,
192
207
  client_mode=args.client_scope,
208
+ only_unifi=args.only_unifi,
193
209
  )
194
210
  write_output(content, output_path=args.output, stdout=args.stdout)
195
211
  return 0
@@ -95,6 +95,7 @@ def build_edges_with_clients(
95
95
  device_index,
96
96
  include_ports=args.include_ports,
97
97
  client_mode=args.client_scope,
98
+ only_unifi=args.only_unifi,
98
99
  )
99
100
  return edges, clients
100
101
 
@@ -153,5 +154,10 @@ def resolve_mkdocs_client_ports(
153
154
  clients = list(fetch_clients(config, site=site))
154
155
  else:
155
156
  clients = mock_clients
156
- client_ports = build_client_port_map(devices, clients, client_mode=args.client_scope)
157
+ client_ports = build_client_port_map(
158
+ devices,
159
+ clients,
160
+ client_mode=args.client_scope,
161
+ only_unifi=args.only_unifi,
162
+ )
157
163
  return client_ports, None
@@ -460,7 +460,13 @@ def _client_field(client: object, name: str) -> object | None:
460
460
 
461
461
 
462
462
  def _client_display_name(client: object) -> str | None:
463
- for key in ("name", "hostname", "mac"):
463
+ raw_name = _client_field(client, "name")
464
+ if isinstance(raw_name, str) and raw_name.strip():
465
+ return raw_name.strip()
466
+ preferred = _client_ucore_display_name(client)
467
+ if preferred:
468
+ return preferred
469
+ for key in ("hostname", "mac"):
464
470
  value = _client_field(client, key)
465
471
  if isinstance(value, str) and value.strip():
466
472
  return value.strip()
@@ -514,6 +520,73 @@ def _client_is_wired(client: object) -> bool:
514
520
  return bool(_client_field(client, "is_wired"))
515
521
 
516
522
 
523
+ def _client_unifi_flag(client: object) -> bool | None:
524
+ for key in ("is_unifi", "is_unifi_device", "is_ubnt", "is_uap", "is_managed"):
525
+ value = _client_field(client, key)
526
+ if isinstance(value, bool):
527
+ return value
528
+ if isinstance(value, int):
529
+ return value != 0
530
+ return None
531
+
532
+
533
+ def _client_vendor(client: object) -> str | None:
534
+ for key in ("oui", "vendor", "vendor_name", "manufacturer", "manufacturer_name"):
535
+ value = _client_field(client, key)
536
+ if isinstance(value, str) and value.strip():
537
+ return value.strip()
538
+ return None
539
+
540
+
541
+ def _client_ucore_info(client: object) -> dict[str, object] | None:
542
+ info = _client_field(client, "unifi_device_info_from_ucore")
543
+ if isinstance(info, dict):
544
+ return info
545
+ return None
546
+
547
+
548
+ def _client_ucore_display_name(client: object) -> str | None:
549
+ ucore = _client_ucore_info(client)
550
+ if not ucore:
551
+ return None
552
+ for key in ("name", "computed_model", "product_model", "product_shortname"):
553
+ value = ucore.get(key)
554
+ if isinstance(value, str) and value.strip():
555
+ return value.strip()
556
+ return None
557
+
558
+
559
+ def _client_hostname_source(client: object) -> str | None:
560
+ value = _client_field(client, "hostname_source")
561
+ if isinstance(value, str) and value.strip():
562
+ return value.strip()
563
+ return None
564
+
565
+
566
+ def _client_is_unifi(client: object) -> bool:
567
+ flag = _client_unifi_flag(client)
568
+ if flag is not None:
569
+ return flag
570
+ ucore = _client_ucore_info(client)
571
+ if ucore:
572
+ managed = ucore.get("managed")
573
+ if isinstance(managed, bool) and managed:
574
+ return True
575
+ if isinstance(ucore.get("product_line"), str) and ucore.get("product_line"):
576
+ return True
577
+ if isinstance(ucore.get("product_shortname"), str) and ucore.get("product_shortname"):
578
+ return True
579
+ for key in ("name", "computed_model", "product_model"):
580
+ value = ucore.get(key)
581
+ if isinstance(value, str) and value.strip():
582
+ return True
583
+ vendor = _client_vendor(client)
584
+ if not vendor:
585
+ return False
586
+ normalized = vendor.lower()
587
+ return "ubiquiti" in normalized or "unifi" in normalized
588
+
589
+
517
590
  def _client_channel(client: object) -> int | None:
518
591
  for key in ("channel", "radio_channel", "wifi_channel"):
519
592
  value = _client_field(client, key)
@@ -533,17 +606,26 @@ def _client_matches_mode(client: object, mode: str) -> bool:
533
606
  return wired
534
607
 
535
608
 
609
+ def _client_matches_filters(client: object, *, client_mode: str, only_unifi: bool) -> bool:
610
+ if not _client_matches_mode(client, client_mode):
611
+ return False
612
+ if only_unifi and not _client_is_unifi(client):
613
+ return False
614
+ return True
615
+
616
+
536
617
  def build_client_edges(
537
618
  clients: Iterable[object],
538
619
  device_index: dict[str, str],
539
620
  *,
540
621
  include_ports: bool = False,
541
622
  client_mode: str = "wired",
623
+ only_unifi: bool = False,
542
624
  ) -> list[Edge]:
543
625
  edges: list[Edge] = []
544
626
  seen: set[tuple[str, str]] = set()
545
627
  for client in clients:
546
- if not _client_matches_mode(client, client_mode):
628
+ if not _client_matches_filters(client, client_mode=client_mode, only_unifi=only_unifi):
547
629
  continue
548
630
  name = _client_display_name(client)
549
631
  uplink_mac = _client_uplink_mac(client)
@@ -580,13 +662,14 @@ def build_node_type_map(
580
662
  clients: Iterable[object] | None = None,
581
663
  *,
582
664
  client_mode: str = "wired",
665
+ only_unifi: bool = False,
583
666
  ) -> dict[str, str]:
584
667
  node_types: dict[str, str] = {}
585
668
  for device in devices:
586
669
  node_types[device.name] = classify_device_type(device)
587
670
  if clients:
588
671
  for client in clients:
589
- if not _client_matches_mode(client, client_mode):
672
+ if not _client_matches_filters(client, client_mode=client_mode, only_unifi=only_unifi):
590
673
  continue
591
674
  name = _client_display_name(client)
592
675
  if name:
@@ -683,11 +766,12 @@ def build_client_port_map(
683
766
  clients: Iterable[object],
684
767
  *,
685
768
  client_mode: str,
769
+ only_unifi: bool = False,
686
770
  ) -> ClientPortMap:
687
771
  device_index = build_device_index(devices)
688
772
  port_map: ClientPortMap = {}
689
773
  for client in clients:
690
- if not _client_matches_mode(client, client_mode):
774
+ if not _client_matches_filters(client, client_mode=client_mode, only_unifi=only_unifi):
691
775
  continue
692
776
  name = _client_display_name(client)
693
777
  uplink_mac = _client_uplink_mac(client)
@@ -23,7 +23,13 @@ def _client_field(client: object, name: str) -> object | None:
23
23
 
24
24
 
25
25
  def _client_display_name(client: object) -> str | None:
26
- for key in ("name", "hostname", "mac"):
26
+ raw_name = _client_field(client, "name")
27
+ if isinstance(raw_name, str) and raw_name.strip():
28
+ return raw_name.strip()
29
+ preferred = _client_ucore_display_name(client)
30
+ if preferred:
31
+ return preferred
32
+ for key in ("hostname", "mac"):
27
33
  value = _client_field(client, key)
28
34
  if isinstance(value, str) and value.strip():
29
35
  return value.strip()
@@ -77,6 +83,73 @@ def _client_is_wired(client: object) -> bool:
77
83
  return bool(_client_field(client, "is_wired"))
78
84
 
79
85
 
86
+ def _client_unifi_flag(client: object) -> bool | None:
87
+ for key in ("is_unifi", "is_unifi_device", "is_ubnt", "is_uap", "is_managed"):
88
+ value = _client_field(client, key)
89
+ if isinstance(value, bool):
90
+ return value
91
+ if isinstance(value, int):
92
+ return value != 0
93
+ return None
94
+
95
+
96
+ def _client_vendor(client: object) -> str | None:
97
+ for key in ("oui", "vendor", "vendor_name", "manufacturer", "manufacturer_name"):
98
+ value = _client_field(client, key)
99
+ if isinstance(value, str) and value.strip():
100
+ return value.strip()
101
+ return None
102
+
103
+
104
+ def _client_ucore_info(client: object) -> dict[str, object] | None:
105
+ info = _client_field(client, "unifi_device_info_from_ucore")
106
+ if isinstance(info, dict):
107
+ return info
108
+ return None
109
+
110
+
111
+ def _client_ucore_display_name(client: object) -> str | None:
112
+ ucore = _client_ucore_info(client)
113
+ if not ucore:
114
+ return None
115
+ for key in ("name", "computed_model", "product_model", "product_shortname"):
116
+ value = ucore.get(key)
117
+ if isinstance(value, str) and value.strip():
118
+ return value.strip()
119
+ return None
120
+
121
+
122
+ def _client_hostname_source(client: object) -> str | None:
123
+ value = _client_field(client, "hostname_source")
124
+ if isinstance(value, str) and value.strip():
125
+ return value.strip()
126
+ return None
127
+
128
+
129
+ def _client_is_unifi(client: object) -> bool:
130
+ flag = _client_unifi_flag(client)
131
+ if flag is not None:
132
+ return flag
133
+ ucore = _client_ucore_info(client)
134
+ if ucore:
135
+ managed = ucore.get("managed")
136
+ if isinstance(managed, bool) and managed:
137
+ return True
138
+ if isinstance(ucore.get("product_line"), str) and ucore.get("product_line"):
139
+ return True
140
+ if isinstance(ucore.get("product_shortname"), str) and ucore.get("product_shortname"):
141
+ return True
142
+ for key in ("name", "computed_model", "product_model"):
143
+ value = ucore.get(key)
144
+ if isinstance(value, str) and value.strip():
145
+ return True
146
+ vendor = _client_vendor(client)
147
+ if not vendor:
148
+ return False
149
+ normalized = vendor.lower()
150
+ return "ubiquiti" in normalized or "unifi" in normalized
151
+
152
+
80
153
  def _client_matches_mode(client: object, mode: str) -> bool:
81
154
  wired = _client_is_wired(client)
82
155
  if mode == "all":
@@ -86,6 +159,14 @@ def _client_matches_mode(client: object, mode: str) -> bool:
86
159
  return wired
87
160
 
88
161
 
162
+ def _client_matches_filters(client: object, *, client_mode: str, only_unifi: bool) -> bool:
163
+ if not _client_matches_mode(client, client_mode):
164
+ return False
165
+ if only_unifi and not _client_is_unifi(client):
166
+ return False
167
+ return True
168
+
169
+
89
170
  def _lldp_sort_key(entry: LLDPEntry) -> tuple[int, str, str]:
90
171
  port_label = local_port_label(entry) or ""
91
172
  port_number = "".join(ch for ch in port_label if ch.isdigit())
@@ -199,10 +280,11 @@ def _client_rows(
199
280
  *,
200
281
  include_ports: bool,
201
282
  client_mode: str,
283
+ only_unifi: bool,
202
284
  ) -> dict[str, list[tuple[str, str | None]]]:
203
285
  rows_by_device: dict[str, list[tuple[str, str | None]]] = {}
204
286
  for client in clients:
205
- if not _client_matches_mode(client, client_mode):
287
+ if not _client_matches_filters(client, client_mode=client_mode, only_unifi=only_unifi):
206
288
  continue
207
289
  name = _client_display_name(client)
208
290
  uplink_mac = _client_uplink_mac(client)
@@ -227,6 +309,7 @@ def _prepare_lldp_maps(
227
309
  include_ports: bool,
228
310
  show_clients: bool,
229
311
  client_mode: str,
312
+ only_unifi: bool,
230
313
  ) -> tuple[
231
314
  dict[tuple[str, str], str],
232
315
  dict[str, list[tuple[int, str]]] | None,
@@ -234,7 +317,13 @@ def _prepare_lldp_maps(
234
317
  ]:
235
318
  device_index = build_device_index(devices)
236
319
  client_rows = (
237
- _client_rows(clients, device_index, include_ports=include_ports, client_mode=client_mode)
320
+ _client_rows(
321
+ clients,
322
+ device_index,
323
+ include_ports=include_ports,
324
+ client_mode=client_mode,
325
+ only_unifi=only_unifi,
326
+ )
238
327
  if clients
239
328
  else {}
240
329
  )
@@ -243,7 +332,12 @@ def _prepare_lldp_maps(
243
332
  if include_ports:
244
333
  port_map = build_port_map(devices, only_unifi=False)
245
334
  if clients and show_clients:
246
- client_port_map = build_client_port_map(devices, clients, client_mode=client_mode)
335
+ client_port_map = build_client_port_map(
336
+ devices,
337
+ clients,
338
+ client_mode=client_mode,
339
+ only_unifi=only_unifi,
340
+ )
247
341
  return port_map, client_port_map, client_rows
248
342
 
249
343
 
@@ -318,6 +412,7 @@ def render_lldp_md(
318
412
  include_ports: bool = False,
319
413
  show_clients: bool = False,
320
414
  client_mode: str = "wired",
415
+ only_unifi: bool = False,
321
416
  ) -> str:
322
417
  device_index = build_device_index(devices)
323
418
  port_map, client_port_map, client_rows = _prepare_lldp_maps(
@@ -326,6 +421,7 @@ def render_lldp_md(
326
421
  include_ports=include_ports,
327
422
  show_clients=show_clients,
328
423
  client_mode=client_mode,
424
+ only_unifi=only_unifi,
329
425
  )
330
426
  sections: list[str] = []
331
427
  for device in sorted(devices, key=lambda item: item.name.lower()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.4.11
3
+ Version: 1.4.12
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -223,7 +223,7 @@ Functional:
223
223
  - `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
224
224
  - `--include-clients`: add active wired clients as leaf nodes.
225
225
  - `--client-scope wired|wireless|all`: which client types to include (default wired).
226
- - `--only-unifi`: only include neighbors that are UniFi devices.
226
+ - `--only-unifi`: only include neighbors that are UniFi devices; when clients are included, filters to UniFi-managed clients (by explicit UniFi flags or vendor/OUI).
227
227
  - `--no-cache`: disable UniFi API cache reads and writes.
228
228
 
229
229
  Mermaid:
@@ -1,4 +1,4 @@
1
- unifi_network_maps/__init__.py,sha256=E_3GCl6XTRrqUMJy7bs7bnXqi2aoli2GL8PC4NQJkSE,23
1
+ unifi_network_maps/__init__.py,sha256=nGraM_ujqcNJ-Dyz-qi99_8ynLwzQU2N68yOSnVykOs,23
2
2
  unifi_network_maps/__main__.py,sha256=XsOjaqslAVgyVlOTokjVddZ2iT8apZXpJ_OB-9WEEe4,179
3
3
  unifi_network_maps/adapters/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
4
4
  unifi_network_maps/adapters/config.py,sha256=Bx9JDZxxY7Gjxyb8FDT0dxiKfgXt_TmzTDbgvpwB53s,1548
@@ -54,8 +54,8 @@ unifi_network_maps/cli/__init__.py,sha256=cds9GvFNZmYAR22Ab3TSzfriSAW--kf9jvC5U-
54
54
  unifi_network_maps/cli/__main__.py,sha256=nK_jh78VW3h3DRvSpjzpcf64zkCqniP2k82xUR9Hw2I,147
55
55
  unifi_network_maps/cli/args.py,sha256=lIgQDeob_SIhjXg76hJsnpgNOKupSjSYum_MqarWOkE,5552
56
56
  unifi_network_maps/cli/main.py,sha256=jQXesuHJLTQl4lBk1DD6em67Wj9oEjBmH9X-X1zA6MI,4150
57
- unifi_network_maps/cli/render.py,sha256=sUyDWm_I_zbEcKuNEpKUXDxhe1XptgOYsmdMP9BJ3Eg,7040
58
- unifi_network_maps/cli/runtime.py,sha256=Hln4LMpuTrEsy6gIBmqkOrUpMb4nTeZ-AH72KyxpZwA,4723
57
+ unifi_network_maps/cli/render.py,sha256=hT4RS3Y5F2TVDe85xpaOckgz9b5c8igY5Q77Uoh8nmk,7357
58
+ unifi_network_maps/cli/runtime.py,sha256=cMtIShERup2z2_uCcEGcaJFJptvdI0L3FDWqKFwSevY,4830
59
59
  unifi_network_maps/io/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
60
60
  unifi_network_maps/io/debug.py,sha256=eUVs6GLfs6VqI6ma8ra7NLhC3q4ek2K8OSRLnpQDF9s,1745
61
61
  unifi_network_maps/io/export.py,sha256=2vURunQDja-_fKKMb-gQG-n34aeM8GFprNJqcGaP4qg,843
@@ -67,11 +67,11 @@ unifi_network_maps/model/labels.py,sha256=m_k8mbzWtOSDOjjHhLUqwIw93pg98HAtGtHkiE
67
67
  unifi_network_maps/model/lldp.py,sha256=SrPW5XC2lfJgaGeVx-KnSFNltyok7gIWWQNg1SkOaj4,3300
68
68
  unifi_network_maps/model/mock.py,sha256=kd1MSiIn-7KKu_nMVmheYPfTyAN5DHt4dzRrBifF_lI,8706
69
69
  unifi_network_maps/model/ports.py,sha256=o3NBlXcC5VV5iPWJsO4Ll1mRKJZC0f8zTHdlkkE34GU,609
70
- unifi_network_maps/model/topology.py,sha256=YOiLGKFEl4AQncaiM0C_uHt85lX1rsNM4n0HEZ5K-ns,28541
70
+ unifi_network_maps/model/topology.py,sha256=l26yHsRGuD-D1x4-qsaMBRTeA8SBU_xhRCi9ZwyG7w8,31472
71
71
  unifi_network_maps/render/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
72
72
  unifi_network_maps/render/device_ports_md.py,sha256=vt5kGFSIAabMbcSxIuVVLSzdb_i58NHGi4hJM2ZLZR4,15975
73
73
  unifi_network_maps/render/legend.py,sha256=TmZsxgCVOM2CZImI9zgRVyzrcg01HZFDj9F715d4CGo,772
74
- unifi_network_maps/render/lldp_md.py,sha256=13g9G_oQ-riTDakt-_qmGX8YtEmL0eikjFMbtiTKKVo,11672
74
+ unifi_network_maps/render/lldp_md.py,sha256=wHMMoxcRicF3NirNh0L62PQFNQYOp6XFxNOTwFm7x0Y,14738
75
75
  unifi_network_maps/render/markdown_tables.py,sha256=VvM0fSnSmpeeDPcD5pXaL_j_PTF0STrMCaqnr2BVHn4,547
76
76
  unifi_network_maps/render/mermaid.py,sha256=xsC57Xg-nKhmlVATzEbwLkMM2BOeDYlBjZuxBIPhHeI,8324
77
77
  unifi_network_maps/render/mermaid_theme.py,sha256=7nqLlvhaUA4z0YOs0ByEx_yHWcQD_hJJjhDtRcbSpg4,1781
@@ -91,9 +91,9 @@ unifi_network_maps/render/templates/mkdocs_html_block.html.j2,sha256=5l5-BbNujOc
91
91
  unifi_network_maps/render/templates/mkdocs_legend.css.j2,sha256=tkTI-RagBSgdjUygVenlTsQFenU09ePbXOfDt_Q7YRM,612
92
92
  unifi_network_maps/render/templates/mkdocs_legend.js.j2,sha256=qMYyCKsJ84uXf1wGgzbc7Bc49RU4oyuaGK9KrgQDQEI,685
93
93
  unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2,sha256=9IncllWQpoI8BN3A7b2zOQ5cksj97ddsjHJ-aBhpw7o,66
94
- unifi_network_maps-1.4.11.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
95
- unifi_network_maps-1.4.11.dist-info/METADATA,sha256=ihN2YNJUN-9W1RR2USmMtMGJq72XSSULMtG8k9qry4g,9851
96
- unifi_network_maps-1.4.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- unifi_network_maps-1.4.11.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
98
- unifi_network_maps-1.4.11.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
99
- unifi_network_maps-1.4.11.dist-info/RECORD,,
94
+ unifi_network_maps-1.4.12.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
95
+ unifi_network_maps-1.4.12.dist-info/METADATA,sha256=ob7P4a7jb8JTC_OWLX4Ej0jPf8SrYNXWyhCCodGmyRM,9952
96
+ unifi_network_maps-1.4.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ unifi_network_maps-1.4.12.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
98
+ unifi_network_maps-1.4.12.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
99
+ unifi_network_maps-1.4.12.dist-info/RECORD,,