unifi-network-maps 1.3.1__tar.gz → 1.4.1__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.3.1 → unifi_network_maps-1.4.1}/CHANGELOG.md +19 -2
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/CONTRIBUTING.md +2 -1
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/PKG-INFO +89 -15
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/README.md +78 -3
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/RELEASING.md +2 -1
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/pyproject.toml +20 -17
- unifi_network_maps-1.4.1/src/unifi_network_maps/__init__.py +1 -0
- unifi_network_maps-1.4.1/src/unifi_network_maps/__main__.py +8 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/unifi.py +90 -9
- unifi_network_maps-1.4.1/src/unifi_network_maps/cli/main.py +614 -0
- unifi_network_maps-1.4.1/src/unifi_network_maps/io/mock_data.py +23 -0
- unifi_network_maps-1.4.1/src/unifi_network_maps/io/mock_generate.py +299 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/lldp.py +26 -12
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/topology.py +111 -3
- unifi_network_maps-1.4.1/src/unifi_network_maps/render/device_ports_md.py +462 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/lldp_md.py +33 -12
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/mermaid.py +62 -3
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/PKG-INFO +89 -15
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/SOURCES.txt +6 -0
- unifi_network_maps-1.4.1/src/unifi_network_maps.egg-info/requires.txt +10 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_cli.py +189 -6
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_clients.py +3 -1
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_debug.py +2 -2
- unifi_network_maps-1.4.1/tests/test_device_ports_md.py +526 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_groups.py +6 -2
- unifi_network_maps-1.4.1/tests/test_lldp_md.py +296 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_mermaid.py +9 -2
- unifi_network_maps-1.4.1/tests/test_mock_generate.py +28 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_topology.py +36 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_unifi.py +72 -0
- unifi_network_maps-1.3.1/src/unifi_network_maps/__init__.py +0 -1
- unifi_network_maps-1.3.1/src/unifi_network_maps/cli/main.py +0 -317
- unifi_network_maps-1.3.1/src/unifi_network_maps.egg-info/requires.txt +0 -9
- unifi_network_maps-1.3.1/tests/test_lldp_md.py +0 -42
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/LICENSE +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/LICENSES.md +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/MANIFEST.in +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/SECURITY.md +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/setup.cfg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/config.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/export.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/svg.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/theme.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_config.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_export.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_svg.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_svg_iso.py +0 -0
- {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_theme.py +0 -0
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
-
|
|
5
|
+
## v1.4.0
|
|
6
|
+
- Added MkDocs output, which includes gateway/switch details and per-port tables.
|
|
7
|
+
- Port tables show speed, PoE status, power, and wired clients per port.
|
|
8
|
+
- Added compact legend with sidebar injection (`--mkdocs-sidebar-legend`).
|
|
9
|
+
- LLDP markdown includes the same device details and port tables when enabled.
|
|
10
|
+
- Improved uplink labeling (gateway shows Internet for WAN/unknown).
|
|
11
|
+
- Aggregated ports are combined into single LAG rows.
|
|
12
|
+
- Bumped minimum Python to 3.13 and aligned CI to 3.13.
|
|
13
|
+
- Pinned runtime/dev/build dependencies and added `requirements*.txt` + `constraints.txt`.
|
|
14
|
+
- Added `--mock-data` for safe, offline rendering from fixtures.
|
|
15
|
+
- Added Faker-powered `--generate-mock` for deterministic mock fixtures (dev-only).
|
|
16
|
+
- Added mock fixtures + SVG/Mermaid examples, with mock smoketest/CI steps.
|
|
17
|
+
|
|
18
|
+
## v1.3.1
|
|
19
|
+
- Added `lldp-md` output with per-device details tables and optional client sections.
|
|
20
|
+
- Added `--client-scope wired|wireless|all` and dashed wireless client links in Mermaid/SVG.
|
|
21
|
+
- Expanded smoketest outputs for wireless/all client scopes and LLDP markdown.
|
|
22
|
+
- Fixed SVG icon loading paths after package reorg.
|
|
23
|
+
- Tuned isometric port label placement on front tiles.
|
|
7
24
|
|
|
8
25
|
## v1.3.0
|
|
9
26
|
- Reorganized package into submodules (`adapters/`, `model/`, `render/`, `io/`, `cli/`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,22 +14,21 @@ Classifier: Intended Audience :: System Administrators
|
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
18
|
Classifier: Topic :: Documentation
|
|
21
19
|
Classifier: Topic :: System :: Networking
|
|
22
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.13
|
|
23
21
|
Description-Content-Type: text/markdown
|
|
24
22
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: unifi-controller-api
|
|
26
|
-
Requires-Dist: python-dotenv
|
|
27
|
-
Requires-Dist: PyYAML
|
|
23
|
+
Requires-Dist: unifi-controller-api==0.3.2
|
|
24
|
+
Requires-Dist: python-dotenv==1.2.1
|
|
25
|
+
Requires-Dist: PyYAML==6.0.3
|
|
28
26
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist: pytest
|
|
32
|
-
Requires-Dist:
|
|
27
|
+
Requires-Dist: Faker==40.1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pre-commit==4.5.1; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov==7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff==0.14.10; extra == "dev"
|
|
33
32
|
Dynamic: license-file
|
|
34
33
|
|
|
35
34
|
# unifi-network-maps
|
|
@@ -38,13 +37,14 @@ Dynamic UniFi -> Mermaid network maps generated from LLDP topology.
|
|
|
38
37
|
|
|
39
38
|
## Setup
|
|
40
39
|
|
|
41
|
-
- Python >= 3.
|
|
40
|
+
- Python >= 3.13
|
|
42
41
|
- Virtualenv required
|
|
43
42
|
|
|
44
43
|
```bash
|
|
45
44
|
python -m venv .venv
|
|
46
45
|
source .venv/bin/activate
|
|
47
|
-
pip install -
|
|
46
|
+
pip install -r requirements-build.txt
|
|
47
|
+
pip install -e . -c constraints.txt
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Local install (non-editable):
|
|
@@ -101,6 +101,15 @@ Isometric SVG output:
|
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
103
|
unifi-network-maps --format svg-iso --output ./network.svg
|
|
104
|
+
|
|
105
|
+
# Single-page MkDocs output (ports included, no clients)
|
|
106
|
+
unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
|
|
107
|
+
|
|
108
|
+
# MkDocs output (map + legend + gateway/switch port tables)
|
|
109
|
+
unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
|
|
110
|
+
|
|
111
|
+
# Include wired clients in the port tables
|
|
112
|
+
unifi-network-maps --format mkdocs --include-clients --output ./docs/unifi-network.md
|
|
104
113
|
```
|
|
105
114
|
|
|
106
115
|
SVG size overrides:
|
|
@@ -118,12 +127,61 @@ Legend only:
|
|
|
118
127
|
unifi-network-maps --legend-only --stdout
|
|
119
128
|
```
|
|
120
129
|
|
|
130
|
+
## Examples (mock data)
|
|
131
|
+
|
|
132
|
+
These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
|
|
133
|
+
Mock generation requires dev dependencies (`pip install -r requirements-dev.txt -c constraints.txt`).
|
|
134
|
+
Regenerate the fixture + SVG with `make mock-data`.
|
|
135
|
+
|
|
136
|
+
Generate mock data (dev-only, uses Faker):
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
unifi-network-maps --generate-mock examples/mock_data.json --mock-seed 1337
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Generate the isometric SVG:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
unifi-network-maps --mock-data examples/mock_data.json \
|
|
146
|
+
--include-ports --include-clients --format svg-iso \
|
|
147
|
+
--output examples/output/network_ports_clients_iso.svg
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+

|
|
151
|
+
|
|
152
|
+
Mermaid example with ports:
|
|
153
|
+
|
|
154
|
+
```mermaid
|
|
155
|
+
graph TB
|
|
156
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 7 (AP Attic) <-> AP Attic: Port 1 (Core Switch)"| ap_attic["AP Attic"];
|
|
157
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 3 (AP Living Room) <-> AP Living Room: Port 1 (Core Switch)"| ap_living_room["AP Living Room"];
|
|
158
|
+
cloud_gateway["Cloud Gateway"] ---|"Cloud Gateway: Port 9 (Core Switch) <-> Core Switch: Port 1 (Cloud Gateway)"| core_switch["Core Switch"];
|
|
159
|
+
class cloud_gateway node_gateway;
|
|
160
|
+
class core_switch node_switch;
|
|
161
|
+
class ap_living_room node_ap;
|
|
162
|
+
class ap_attic node_ap;
|
|
163
|
+
classDef node_gateway fill:#ffe3b3,stroke:#d98300,stroke-width:1px;
|
|
164
|
+
classDef node_switch fill:#d6ecff,stroke:#3a7bd5,stroke-width:1px;
|
|
165
|
+
classDef node_ap fill:#d7f5e7,stroke:#27ae60,stroke-width:1px;
|
|
166
|
+
classDef node_client fill:#f2e5ff,stroke:#7f3fbf,stroke-width:1px;
|
|
167
|
+
classDef node_other fill:#eeeeee,stroke:#8f8f8f,stroke-width:1px;
|
|
168
|
+
classDef node_legend font-size:10px;
|
|
169
|
+
linkStyle 0 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
170
|
+
linkStyle 1 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
171
|
+
```
|
|
172
|
+
|
|
121
173
|
## Local install check
|
|
122
174
|
|
|
123
175
|
```bash
|
|
124
176
|
pip install .
|
|
125
177
|
```
|
|
126
178
|
|
|
179
|
+
## Dev
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -r requirements-dev.txt -c constraints.txt
|
|
183
|
+
```
|
|
184
|
+
|
|
127
185
|
## Release
|
|
128
186
|
|
|
129
187
|
Build and upload to PyPI:
|
|
@@ -164,6 +222,14 @@ The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `O
|
|
|
164
222
|
Source:
|
|
165
223
|
- `--site`: override `UNIFI_SITE`.
|
|
166
224
|
- `--env-file`: load environment variables from a specific `.env` file.
|
|
225
|
+
- `--mock-data`: use mock data JSON instead of the UniFi API.
|
|
226
|
+
Mock:
|
|
227
|
+
- `--generate-mock`: write mock data JSON and exit.
|
|
228
|
+
- `--mock-seed`: seed for deterministic mock generation.
|
|
229
|
+
- `--mock-switches`: number of switches to generate.
|
|
230
|
+
- `--mock-aps`: number of access points to generate.
|
|
231
|
+
- `--mock-wired-clients`: number of wired clients to generate.
|
|
232
|
+
- `--mock-wireless-clients`: number of wireless clients to generate.
|
|
167
233
|
|
|
168
234
|
Functional:
|
|
169
235
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
@@ -174,6 +240,8 @@ Functional:
|
|
|
174
240
|
Mermaid:
|
|
175
241
|
- `--direction LR|TB`: diagram direction for Mermaid (default TB).
|
|
176
242
|
- `--group-by-type`: group nodes by gateway/switch/AP in Mermaid subgraphs.
|
|
243
|
+
- `--legend-scale`: scale legend font/link sizes for Mermaid outputs (default 1.0).
|
|
244
|
+
- `--legend-style auto|compact|diagram`: legend rendering mode (auto uses compact for mkdocs).
|
|
177
245
|
- `--legend-only`: render just the legend as a separate Mermaid graph (Mermaid only).
|
|
178
246
|
|
|
179
247
|
SVG:
|
|
@@ -181,9 +249,10 @@ SVG:
|
|
|
181
249
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
182
250
|
|
|
183
251
|
Output:
|
|
184
|
-
- `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
|
|
252
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
185
253
|
- `--stdout`: write output to stdout.
|
|
186
254
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
255
|
+
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
187
256
|
|
|
188
257
|
Debug:
|
|
189
258
|
- `--debug-dump`: dump gateway + sample devices to stderr for debugging.
|
|
@@ -228,5 +297,10 @@ svg:
|
|
|
228
297
|
to: "#b6dcff"
|
|
229
298
|
```
|
|
230
299
|
|
|
300
|
+
## MkDocs Material example
|
|
301
|
+
|
|
302
|
+
See `examples/mkdocs/` for a ready-to-use setup that renders Mermaid diagrams
|
|
303
|
+
with Material for MkDocs, including a sample `unifi-network` page and legend.
|
|
304
|
+
|
|
231
305
|
The built-in themes live at `src/unifi_network_maps/assets/themes/default.yaml` and
|
|
232
306
|
`src/unifi_network_maps/assets/themes/dark.yaml`.
|
|
@@ -4,13 +4,14 @@ Dynamic UniFi -> Mermaid network maps generated from LLDP topology.
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
- Python >= 3.
|
|
7
|
+
- Python >= 3.13
|
|
8
8
|
- Virtualenv required
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
python -m venv .venv
|
|
12
12
|
source .venv/bin/activate
|
|
13
|
-
pip install -
|
|
13
|
+
pip install -r requirements-build.txt
|
|
14
|
+
pip install -e . -c constraints.txt
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
Local install (non-editable):
|
|
@@ -67,6 +68,15 @@ Isometric SVG output:
|
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
unifi-network-maps --format svg-iso --output ./network.svg
|
|
71
|
+
|
|
72
|
+
# Single-page MkDocs output (ports included, no clients)
|
|
73
|
+
unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
|
|
74
|
+
|
|
75
|
+
# MkDocs output (map + legend + gateway/switch port tables)
|
|
76
|
+
unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
|
|
77
|
+
|
|
78
|
+
# Include wired clients in the port tables
|
|
79
|
+
unifi-network-maps --format mkdocs --include-clients --output ./docs/unifi-network.md
|
|
70
80
|
```
|
|
71
81
|
|
|
72
82
|
SVG size overrides:
|
|
@@ -84,12 +94,61 @@ Legend only:
|
|
|
84
94
|
unifi-network-maps --legend-only --stdout
|
|
85
95
|
```
|
|
86
96
|
|
|
97
|
+
## Examples (mock data)
|
|
98
|
+
|
|
99
|
+
These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
|
|
100
|
+
Mock generation requires dev dependencies (`pip install -r requirements-dev.txt -c constraints.txt`).
|
|
101
|
+
Regenerate the fixture + SVG with `make mock-data`.
|
|
102
|
+
|
|
103
|
+
Generate mock data (dev-only, uses Faker):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
unifi-network-maps --generate-mock examples/mock_data.json --mock-seed 1337
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Generate the isometric SVG:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
unifi-network-maps --mock-data examples/mock_data.json \
|
|
113
|
+
--include-ports --include-clients --format svg-iso \
|
|
114
|
+
--output examples/output/network_ports_clients_iso.svg
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
Mermaid example with ports:
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
graph TB
|
|
123
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 7 (AP Attic) <-> AP Attic: Port 1 (Core Switch)"| ap_attic["AP Attic"];
|
|
124
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 3 (AP Living Room) <-> AP Living Room: Port 1 (Core Switch)"| ap_living_room["AP Living Room"];
|
|
125
|
+
cloud_gateway["Cloud Gateway"] ---|"Cloud Gateway: Port 9 (Core Switch) <-> Core Switch: Port 1 (Cloud Gateway)"| core_switch["Core Switch"];
|
|
126
|
+
class cloud_gateway node_gateway;
|
|
127
|
+
class core_switch node_switch;
|
|
128
|
+
class ap_living_room node_ap;
|
|
129
|
+
class ap_attic node_ap;
|
|
130
|
+
classDef node_gateway fill:#ffe3b3,stroke:#d98300,stroke-width:1px;
|
|
131
|
+
classDef node_switch fill:#d6ecff,stroke:#3a7bd5,stroke-width:1px;
|
|
132
|
+
classDef node_ap fill:#d7f5e7,stroke:#27ae60,stroke-width:1px;
|
|
133
|
+
classDef node_client fill:#f2e5ff,stroke:#7f3fbf,stroke-width:1px;
|
|
134
|
+
classDef node_other fill:#eeeeee,stroke:#8f8f8f,stroke-width:1px;
|
|
135
|
+
classDef node_legend font-size:10px;
|
|
136
|
+
linkStyle 0 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
137
|
+
linkStyle 1 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
138
|
+
```
|
|
139
|
+
|
|
87
140
|
## Local install check
|
|
88
141
|
|
|
89
142
|
```bash
|
|
90
143
|
pip install .
|
|
91
144
|
```
|
|
92
145
|
|
|
146
|
+
## Dev
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -r requirements-dev.txt -c constraints.txt
|
|
150
|
+
```
|
|
151
|
+
|
|
93
152
|
## Release
|
|
94
153
|
|
|
95
154
|
Build and upload to PyPI:
|
|
@@ -130,6 +189,14 @@ The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `O
|
|
|
130
189
|
Source:
|
|
131
190
|
- `--site`: override `UNIFI_SITE`.
|
|
132
191
|
- `--env-file`: load environment variables from a specific `.env` file.
|
|
192
|
+
- `--mock-data`: use mock data JSON instead of the UniFi API.
|
|
193
|
+
Mock:
|
|
194
|
+
- `--generate-mock`: write mock data JSON and exit.
|
|
195
|
+
- `--mock-seed`: seed for deterministic mock generation.
|
|
196
|
+
- `--mock-switches`: number of switches to generate.
|
|
197
|
+
- `--mock-aps`: number of access points to generate.
|
|
198
|
+
- `--mock-wired-clients`: number of wired clients to generate.
|
|
199
|
+
- `--mock-wireless-clients`: number of wireless clients to generate.
|
|
133
200
|
|
|
134
201
|
Functional:
|
|
135
202
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
@@ -140,6 +207,8 @@ Functional:
|
|
|
140
207
|
Mermaid:
|
|
141
208
|
- `--direction LR|TB`: diagram direction for Mermaid (default TB).
|
|
142
209
|
- `--group-by-type`: group nodes by gateway/switch/AP in Mermaid subgraphs.
|
|
210
|
+
- `--legend-scale`: scale legend font/link sizes for Mermaid outputs (default 1.0).
|
|
211
|
+
- `--legend-style auto|compact|diagram`: legend rendering mode (auto uses compact for mkdocs).
|
|
143
212
|
- `--legend-only`: render just the legend as a separate Mermaid graph (Mermaid only).
|
|
144
213
|
|
|
145
214
|
SVG:
|
|
@@ -147,9 +216,10 @@ SVG:
|
|
|
147
216
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
148
217
|
|
|
149
218
|
Output:
|
|
150
|
-
- `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
|
|
219
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
151
220
|
- `--stdout`: write output to stdout.
|
|
152
221
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
222
|
+
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
153
223
|
|
|
154
224
|
Debug:
|
|
155
225
|
- `--debug-dump`: dump gateway + sample devices to stderr for debugging.
|
|
@@ -194,5 +264,10 @@ svg:
|
|
|
194
264
|
to: "#b6dcff"
|
|
195
265
|
```
|
|
196
266
|
|
|
267
|
+
## MkDocs Material example
|
|
268
|
+
|
|
269
|
+
See `examples/mkdocs/` for a ready-to-use setup that renders Mermaid diagrams
|
|
270
|
+
with Material for MkDocs, including a sample `unifi-network` page and legend.
|
|
271
|
+
|
|
197
272
|
The built-in themes live at `src/unifi_network_maps/assets/themes/default.yaml` and
|
|
198
273
|
`src/unifi_network_maps/assets/themes/dark.yaml`.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools
|
|
2
|
+
requires = ["setuptools==80.9.0", "wheel==0.45.1"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unifi-network-maps"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.1"
|
|
8
8
|
description = "Dynamic UniFi -> network maps in mermaid or svg"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{ name = "Merlijn" }]
|
|
@@ -18,16 +18,14 @@ classifiers = [
|
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
-
"Programming Language :: Python :: 3.
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
|
-
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
24
22
|
"Topic :: Documentation",
|
|
25
23
|
"Topic :: System :: Networking",
|
|
26
24
|
]
|
|
27
25
|
dependencies = [
|
|
28
|
-
"unifi-controller-api",
|
|
29
|
-
"python-dotenv",
|
|
30
|
-
"PyYAML",
|
|
26
|
+
"unifi-controller-api==0.3.2",
|
|
27
|
+
"python-dotenv==1.2.1",
|
|
28
|
+
"PyYAML==6.0.3",
|
|
31
29
|
]
|
|
32
30
|
|
|
33
31
|
[project.urls]
|
|
@@ -38,10 +36,11 @@ Changelog = "https://github.com/merlijntishauser/unifi-network-maps/blob/main/CH
|
|
|
38
36
|
|
|
39
37
|
[project.optional-dependencies]
|
|
40
38
|
dev = [
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"pytest
|
|
44
|
-
"
|
|
39
|
+
"Faker==40.1.0",
|
|
40
|
+
"pre-commit==4.5.1",
|
|
41
|
+
"pytest==9.0.2",
|
|
42
|
+
"pytest-cov==7.0.0",
|
|
43
|
+
"ruff==0.14.10",
|
|
45
44
|
]
|
|
46
45
|
|
|
47
46
|
[project.scripts]
|
|
@@ -49,7 +48,7 @@ unifi-network-maps = "unifi_network_maps.cli:main"
|
|
|
49
48
|
|
|
50
49
|
[tool.ruff]
|
|
51
50
|
line-length = 100
|
|
52
|
-
target-version = "
|
|
51
|
+
target-version = "py313"
|
|
53
52
|
|
|
54
53
|
[tool.ruff.lint]
|
|
55
54
|
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -61,13 +60,17 @@ line-ending = "lf"
|
|
|
61
60
|
[tool.pytest.ini_options]
|
|
62
61
|
testpaths = ["tests"]
|
|
63
62
|
norecursedirs = ["src/unifi_network_maps/assets"]
|
|
63
|
+
# Remove --cov from here so it doesn't conflict with PyCharm's runner
|
|
64
|
+
addopts = "-ra"
|
|
64
65
|
|
|
65
66
|
[tool.coverage.run]
|
|
66
67
|
branch = true
|
|
67
68
|
source = ["unifi_network_maps"]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
|
|
70
|
+
[tool.coverage.paths]
|
|
71
|
+
source = [
|
|
72
|
+
"src/unifi_network_maps",
|
|
73
|
+
"*/site-packages/unifi_network_maps",
|
|
71
74
|
]
|
|
72
75
|
|
|
73
76
|
[tool.coverage.report]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.1"
|
{unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/unifi.py
RENAMED
|
@@ -39,30 +39,83 @@ def _cache_key(*parts: str) -> str:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def _load_cache(path: Path, ttl_seconds: int) -> object | None:
|
|
42
|
-
|
|
42
|
+
data, age = _load_cache_with_age(path)
|
|
43
|
+
if data is None:
|
|
43
44
|
return None
|
|
45
|
+
if ttl_seconds <= 0:
|
|
46
|
+
return None
|
|
47
|
+
if age is None or age > ttl_seconds:
|
|
48
|
+
return None
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_cache_with_age(path: Path) -> tuple[object | None, float | None]:
|
|
53
|
+
if not path.exists():
|
|
54
|
+
return None, None
|
|
44
55
|
try:
|
|
45
56
|
payload = pickle.loads(path.read_bytes())
|
|
46
57
|
except Exception as exc:
|
|
47
58
|
logger.debug("Failed to read cache %s: %s", path, exc)
|
|
48
|
-
return None
|
|
59
|
+
return None, None
|
|
49
60
|
timestamp = payload.get("timestamp")
|
|
50
61
|
if not isinstance(timestamp, int | float):
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
return None, None
|
|
63
|
+
data = payload.get("data")
|
|
64
|
+
if not isinstance(data, list):
|
|
65
|
+
logger.debug("Cached payload at %s is not a list", path)
|
|
66
|
+
return None, None
|
|
67
|
+
return data, time.time() - timestamp
|
|
55
68
|
|
|
56
69
|
|
|
57
70
|
def _save_cache(path: Path, data: object) -> None:
|
|
58
71
|
try:
|
|
59
72
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
73
|
payload = {"timestamp": time.time(), "data": data}
|
|
61
|
-
path.
|
|
74
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
75
|
+
tmp_path.write_bytes(pickle.dumps(payload))
|
|
76
|
+
tmp_path.replace(path)
|
|
62
77
|
except Exception as exc:
|
|
63
78
|
logger.debug("Failed to write cache %s: %s", path, exc)
|
|
64
79
|
|
|
65
80
|
|
|
81
|
+
def _retry_attempts() -> int:
|
|
82
|
+
value = os.environ.get("UNIFI_RETRY_ATTEMPTS", "").strip()
|
|
83
|
+
if not value:
|
|
84
|
+
return 2
|
|
85
|
+
if value.isdigit():
|
|
86
|
+
return max(1, int(value))
|
|
87
|
+
logger.warning("Invalid UNIFI_RETRY_ATTEMPTS value: %s", value)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _retry_backoff_seconds() -> float:
|
|
92
|
+
value = os.environ.get("UNIFI_RETRY_BACKOFF_SECONDS", "").strip()
|
|
93
|
+
if not value:
|
|
94
|
+
return 0.5
|
|
95
|
+
try:
|
|
96
|
+
return max(0.0, float(value))
|
|
97
|
+
except ValueError:
|
|
98
|
+
logger.warning("Invalid UNIFI_RETRY_BACKOFF_SECONDS value: %s", value)
|
|
99
|
+
return 0.5
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _call_with_retries(operation: str, func) -> object:
|
|
103
|
+
attempts = _retry_attempts()
|
|
104
|
+
backoff = _retry_backoff_seconds()
|
|
105
|
+
last_exc: Exception | None = None
|
|
106
|
+
for attempt in range(1, attempts + 1):
|
|
107
|
+
try:
|
|
108
|
+
return func()
|
|
109
|
+
except Exception as exc: # noqa: BLE001 - surface full error after retries
|
|
110
|
+
last_exc = exc
|
|
111
|
+
logger.warning("Failed %s attempt %d/%d: %s", operation, attempt, attempts, exc)
|
|
112
|
+
if attempt < attempts and backoff > 0:
|
|
113
|
+
time.sleep(backoff * attempt)
|
|
114
|
+
if last_exc:
|
|
115
|
+
raise last_exc
|
|
116
|
+
raise RuntimeError(f"Failed {operation}")
|
|
117
|
+
|
|
118
|
+
|
|
66
119
|
def _init_controller(config: Config, *, is_udm_pro: bool) -> UnifiController:
|
|
67
120
|
from unifi_controller_api import UnifiController
|
|
68
121
|
|
|
@@ -91,6 +144,7 @@ def fetch_devices(
|
|
|
91
144
|
ttl_seconds = _cache_ttl_seconds()
|
|
92
145
|
cache_path = _cache_dir() / f"devices_{_cache_key(config.url, site_name, str(detailed))}.pkl"
|
|
93
146
|
cached = _load_cache(cache_path, ttl_seconds)
|
|
147
|
+
stale_cached, cache_age = _load_cache_with_age(cache_path)
|
|
94
148
|
if cached is not None:
|
|
95
149
|
logger.info("Using cached devices (%d)", len(cached))
|
|
96
150
|
return cached
|
|
@@ -101,7 +155,20 @@ def fetch_devices(
|
|
|
101
155
|
logger.info("UDM Pro authentication failed, retrying legacy auth")
|
|
102
156
|
controller = _init_controller(config, is_udm_pro=False)
|
|
103
157
|
|
|
104
|
-
|
|
158
|
+
def _fetch() -> list[object]:
|
|
159
|
+
return controller.get_unifi_site_device(site_name=site_name, detailed=detailed, raw=False)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
devices = _call_with_retries("device fetch", _fetch)
|
|
163
|
+
except Exception as exc: # noqa: BLE001 - fallback to cache
|
|
164
|
+
if stale_cached is not None:
|
|
165
|
+
logger.warning(
|
|
166
|
+
"Device fetch failed; using stale cache (%ds old): %s",
|
|
167
|
+
int(cache_age or 0),
|
|
168
|
+
exc,
|
|
169
|
+
)
|
|
170
|
+
return stale_cached
|
|
171
|
+
raise
|
|
105
172
|
_save_cache(cache_path, devices)
|
|
106
173
|
logger.info("Fetched %d devices", len(devices))
|
|
107
174
|
return devices
|
|
@@ -118,6 +185,7 @@ def fetch_clients(config: Config, *, site: str | None = None) -> Iterable[object
|
|
|
118
185
|
ttl_seconds = _cache_ttl_seconds()
|
|
119
186
|
cache_path = _cache_dir() / f"clients_{_cache_key(config.url, site_name)}.pkl"
|
|
120
187
|
cached = _load_cache(cache_path, ttl_seconds)
|
|
188
|
+
stale_cached, cache_age = _load_cache_with_age(cache_path)
|
|
121
189
|
if cached is not None:
|
|
122
190
|
logger.info("Using cached clients (%d)", len(cached))
|
|
123
191
|
return cached
|
|
@@ -128,7 +196,20 @@ def fetch_clients(config: Config, *, site: str | None = None) -> Iterable[object
|
|
|
128
196
|
logger.info("UDM Pro authentication failed, retrying legacy auth")
|
|
129
197
|
controller = _init_controller(config, is_udm_pro=False)
|
|
130
198
|
|
|
131
|
-
|
|
199
|
+
def _fetch() -> list[object]:
|
|
200
|
+
return controller.get_unifi_site_client(site_name=site_name, raw=True)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
clients = _call_with_retries("client fetch", _fetch)
|
|
204
|
+
except Exception as exc: # noqa: BLE001 - fallback to cache
|
|
205
|
+
if stale_cached is not None:
|
|
206
|
+
logger.warning(
|
|
207
|
+
"Client fetch failed; using stale cache (%ds old): %s",
|
|
208
|
+
int(cache_age or 0),
|
|
209
|
+
exc,
|
|
210
|
+
)
|
|
211
|
+
return stale_cached
|
|
212
|
+
raise
|
|
132
213
|
_save_cache(cache_path, clients)
|
|
133
214
|
logger.info("Fetched %d clients", len(clients))
|
|
134
215
|
return clients
|