unifi-network-maps 1.4.13__tar.gz → 1.4.14__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.
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/CHANGELOG.md +9 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/PKG-INFO +8 -2
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/README.md +7 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/pyproject.toml +2 -2
- unifi_network_maps-1.4.14/src/unifi_network_maps/__init__.py +1 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/unifi.py +107 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/args.py +1 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/main.py +39 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mock_data.py +17 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/paths.py +7 -3
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/mock.py +17 -1
- unifi_network_maps-1.4.14/src/unifi_network_maps/model/vlans.py +119 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/PKG-INFO +8 -2
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/SOURCES.txt +3 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_cli.py +86 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_mock_generate.py +2 -0
- unifi_network_maps-1.4.14/tests/test_vlan_info.py +30 -0
- unifi_network_maps-1.4.13/src/unifi_network_maps/__init__.py +0 -1
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/CONTRIBUTING.md +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/LICENSE +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/LICENSES.md +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/MANIFEST.in +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/RELEASING.md +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/SECURITY.md +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/setup.cfg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/__main__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/config.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/render.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/runtime.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/export.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mkdocs_assets.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mock_generate.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/lldp.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/topology.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/device_ports_md.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/legend.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/lldp_md.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/markdown_tables.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mermaid.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mkdocs.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templating.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/theme.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/requires.txt +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_clients.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_config.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_contract_unifi.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_contract_unifi_live.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_debug.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_device_ports_md.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_export.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_groups.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_lldp_md.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_mermaid.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_mkdocs.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_svg.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_svg_iso.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_theme.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/tests/test_topology.py +0 -0
- {unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/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.14] - 2026-02-01
|
|
9
|
+
### Added
|
|
10
|
+
- JSON output with VLAN inventory
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Added log message when /tmp can't be resolved
|
|
14
|
+
|
|
8
15
|
## [1.4.13] - 2026-01-25
|
|
9
16
|
### Fixed
|
|
10
17
|
- Path Traversal Vulnerability in File Operations
|
|
@@ -192,7 +199,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
192
199
|
- Introduced SVG renderer and tree layout fixes.
|
|
193
200
|
- Increased test coverage and added coverage tooling.
|
|
194
201
|
|
|
195
|
-
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.
|
|
202
|
+
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.14...HEAD
|
|
203
|
+
[1.4.14]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.14
|
|
196
204
|
[1.4.13]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.13
|
|
197
205
|
[1.4.12]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.12
|
|
198
206
|
[1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.11
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.14
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -147,6 +147,12 @@ Legend only:
|
|
|
147
147
|
unifi-network-maps --legend-only --stdout
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
JSON payload (devices + clients + VLAN inventory):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
unifi-network-maps --format json --output ./payload.json
|
|
154
|
+
```
|
|
155
|
+
|
|
150
156
|
## Home Assistant integration
|
|
151
157
|
|
|
152
158
|
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
@@ -238,7 +244,7 @@ SVG:
|
|
|
238
244
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
239
245
|
|
|
240
246
|
Output:
|
|
241
|
-
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
247
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs|json`: output format (default mermaid).
|
|
242
248
|
- `--stdout`: write output to stdout.
|
|
243
249
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
244
250
|
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
@@ -110,6 +110,12 @@ Legend only:
|
|
|
110
110
|
unifi-network-maps --legend-only --stdout
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
JSON payload (devices + clients + VLAN inventory):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
unifi-network-maps --format json --output ./payload.json
|
|
117
|
+
```
|
|
118
|
+
|
|
113
119
|
## Home Assistant integration
|
|
114
120
|
|
|
115
121
|
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
@@ -201,7 +207,7 @@ SVG:
|
|
|
201
207
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
202
208
|
|
|
203
209
|
Output:
|
|
204
|
-
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
210
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs|json`: output format (default mermaid).
|
|
205
211
|
- `--stdout`: write output to stdout.
|
|
206
212
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
207
213
|
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==80.10.
|
|
2
|
+
requires = ["setuptools==80.10.2", "wheel==0.46.3"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unifi-network-maps"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.14"
|
|
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.14"
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/unifi.py
RENAMED
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from typing import IO, TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
from ..io.paths import resolve_cache_dir
|
|
20
|
+
from ..model.vlans import build_vlan_info, normalize_networks
|
|
20
21
|
from .config import Config
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
@@ -170,6 +171,19 @@ def _serialize_devices_for_cache(devices: Sequence[object]) -> list[dict[str, ob
|
|
|
170
171
|
return [_serialize_device_for_cache(device) for device in devices]
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
def _serialize_network_for_cache(network: object) -> dict[str, object]:
|
|
175
|
+
return {
|
|
176
|
+
"name": _first_attr(network, "name", "network_name", "networkName"),
|
|
177
|
+
"vlan": _first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid"),
|
|
178
|
+
"vlan_enabled": _first_attr(network, "vlan_enabled", "vlanEnabled"),
|
|
179
|
+
"purpose": _first_attr(network, "purpose"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _serialize_networks_for_cache(networks: Sequence[object]) -> list[dict[str, object]]:
|
|
184
|
+
return [_serialize_network_for_cache(network) for network in networks]
|
|
185
|
+
|
|
186
|
+
|
|
173
187
|
def _cache_lock_path(path: Path) -> Path:
|
|
174
188
|
return path.with_suffix(path.suffix + ".lock")
|
|
175
189
|
|
|
@@ -465,3 +479,96 @@ def fetch_clients(
|
|
|
465
479
|
_save_cache(cache_path, clients)
|
|
466
480
|
logger.debug("Fetched %d clients", len(clients))
|
|
467
481
|
return clients
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def fetch_networks(
|
|
485
|
+
config: Config,
|
|
486
|
+
*,
|
|
487
|
+
site: str | None = None,
|
|
488
|
+
use_cache: bool = True,
|
|
489
|
+
) -> Sequence[object]:
|
|
490
|
+
"""Fetch network inventory from UniFi Controller."""
|
|
491
|
+
try:
|
|
492
|
+
from unifi_controller_api import UnifiAuthenticationError
|
|
493
|
+
except ImportError as exc:
|
|
494
|
+
raise RuntimeError("Missing dependency: unifi-controller-api") from exc
|
|
495
|
+
|
|
496
|
+
site_name = site or config.site
|
|
497
|
+
ttl_seconds = _cache_ttl_seconds()
|
|
498
|
+
cache_path = _cache_dir() / f"networks_{_cache_key(config.url, site_name)}.json"
|
|
499
|
+
if use_cache and _is_cache_dir_safe(cache_path.parent):
|
|
500
|
+
cached = _load_cache(cache_path, ttl_seconds)
|
|
501
|
+
stale_cached, cache_age = _load_cache_with_age(cache_path)
|
|
502
|
+
else:
|
|
503
|
+
cached = None
|
|
504
|
+
stale_cached, cache_age = None, None
|
|
505
|
+
if cached is not None:
|
|
506
|
+
logger.debug("Using cached networks (%d)", len(cached))
|
|
507
|
+
return cached
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
controller = _init_controller(config, is_udm_pro=True)
|
|
511
|
+
except UnifiAuthenticationError:
|
|
512
|
+
logger.debug("UDM Pro authentication failed, retrying legacy auth")
|
|
513
|
+
controller = _init_controller(config, is_udm_pro=False)
|
|
514
|
+
|
|
515
|
+
def _fetch() -> Sequence[object]:
|
|
516
|
+
try:
|
|
517
|
+
return controller.get_unifi_site_networkconf(site_name=site_name, raw=False)
|
|
518
|
+
except Exception as exc: # noqa: BLE001 - fallback to raw network data
|
|
519
|
+
logger.warning("Networkconf model parse failed; retrying raw fetch: %s", exc)
|
|
520
|
+
return controller.get_unifi_site_networkconf(site_name=site_name, raw=True)
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
networks = _call_with_retries("network fetch", _fetch)
|
|
524
|
+
except Exception as exc: # noqa: BLE001 - fallback to cache
|
|
525
|
+
if stale_cached is not None:
|
|
526
|
+
logger.warning(
|
|
527
|
+
"Network fetch failed; using stale cache (%ds old): %s",
|
|
528
|
+
int(cache_age or 0),
|
|
529
|
+
exc,
|
|
530
|
+
)
|
|
531
|
+
return stale_cached
|
|
532
|
+
raise
|
|
533
|
+
if use_cache:
|
|
534
|
+
_save_cache(cache_path, _serialize_networks_for_cache(networks))
|
|
535
|
+
logger.debug("Fetched %d networks", len(networks))
|
|
536
|
+
return networks
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def fetch_payload(
|
|
540
|
+
config: Config,
|
|
541
|
+
*,
|
|
542
|
+
site: str | None = None,
|
|
543
|
+
include_clients: bool = True,
|
|
544
|
+
use_cache: bool = True,
|
|
545
|
+
) -> dict[str, list[object] | list[dict[str, object]]]:
|
|
546
|
+
"""Fetch devices, clients, and VLAN inventory for payload output."""
|
|
547
|
+
devices = list(fetch_devices(config, site=site, detailed=True, use_cache=use_cache))
|
|
548
|
+
clients = _fetch_payload_clients(
|
|
549
|
+
config,
|
|
550
|
+
site=site,
|
|
551
|
+
include_clients=include_clients,
|
|
552
|
+
use_cache=use_cache,
|
|
553
|
+
)
|
|
554
|
+
networks = list(fetch_networks(config, site=site, use_cache=use_cache))
|
|
555
|
+
normalized_networks = normalize_networks(networks)
|
|
556
|
+
vlan_info = build_vlan_info(clients, normalized_networks)
|
|
557
|
+
return {
|
|
558
|
+
"devices": devices,
|
|
559
|
+
"clients": clients,
|
|
560
|
+
"networks": normalized_networks,
|
|
561
|
+
"vlan_info": vlan_info,
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _fetch_payload_clients(
|
|
566
|
+
config: Config,
|
|
567
|
+
*,
|
|
568
|
+
site: str | None,
|
|
569
|
+
include_clients: bool,
|
|
570
|
+
use_cache: bool,
|
|
571
|
+
) -> list[object]:
|
|
572
|
+
if not include_clients:
|
|
573
|
+
return []
|
|
574
|
+
return list(fetch_clients(config, site=site, use_cache=use_cache))
|
|
@@ -125,7 +125,7 @@ def add_general_render_args(parser: argparse._ArgumentGroup) -> None:
|
|
|
125
125
|
parser.add_argument(
|
|
126
126
|
"--format",
|
|
127
127
|
default="mermaid",
|
|
128
|
-
choices=["mermaid", "svg", "svg-iso", "lldp-md", "mkdocs"],
|
|
128
|
+
choices=["mermaid", "svg", "svg-iso", "lldp-md", "mkdocs", "json"],
|
|
129
129
|
help="Output format",
|
|
130
130
|
)
|
|
131
131
|
parser.add_argument(
|
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from ..adapters.config import Config
|
|
11
|
+
from ..adapters.unifi import fetch_payload
|
|
10
12
|
from ..io.export import write_output
|
|
11
|
-
from ..io.mock_data import load_mock_data
|
|
13
|
+
from ..io.mock_data import load_mock_data, load_mock_payload
|
|
12
14
|
from ..io.paths import (
|
|
13
15
|
resolve_env_file,
|
|
14
16
|
resolve_mock_data_path,
|
|
15
17
|
resolve_output_path,
|
|
16
18
|
resolve_theme_path,
|
|
17
19
|
)
|
|
20
|
+
from ..model.vlans import build_vlan_info, normalize_networks
|
|
18
21
|
from ..render.legend import render_legend_only, resolve_legend_style
|
|
19
22
|
from ..render.theme import resolve_themes
|
|
20
23
|
from .args import build_parser
|
|
@@ -117,6 +120,38 @@ def _load_runtime_context(
|
|
|
117
120
|
return config, site, None, None
|
|
118
121
|
|
|
119
122
|
|
|
123
|
+
def _handle_json_format(
|
|
124
|
+
args: argparse.Namespace,
|
|
125
|
+
*,
|
|
126
|
+
config: Config | None,
|
|
127
|
+
site: str,
|
|
128
|
+
) -> int | None:
|
|
129
|
+
if args.format != "json":
|
|
130
|
+
return None
|
|
131
|
+
payload: dict[str, list[object] | list[dict[str, object]]]
|
|
132
|
+
if args.mock_data:
|
|
133
|
+
payload = load_mock_payload(args.mock_data)
|
|
134
|
+
if not args.include_clients:
|
|
135
|
+
payload["clients"] = []
|
|
136
|
+
networks = normalize_networks(payload.get("networks", []))
|
|
137
|
+
payload["networks"] = networks
|
|
138
|
+
payload["vlan_info"] = build_vlan_info(payload.get("clients", []), networks)
|
|
139
|
+
else:
|
|
140
|
+
if config is None:
|
|
141
|
+
logging.error("Config required to run")
|
|
142
|
+
return 2
|
|
143
|
+
payload = fetch_payload(
|
|
144
|
+
config,
|
|
145
|
+
site=site,
|
|
146
|
+
include_clients=args.include_clients,
|
|
147
|
+
use_cache=not args.no_cache,
|
|
148
|
+
)
|
|
149
|
+
content = json.dumps(payload, indent=2, sort_keys=True)
|
|
150
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
151
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
120
155
|
def main(argv: list[str] | None = None) -> int:
|
|
121
156
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
122
157
|
for handler in logging.getLogger().handlers:
|
|
@@ -132,6 +167,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
132
167
|
except ValueError as exc:
|
|
133
168
|
logging.error(str(exc))
|
|
134
169
|
return 2
|
|
170
|
+
payload_result = _handle_json_format(args, config=config, site=site)
|
|
171
|
+
if payload_result is not None:
|
|
172
|
+
return payload_result
|
|
135
173
|
try:
|
|
136
174
|
mermaid_theme, svg_theme = resolve_themes(args.theme_file)
|
|
137
175
|
except Exception as exc: # noqa: BLE001
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mock_data.py
RENAMED
|
@@ -23,3 +23,20 @@ def load_mock_data(path: str) -> tuple[list[object], list[object]]:
|
|
|
23
23
|
devices = _as_list(payload.get("devices"), "devices")
|
|
24
24
|
clients = _as_list(payload.get("clients"), "clients")
|
|
25
25
|
return devices, clients
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_mock_payload(path: str) -> dict[str, list[object] | list[dict[str, object]]]:
|
|
29
|
+
resolved = resolve_mock_data_path(path)
|
|
30
|
+
payload = json.loads(resolved.read_text(encoding="utf-8"))
|
|
31
|
+
if not isinstance(payload, dict):
|
|
32
|
+
raise ValueError("Mock data must be a JSON object")
|
|
33
|
+
devices = _as_list(payload.get("devices"), "devices")
|
|
34
|
+
clients = _as_list(payload.get("clients"), "clients")
|
|
35
|
+
networks = _as_list(payload.get("networks"), "networks")
|
|
36
|
+
vlan_info = _as_list(payload.get("vlan_info"), "vlan_info")
|
|
37
|
+
return {
|
|
38
|
+
"devices": devices,
|
|
39
|
+
"clients": clients,
|
|
40
|
+
"networks": networks,
|
|
41
|
+
"vlan_info": vlan_info,
|
|
42
|
+
}
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
from collections.abc import Iterable
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
def _safe_home_dir() -> Path | None:
|
|
12
15
|
try:
|
|
@@ -22,8 +25,9 @@ def _base_roots() -> list[Path]:
|
|
|
22
25
|
roots.append(home)
|
|
23
26
|
try:
|
|
24
27
|
roots.append(Path(tempfile.gettempdir()).resolve())
|
|
25
|
-
except
|
|
26
|
-
|
|
28
|
+
except OSError as exc:
|
|
29
|
+
# Best-effort temp dir; resolution can fail in restricted environments.
|
|
30
|
+
logger.debug("Failed to resolve temp directory: %s", exc)
|
|
27
31
|
return roots
|
|
28
32
|
|
|
29
33
|
|
|
@@ -176,7 +180,7 @@ def resolve_output_path(path: str | Path, *, format_name: str | None) -> Path:
|
|
|
176
180
|
extensions: set[str] | None
|
|
177
181
|
if format_name == "svg" or format_name == "svg-iso":
|
|
178
182
|
extensions = {".svg"}
|
|
179
|
-
elif format_name
|
|
183
|
+
elif format_name in {"mock", "json"}:
|
|
180
184
|
extensions = {".json"}
|
|
181
185
|
elif format_name == "mermaid":
|
|
182
186
|
extensions = {".md", ".mermaid", ".mmd"}
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/mock.py
RENAMED
|
@@ -9,6 +9,8 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from faker import Faker
|
|
11
11
|
|
|
12
|
+
from .vlans import build_vlan_info, normalize_networks
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass(frozen=True)
|
|
14
16
|
class MockOptions:
|
|
@@ -34,7 +36,14 @@ def generate_mock_payload(options: MockOptions) -> dict[str, list[dict[str, Any]
|
|
|
34
36
|
state = _build_state(options.seed)
|
|
35
37
|
devices, core_switch, aps = _build_devices(options, state)
|
|
36
38
|
clients = _build_clients(options, state, core_switch, aps)
|
|
37
|
-
|
|
39
|
+
networks = _build_networks()
|
|
40
|
+
vlan_info = build_vlan_info(clients, networks)
|
|
41
|
+
return {
|
|
42
|
+
"devices": devices,
|
|
43
|
+
"clients": clients,
|
|
44
|
+
"networks": normalize_networks(networks),
|
|
45
|
+
"vlan_info": vlan_info,
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
def mock_payload_json(options: MockOptions) -> str:
|
|
@@ -108,6 +117,13 @@ def _build_clients(
|
|
|
108
117
|
return clients
|
|
109
118
|
|
|
110
119
|
|
|
120
|
+
def _build_networks() -> list[dict[str, Any]]:
|
|
121
|
+
return [
|
|
122
|
+
{"name": "LAN", "vlan_enabled": False, "purpose": "corporate"},
|
|
123
|
+
{"name": "Guest", "vlan": 20, "vlan_enabled": True, "purpose": "guest"},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
111
127
|
def _build_wired_clients(
|
|
112
128
|
count: int, state: _MockState, core_switch: dict[str, Any]
|
|
113
129
|
) -> list[dict[str, Any]]:
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""VLAN inventory helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _as_list(value: object | None) -> list[object]:
|
|
9
|
+
if value is None:
|
|
10
|
+
return []
|
|
11
|
+
if isinstance(value, list):
|
|
12
|
+
return value
|
|
13
|
+
if isinstance(value, dict):
|
|
14
|
+
return [value]
|
|
15
|
+
if isinstance(value, str | bytes):
|
|
16
|
+
return []
|
|
17
|
+
if isinstance(value, Iterable):
|
|
18
|
+
return list(value)
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_attr(obj: object, name: str) -> object | None:
|
|
23
|
+
if isinstance(obj, dict):
|
|
24
|
+
return obj.get(name)
|
|
25
|
+
return getattr(obj, name, None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _first_attr(obj: object, *names: str) -> object | None:
|
|
29
|
+
for name in names:
|
|
30
|
+
value = _get_attr(obj, name)
|
|
31
|
+
if value is not None:
|
|
32
|
+
return value
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _as_bool(value: object | None) -> bool:
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
return value
|
|
39
|
+
if isinstance(value, int | float):
|
|
40
|
+
return value != 0
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _as_vlan_id(value: object | None) -> int | None:
|
|
47
|
+
if isinstance(value, int):
|
|
48
|
+
return value if value > 0 else None
|
|
49
|
+
if isinstance(value, str):
|
|
50
|
+
return int(value) if value.isdigit() and int(value) > 0 else None
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _network_vlan_id(network: object) -> int | None:
|
|
55
|
+
vlan_value = _first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid")
|
|
56
|
+
vlan_enabled = _as_bool(_first_attr(network, "vlan_enabled", "vlanEnabled"))
|
|
57
|
+
vlan_id = _as_vlan_id(vlan_value)
|
|
58
|
+
if vlan_id is not None:
|
|
59
|
+
return vlan_id
|
|
60
|
+
if not vlan_enabled:
|
|
61
|
+
return 1
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_networks(networks: Iterable[object]) -> list[dict[str, object]]:
|
|
66
|
+
normalized: list[dict[str, object]] = []
|
|
67
|
+
for network in _as_list(networks):
|
|
68
|
+
if network is None:
|
|
69
|
+
continue
|
|
70
|
+
normalized.append(
|
|
71
|
+
{
|
|
72
|
+
"network_id": _first_attr(network, "_id", "id", "network_id", "networkId"),
|
|
73
|
+
"name": _first_attr(network, "name", "network_name", "networkName"),
|
|
74
|
+
"vlan_id": _network_vlan_id(network),
|
|
75
|
+
"vlan_enabled": _as_bool(_first_attr(network, "vlan_enabled", "vlanEnabled")),
|
|
76
|
+
"purpose": _first_attr(network, "purpose"),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
return normalized
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_vlan_info(
|
|
83
|
+
clients: Iterable[object], networks: Iterable[object]
|
|
84
|
+
) -> list[dict[str, object]]:
|
|
85
|
+
vlan_counts = _client_vlan_counts(clients)
|
|
86
|
+
vlan_entries = _network_vlan_entries(networks)
|
|
87
|
+
for vlan_id, count in vlan_counts.items():
|
|
88
|
+
entry = vlan_entries.setdefault(
|
|
89
|
+
vlan_id,
|
|
90
|
+
{"id": vlan_id, "name": None, "client_count": 0},
|
|
91
|
+
)
|
|
92
|
+
entry["client_count"] = count
|
|
93
|
+
return [vlan_entries[key] for key in sorted(vlan_entries)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _client_vlan_counts(clients: Iterable[object]) -> dict[int, int]:
|
|
97
|
+
vlan_counts: dict[int, int] = {}
|
|
98
|
+
for client in _as_list(clients):
|
|
99
|
+
vlan_id = _as_vlan_id(_first_attr(client, "vlan", "vlan_id", "vlanId", "vlanid"))
|
|
100
|
+
if vlan_id is None:
|
|
101
|
+
continue
|
|
102
|
+
vlan_counts[vlan_id] = vlan_counts.get(vlan_id, 0) + 1
|
|
103
|
+
return vlan_counts
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _network_vlan_entries(networks: Iterable[object]) -> dict[int, dict[str, object]]:
|
|
107
|
+
vlan_entries: dict[int, dict[str, object]] = {}
|
|
108
|
+
for network in normalize_networks(networks):
|
|
109
|
+
vlan_id = network.get("vlan_id")
|
|
110
|
+
if not isinstance(vlan_id, int):
|
|
111
|
+
continue
|
|
112
|
+
entry = vlan_entries.setdefault(
|
|
113
|
+
vlan_id,
|
|
114
|
+
{"id": vlan_id, "name": None, "client_count": 0},
|
|
115
|
+
)
|
|
116
|
+
name = network.get("name")
|
|
117
|
+
if name and not entry["name"]:
|
|
118
|
+
entry["name"] = name
|
|
119
|
+
return vlan_entries
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.14
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -147,6 +147,12 @@ Legend only:
|
|
|
147
147
|
unifi-network-maps --legend-only --stdout
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
JSON payload (devices + clients + VLAN inventory):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
unifi-network-maps --format json --output ./payload.json
|
|
154
|
+
```
|
|
155
|
+
|
|
150
156
|
## Home Assistant integration
|
|
151
157
|
|
|
152
158
|
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
@@ -238,7 +244,7 @@ SVG:
|
|
|
238
244
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
239
245
|
|
|
240
246
|
Output:
|
|
241
|
-
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
247
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs|json`: output format (default mermaid).
|
|
242
248
|
- `--stdout`: write output to stdout.
|
|
243
249
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
244
250
|
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/SOURCES.txt
RENAMED
|
@@ -84,6 +84,7 @@ src/unifi_network_maps/model/lldp.py
|
|
|
84
84
|
src/unifi_network_maps/model/mock.py
|
|
85
85
|
src/unifi_network_maps/model/ports.py
|
|
86
86
|
src/unifi_network_maps/model/topology.py
|
|
87
|
+
src/unifi_network_maps/model/vlans.py
|
|
87
88
|
src/unifi_network_maps/render/__init__.py
|
|
88
89
|
src/unifi_network_maps/render/device_ports_md.py
|
|
89
90
|
src/unifi_network_maps/render/legend.py
|
|
@@ -126,4 +127,5 @@ tests/test_svg.py
|
|
|
126
127
|
tests/test_svg_iso.py
|
|
127
128
|
tests/test_theme.py
|
|
128
129
|
tests/test_topology.py
|
|
129
|
-
tests/test_unifi.py
|
|
130
|
+
tests/test_unifi.py
|
|
131
|
+
tests/test_vlan_info.py
|
|
@@ -155,6 +155,92 @@ def test_main_mermaid_includes_wired_clients(monkeypatch):
|
|
|
155
155
|
assert captured["node_types"]["Client"] == "client"
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def test_main_payload_from_mock_includes_vlan_info(monkeypatch, tmp_path):
|
|
159
|
+
captured = {}
|
|
160
|
+
payload = {
|
|
161
|
+
"devices": [
|
|
162
|
+
{
|
|
163
|
+
"name": "Gateway",
|
|
164
|
+
"model_name": "",
|
|
165
|
+
"model": "",
|
|
166
|
+
"mac": "aa:bb",
|
|
167
|
+
"ip": "",
|
|
168
|
+
"type": "udm",
|
|
169
|
+
"lldp_info": [],
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"clients": [{"name": "Client A", "is_wired": True, "sw_mac": "aa:bb", "vlan": 20}],
|
|
173
|
+
"networks": [
|
|
174
|
+
{"name": "LAN", "vlan_enabled": False},
|
|
175
|
+
{"name": "Guest", "vlan": 20, "vlan_enabled": True},
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
mock_path = tmp_path / "mock.json"
|
|
179
|
+
mock_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
180
|
+
|
|
181
|
+
def write_output(content, *, output_path, stdout, **_kwargs):
|
|
182
|
+
captured["content"] = content
|
|
183
|
+
|
|
184
|
+
monkeypatch.setattr(cli_module.Config, "from_env", lambda **_kwargs: _dummy_config())
|
|
185
|
+
monkeypatch.setattr(cli_module, "write_output", write_output)
|
|
186
|
+
|
|
187
|
+
assert (
|
|
188
|
+
main(
|
|
189
|
+
[
|
|
190
|
+
"--mock-data",
|
|
191
|
+
str(mock_path),
|
|
192
|
+
"--format",
|
|
193
|
+
"json",
|
|
194
|
+
"--include-clients",
|
|
195
|
+
"--stdout",
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
== 0
|
|
199
|
+
)
|
|
200
|
+
output = json.loads(captured["content"])
|
|
201
|
+
assert "networks" in output
|
|
202
|
+
assert "vlan_info" in output
|
|
203
|
+
vlan_map = {entry["id"]: entry for entry in output["vlan_info"]}
|
|
204
|
+
assert vlan_map[1]["client_count"] == 0
|
|
205
|
+
assert vlan_map[20]["client_count"] == 1
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_main_payload_from_mock_excludes_clients_by_default(monkeypatch, tmp_path):
|
|
209
|
+
captured = {}
|
|
210
|
+
payload = {
|
|
211
|
+
"devices": [
|
|
212
|
+
{
|
|
213
|
+
"name": "Gateway",
|
|
214
|
+
"model_name": "",
|
|
215
|
+
"model": "",
|
|
216
|
+
"mac": "aa:bb",
|
|
217
|
+
"ip": "",
|
|
218
|
+
"type": "udm",
|
|
219
|
+
"lldp_info": [],
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
"clients": [{"name": "Client A", "is_wired": True, "sw_mac": "aa:bb", "vlan": 20}],
|
|
223
|
+
"networks": [
|
|
224
|
+
{"name": "LAN", "vlan_enabled": False},
|
|
225
|
+
{"name": "Guest", "vlan": 20, "vlan_enabled": True},
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
mock_path = tmp_path / "mock.json"
|
|
229
|
+
mock_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
230
|
+
|
|
231
|
+
def write_output(content, *, output_path, stdout, **_kwargs):
|
|
232
|
+
captured["content"] = content
|
|
233
|
+
|
|
234
|
+
monkeypatch.setattr(cli_module.Config, "from_env", lambda **_kwargs: _dummy_config())
|
|
235
|
+
monkeypatch.setattr(cli_module, "write_output", write_output)
|
|
236
|
+
|
|
237
|
+
assert main(["--mock-data", str(mock_path), "--format", "json", "--stdout"]) == 0
|
|
238
|
+
output = json.loads(captured["content"])
|
|
239
|
+
vlan_map = {entry["id"]: entry for entry in output["vlan_info"]}
|
|
240
|
+
assert vlan_map[1]["client_count"] == 0
|
|
241
|
+
assert vlan_map[20]["client_count"] == 0
|
|
242
|
+
|
|
243
|
+
|
|
158
244
|
def test_main_logs_topology_errors(monkeypatch, caplog):
|
|
159
245
|
monkeypatch.setattr(cli_module.Config, "from_env", lambda **_kwargs: _dummy_config())
|
|
160
246
|
monkeypatch.setattr(runtime_module, "fetch_devices", lambda *args, **kwargs: [])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from unifi_network_maps.model.vlans import build_vlan_info
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _vlan_map(entries: list[dict[str, object]]) -> dict[int, dict[str, object]]:
|
|
5
|
+
mapped: dict[int, dict[str, object]] = {}
|
|
6
|
+
for entry in entries:
|
|
7
|
+
vlan_id = entry.get("id")
|
|
8
|
+
if isinstance(vlan_id, int):
|
|
9
|
+
mapped[vlan_id] = entry
|
|
10
|
+
return mapped
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_build_vlan_info_includes_networks_and_clients():
|
|
14
|
+
networks = [
|
|
15
|
+
{"name": "LAN", "vlan_enabled": False},
|
|
16
|
+
{"name": "Guest", "vlan": 20, "vlan_enabled": True},
|
|
17
|
+
]
|
|
18
|
+
clients = [
|
|
19
|
+
{"name": "Client A", "is_wired": True, "vlan": 20},
|
|
20
|
+
{"name": "Client B", "is_wired": True, "vlan": 30},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
vlan_info = build_vlan_info(clients, networks)
|
|
24
|
+
vlan_map = _vlan_map(vlan_info)
|
|
25
|
+
|
|
26
|
+
assert vlan_map[1]["name"] == "LAN"
|
|
27
|
+
assert vlan_map[1]["client_count"] == 0
|
|
28
|
+
assert vlan_map[20]["name"] == "Guest"
|
|
29
|
+
assert vlan_map[20]["client_count"] == 1
|
|
30
|
+
assert vlan_map[30]["client_count"] == 1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.4.13"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/__init__.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/config.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__init__.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__main__.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/render.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/runtime.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mkdocs_assets.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mock_generate.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/__init__.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/labels.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/lldp.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/ports.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/topology.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/legend.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/lldp_md.py
RENAMED
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mermaid.py
RENAMED
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mkdocs.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg_theme.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templating.py
RENAMED
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unifi_network_maps-1.4.13 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|