unifi-network-maps 1.4.11__tar.gz → 1.4.12__tar.gz

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.
Files changed (130) hide show
  1. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/CHANGELOG.md +10 -2
  2. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/PKG-INFO +2 -2
  3. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/README.md +1 -1
  4. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/pyproject.toml +1 -1
  5. unifi_network_maps-1.4.12/src/unifi_network_maps/__init__.py +1 -0
  6. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/render.py +19 -3
  7. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/runtime.py +7 -1
  8. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/topology.py +88 -4
  9. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/lldp_md.py +100 -4
  10. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/PKG-INFO +2 -2
  11. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_clients.py +65 -0
  12. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_lldp_md.py +55 -0
  13. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_topology.py +32 -0
  14. unifi_network_maps-1.4.11/src/unifi_network_maps/__init__.py +0 -1
  15. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/CONTRIBUTING.md +0 -0
  16. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/LICENSE +0 -0
  17. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/LICENSES.md +0 -0
  18. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/MANIFEST.in +0 -0
  19. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/RELEASING.md +0 -0
  20. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/SECURITY.md +0 -0
  21. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/setup.cfg +0 -0
  22. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/__main__.py +0 -0
  23. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/adapters/__init__.py +0 -0
  24. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/adapters/config.py +0 -0
  25. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/adapters/unifi.py +0 -0
  26. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/__init__.py +0 -0
  27. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
  28. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
  29. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
  30. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
  31. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
  32. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
  33. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
  34. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
  35. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
  36. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
  37. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
  38. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
  39. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
  40. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
  41. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
  42. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
  43. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
  44. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
  45. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
  46. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
  47. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
  48. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
  49. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
  50. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
  51. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
  52. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
  53. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
  54. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
  55. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
  56. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
  57. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
  58. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
  59. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
  60. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
  61. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
  62. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
  63. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
  64. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
  65. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
  66. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
  67. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
  68. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
  69. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
  70. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/icons/server.svg +0 -0
  71. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
  72. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
  73. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/__init__.py +0 -0
  74. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/__main__.py +0 -0
  75. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/args.py +0 -0
  76. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/cli/main.py +0 -0
  77. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/__init__.py +0 -0
  78. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/debug.py +0 -0
  79. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/export.py +0 -0
  80. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/mkdocs_assets.py +0 -0
  81. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/mock_data.py +0 -0
  82. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/io/mock_generate.py +0 -0
  83. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/__init__.py +0 -0
  84. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/labels.py +0 -0
  85. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/lldp.py +0 -0
  86. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/mock.py +0 -0
  87. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/model/ports.py +0 -0
  88. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/__init__.py +0 -0
  89. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/device_ports_md.py +0 -0
  90. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/legend.py +0 -0
  91. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/markdown_tables.py +0 -0
  92. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/mermaid.py +0 -0
  93. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
  94. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/mkdocs.py +0 -0
  95. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/svg.py +0 -0
  96. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/svg_theme.py +0 -0
  97. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
  98. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
  99. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
  100. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
  101. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
  102. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
  103. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +0 -0
  104. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
  105. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
  106. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
  107. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
  108. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/templating.py +0 -0
  109. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps/render/theme.py +0 -0
  110. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/SOURCES.txt +0 -0
  111. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
  112. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
  113. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/requires.txt +0 -0
  114. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
  115. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_cli.py +0 -0
  116. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_config.py +0 -0
  117. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_contract_unifi.py +0 -0
  118. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_contract_unifi_live.py +0 -0
  119. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_debug.py +0 -0
  120. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_device_ports_md.py +0 -0
  121. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_export.py +0 -0
  122. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_groups.py +0 -0
  123. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_labels.py +0 -0
  124. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_lldp.py +0 -0
  125. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_mermaid.py +0 -0
  126. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_mock_generate.py +0 -0
  127. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_svg.py +0 -0
  128. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_svg_iso.py +0 -0
  129. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_theme.py +0 -0
  130. {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.12}/tests/test_unifi.py +0 -0
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.12] - 2026-01-21
9
+ ### Added
10
+ - Filter UniFi clients with --only-unifi, and not only neighbors
11
+
12
+ ### Fixed
13
+ - inconsistencies in --only-unifi
14
+
8
15
  ## [1.4.11] - 2026-01-19
9
16
  ### Added
10
17
  - Add data-edge-left/right attributes to SVG paths
@@ -176,8 +183,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
176
183
  - Introduced SVG renderer and tree layout fixes.
177
184
  - Increased test coverage and added coverage tooling.
178
185
 
179
- [Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...HEAD
180
- [1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.111
186
+ [Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.12...HEAD
187
+ [1.4.12]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.12
188
+ [1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.11
181
189
  [1.4.10]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.9...v1.4.10
182
190
  [1.4.9]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.8...v1.4.9
183
191
  [1.4.8]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.7...v1.4.8
@@ -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:
@@ -186,7 +186,7 @@ Functional:
186
186
  - `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
187
187
  - `--include-clients`: add active wired clients as leaf nodes.
188
188
  - `--client-scope wired|wireless|all`: which client types to include (default wired).
189
- - `--only-unifi`: only include neighbors that are UniFi devices.
189
+ - `--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).
190
190
  - `--no-cache`: disable UniFi API cache reads and writes.
191
191
 
192
192
  Mermaid:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unifi-network-maps"
7
- version = "1.4.11"
7
+ version = "1.4.12"
8
8
  description = "Dynamic UniFi -> network maps in mermaid or svg"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1 @@
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:
@@ -57,6 +57,58 @@ def test_build_client_edges_includes_uplink_port_label():
57
57
  assert edges[0].label == "Switch A: Port 3 <-> Laptop"
58
58
 
59
59
 
60
+ def test_build_client_edges_only_unifi_filters_non_unifi():
61
+ device_index = {"aa:bb:cc:dd:ee:ff": "Switch A"}
62
+ clients = [
63
+ {"name": "Desk PC", "is_wired": True, "sw_mac": "aa:bb:cc:dd:ee:ff", "is_unifi": False},
64
+ {"name": "Protect Cam", "is_wired": True, "sw_mac": "aa:bb:cc:dd:ee:ff", "is_unifi": True},
65
+ ]
66
+ edges = build_client_edges(clients, device_index, only_unifi=True)
67
+ assert [edge.right for edge in edges] == ["Protect Cam"]
68
+
69
+
70
+ def test_build_client_edges_only_unifi_vendor_fallback():
71
+ device_index = {"aa:bb:cc:dd:ee:ff": "Switch A"}
72
+ clients = [
73
+ {
74
+ "name": "UniFi Sensor",
75
+ "is_wired": True,
76
+ "sw_mac": "aa:bb:cc:dd:ee:ff",
77
+ "oui": "Ubiquiti Inc.",
78
+ }
79
+ ]
80
+ edges = build_client_edges(clients, device_index, only_unifi=True)
81
+ assert edges[0].right == "UniFi Sensor"
82
+
83
+
84
+ def test_build_client_edges_only_unifi_ucore_managed():
85
+ device_index = {"aa:bb:cc:dd:ee:ff": "Switch A"}
86
+ clients = [
87
+ {
88
+ "name": "Doorbell Lite",
89
+ "is_wired": True,
90
+ "sw_mac": "aa:bb:cc:dd:ee:ff",
91
+ "unifi_device_info_from_ucore": {"managed": True},
92
+ }
93
+ ]
94
+ edges = build_client_edges(clients, device_index, only_unifi=True)
95
+ assert edges[0].right == "Doorbell Lite"
96
+
97
+
98
+ def test_build_client_edges_prefers_ucore_name_over_hostname():
99
+ device_index = {"aa:bb:cc:dd:ee:ff": "Switch A"}
100
+ clients = [
101
+ {
102
+ "hostname": "espressif",
103
+ "is_wired": True,
104
+ "sw_mac": "aa:bb:cc:dd:ee:ff",
105
+ "unifi_device_info_from_ucore": {"name": "Smart PoE Chime"},
106
+ }
107
+ ]
108
+ edges = build_client_edges(clients, device_index, only_unifi=True)
109
+ assert edges[0].right == "Smart PoE Chime"
110
+
111
+
60
112
  def test_build_node_type_map_skips_wireless_clients():
61
113
  devices = [
62
114
  Device(name="Gateway", model_name="", model="", mac="aa", ip="", type="udm", lldp_info=[])
@@ -64,3 +116,16 @@ def test_build_node_type_map_skips_wireless_clients():
64
116
  clients = [{"name": "Phone", "is_wired": False}]
65
117
  node_types = build_node_type_map(devices, clients)
66
118
  assert "Phone" not in node_types
119
+
120
+
121
+ def test_build_node_type_map_only_unifi_filters_clients():
122
+ devices = [
123
+ Device(name="Gateway", model_name="", model="", mac="aa", ip="", type="udm", lldp_info=[])
124
+ ]
125
+ clients = [
126
+ {"name": "Desk PC", "is_wired": True, "is_unifi": False},
127
+ {"name": "Protect Cam", "is_wired": True, "is_unifi": True},
128
+ ]
129
+ node_types = build_node_type_map(devices, clients, only_unifi=True)
130
+ assert "Protect Cam" in node_types
131
+ assert "Desk PC" not in node_types
@@ -67,6 +67,61 @@ def test_render_lldp_md_includes_clients_when_requested():
67
67
  assert "| TV | Port 3 |" in output
68
68
 
69
69
 
70
+ def test_render_lldp_md_only_unifi_filters_clients():
71
+ devices = [
72
+ Device(
73
+ name="Switch A", model_name="", model="", mac="aa:bb", ip="", type="usw", lldp_info=[]
74
+ )
75
+ ]
76
+ clients = [
77
+ {"name": "Desk PC", "is_wired": True, "sw_mac": "aa:bb", "sw_port": 1},
78
+ {
79
+ "name": "Protect Cam",
80
+ "is_wired": True,
81
+ "sw_mac": "aa:bb",
82
+ "sw_port": 2,
83
+ "is_unifi": True,
84
+ },
85
+ ]
86
+ output = render_lldp_md(
87
+ devices,
88
+ clients=clients,
89
+ include_ports=True,
90
+ show_clients=True,
91
+ client_mode="wired",
92
+ only_unifi=True,
93
+ )
94
+ assert "| Protect Cam | Port 2 |" in output
95
+ assert "Desk PC" not in output
96
+
97
+
98
+ def test_render_lldp_md_uses_ucore_name_for_clients():
99
+ devices = [
100
+ Device(
101
+ name="Switch A", model_name="", model="", mac="aa:bb", ip="", type="usw", lldp_info=[]
102
+ )
103
+ ]
104
+ clients = [
105
+ {
106
+ "hostname": "espressif",
107
+ "is_wired": True,
108
+ "sw_mac": "aa:bb",
109
+ "sw_port": 4,
110
+ "unifi_device_info_from_ucore": {"name": "Smart PoE Chime"},
111
+ }
112
+ ]
113
+ output = render_lldp_md(
114
+ devices,
115
+ clients=clients,
116
+ include_ports=True,
117
+ show_clients=True,
118
+ client_mode="wired",
119
+ only_unifi=True,
120
+ )
121
+ assert "| Smart PoE Chime | Port 4 |" in output
122
+ assert "espressif" not in output
123
+
124
+
70
125
  def test_render_lldp_md_includes_ports_only_when_enabled():
71
126
  devices = [
72
127
  Device(
@@ -422,6 +422,38 @@ def test_build_edges_only_unifi_false_uses_chassis_id():
422
422
  assert edges[0].right == "bb"
423
423
 
424
424
 
425
+ def test_build_edges_only_unifi_skips_unknown_uplink():
426
+ device = SimpleNamespace(
427
+ name="Switch",
428
+ model_name="",
429
+ model="",
430
+ mac="aa",
431
+ ip="",
432
+ type="switch",
433
+ lldp_info=[],
434
+ port_table=[],
435
+ uplink_mac="cc",
436
+ )
437
+ edges = build_edges([coerce_device(device)], only_unifi=True)
438
+ assert edges == []
439
+
440
+
441
+ def test_build_edges_only_unifi_false_includes_unknown_uplink():
442
+ device = SimpleNamespace(
443
+ name="Switch",
444
+ model_name="",
445
+ model="",
446
+ mac="aa",
447
+ ip="",
448
+ type="switch",
449
+ lldp_info=[],
450
+ port_table=[],
451
+ uplink_mac="cc",
452
+ )
453
+ edges = build_edges([coerce_device(device)], only_unifi=False)
454
+ assert (edges[0].left, edges[0].right) == ("cc", "Switch")
455
+
456
+
425
457
  def test_build_edges_resolves_port_idx_from_ifname():
426
458
  lldp = SimpleNamespace(
427
459
  chassis_id="bb",
@@ -1 +0,0 @@
1
- __version__ = "1.4.11"