unifi-network-maps 1.3.0__tar.gz → 1.3.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.0 → unifi_network_maps-1.3.1}/PKG-INFO +22 -5
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/README.md +20 -1
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/pyproject.toml +3 -3
- unifi_network_maps-1.3.1/src/unifi_network_maps/__init__.py +1 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/cli/main.py +43 -7
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/topology.py +30 -4
- unifi_network_maps-1.3.1/src/unifi_network_maps/render/lldp_md.py +254 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/mermaid.py +5 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/svg.py +18 -6
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/PKG-INFO +22 -5
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/SOURCES.txt +2 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_cli.py +40 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_clients.py +7 -0
- unifi_network_maps-1.3.1/tests/test_lldp_md.py +42 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_mermaid.py +5 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_svg.py +8 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_svg_iso.py +8 -0
- unifi_network_maps-1.3.0/src/unifi_network_maps/__init__.py +0 -1
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/CHANGELOG.md +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/CONTRIBUTING.md +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/LICENSE +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/LICENSES.md +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/MANIFEST.in +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/RELEASING.md +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/SECURITY.md +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/setup.cfg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/adapters/config.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/adapters/unifi.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/io/export.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/lldp.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/theme.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/requires.txt +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_config.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_debug.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_export.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_groups.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_theme.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_topology.py +0 -0
- {unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/tests/test_unifi.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/merlijntishauser/unifi-network-maps
|
|
8
8
|
Project-URL: Repository, https://github.com/merlijntishauser/unifi-network-maps
|
|
9
9
|
Project-URL: Issues, https://github.com/merlijntishauser/unifi-network-maps/issues
|
|
@@ -11,7 +11,6 @@ Project-URL: Changelog, https://github.com/merlijntishauser/unifi-network-maps/b
|
|
|
11
11
|
Keywords: unifi,mermaid,network,topology,diagram,svg
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: System Administrators
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -23,7 +22,6 @@ Classifier: Topic :: System :: Networking
|
|
|
23
22
|
Requires-Python: >=3.10
|
|
24
23
|
Description-Content-Type: text/markdown
|
|
25
24
|
License-File: LICENSE
|
|
26
|
-
License-File: LICENSES.md
|
|
27
25
|
Requires-Dist: unifi-controller-api
|
|
28
26
|
Requires-Dist: python-dotenv
|
|
29
27
|
Requires-Dist: PyYAML
|
|
@@ -109,6 +107,9 @@ SVG size overrides:
|
|
|
109
107
|
|
|
110
108
|
```bash
|
|
111
109
|
unifi-network-maps --format svg --svg-width 1400 --svg-height 900 --output ./network.svg
|
|
110
|
+
|
|
111
|
+
# LLDP tables for troubleshooting
|
|
112
|
+
unifi-network-maps --format lldp-md --output ./lldp.md
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
Legend only:
|
|
@@ -142,6 +143,20 @@ git push origin vX.Y.Z
|
|
|
142
143
|
|
|
143
144
|
See `LICENSES.md` for third-party license info.
|
|
144
145
|
|
|
146
|
+
## Installation
|
|
147
|
+
|
|
148
|
+
PyPI: https://pypi.org/project/unifi-network-maps/
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install unifi-network-maps
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then run:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
unifi-network-maps --help
|
|
158
|
+
```
|
|
159
|
+
|
|
145
160
|
## Options
|
|
146
161
|
|
|
147
162
|
The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `Output`, `Debug`).
|
|
@@ -153,6 +168,7 @@ Source:
|
|
|
153
168
|
Functional:
|
|
154
169
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
155
170
|
- `--include-clients`: add active wired clients as leaf nodes.
|
|
171
|
+
- `--client-scope wired|wireless|all`: which client types to include (default wired).
|
|
156
172
|
- `--only-unifi`: only include neighbors that are UniFi devices.
|
|
157
173
|
|
|
158
174
|
Mermaid:
|
|
@@ -165,7 +181,7 @@ SVG:
|
|
|
165
181
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
166
182
|
|
|
167
183
|
Output:
|
|
168
|
-
- `--format mermaid|svg|svg-iso`: output format (default mermaid).
|
|
184
|
+
- `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
|
|
169
185
|
- `--stdout`: write output to stdout.
|
|
170
186
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
171
187
|
|
|
@@ -178,6 +194,7 @@ Debug:
|
|
|
178
194
|
- Default output is top-to-bottom (TB) and rendered as a hop-based tree from the gateway(s).
|
|
179
195
|
- Nodes are color-coded by type (gateway/switch/AP/client) with a sensible default palette.
|
|
180
196
|
- PoE links are highlighted in blue and annotated with a power icon when detected from `port_table`.
|
|
197
|
+
- Wireless client links render as dashed lines to indicate the last-known upstream.
|
|
181
198
|
- SVG output uses vendored device glyphs from `src/unifi_network_maps/assets/icons`.
|
|
182
199
|
- Isometric SVG output uses MIT-licensed icons from `markmanx/isopacks`.
|
|
183
200
|
- SVG port labels render inside child nodes for readability.
|
|
@@ -73,6 +73,9 @@ SVG size overrides:
|
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
unifi-network-maps --format svg --svg-width 1400 --svg-height 900 --output ./network.svg
|
|
76
|
+
|
|
77
|
+
# LLDP tables for troubleshooting
|
|
78
|
+
unifi-network-maps --format lldp-md --output ./lldp.md
|
|
76
79
|
```
|
|
77
80
|
|
|
78
81
|
Legend only:
|
|
@@ -106,6 +109,20 @@ git push origin vX.Y.Z
|
|
|
106
109
|
|
|
107
110
|
See `LICENSES.md` for third-party license info.
|
|
108
111
|
|
|
112
|
+
## Installation
|
|
113
|
+
|
|
114
|
+
PyPI: https://pypi.org/project/unifi-network-maps/
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install unifi-network-maps
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
unifi-network-maps --help
|
|
124
|
+
```
|
|
125
|
+
|
|
109
126
|
## Options
|
|
110
127
|
|
|
111
128
|
The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `Output`, `Debug`).
|
|
@@ -117,6 +134,7 @@ Source:
|
|
|
117
134
|
Functional:
|
|
118
135
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
119
136
|
- `--include-clients`: add active wired clients as leaf nodes.
|
|
137
|
+
- `--client-scope wired|wireless|all`: which client types to include (default wired).
|
|
120
138
|
- `--only-unifi`: only include neighbors that are UniFi devices.
|
|
121
139
|
|
|
122
140
|
Mermaid:
|
|
@@ -129,7 +147,7 @@ SVG:
|
|
|
129
147
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
130
148
|
|
|
131
149
|
Output:
|
|
132
|
-
- `--format mermaid|svg|svg-iso`: output format (default mermaid).
|
|
150
|
+
- `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
|
|
133
151
|
- `--stdout`: write output to stdout.
|
|
134
152
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
135
153
|
|
|
@@ -142,6 +160,7 @@ Debug:
|
|
|
142
160
|
- Default output is top-to-bottom (TB) and rendered as a hop-based tree from the gateway(s).
|
|
143
161
|
- Nodes are color-coded by type (gateway/switch/AP/client) with a sensible default palette.
|
|
144
162
|
- PoE links are highlighted in blue and annotated with a power icon when detected from `port_table`.
|
|
163
|
+
- Wireless client links render as dashed lines to indicate the last-known upstream.
|
|
145
164
|
- SVG output uses vendored device glyphs from `src/unifi_network_maps/assets/icons`.
|
|
146
165
|
- Isometric SVG output uses MIT-licensed icons from `markmanx/isopacks`.
|
|
147
166
|
- SVG port labels render inside child nodes for readability.
|
|
@@ -4,17 +4,17 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unifi-network-maps"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.1"
|
|
8
8
|
description = "Dynamic UniFi -> network maps in mermaid or svg"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
license =
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
12
13
|
authors = [{ name = "Merlijn" }]
|
|
13
14
|
keywords = ["unifi", "mermaid", "network", "topology", "diagram", "svg"]
|
|
14
15
|
classifiers = [
|
|
15
16
|
"Development Status :: 3 - Alpha",
|
|
16
17
|
"Intended Audience :: System Administrators",
|
|
17
|
-
"License :: OSI Approved :: MIT License",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.1"
|
|
@@ -18,6 +18,7 @@ from ..model.topology import (
|
|
|
18
18
|
group_devices_by_type,
|
|
19
19
|
normalize_devices,
|
|
20
20
|
)
|
|
21
|
+
from ..render.lldp_md import render_lldp_md
|
|
21
22
|
from ..render.mermaid import render_legend, render_mermaid
|
|
22
23
|
from ..render.mermaid_theme import MermaidTheme
|
|
23
24
|
from ..render.svg import SvgOptions, render_svg
|
|
@@ -65,6 +66,12 @@ def _add_functional_args(parser: argparse._ArgumentGroup) -> None:
|
|
|
65
66
|
action="store_true",
|
|
66
67
|
help="Include active clients as leaf nodes",
|
|
67
68
|
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--client-scope",
|
|
71
|
+
choices=["wired", "wireless", "all"],
|
|
72
|
+
default="wired",
|
|
73
|
+
help="Client types to include (default: wired)",
|
|
74
|
+
)
|
|
68
75
|
parser.add_argument(
|
|
69
76
|
"--only-unifi", action="store_true", help="Only include neighbors that are UniFi devices"
|
|
70
77
|
)
|
|
@@ -94,7 +101,7 @@ def _add_general_render_args(parser: argparse._ArgumentGroup) -> None:
|
|
|
94
101
|
parser.add_argument(
|
|
95
102
|
"--format",
|
|
96
103
|
default="mermaid",
|
|
97
|
-
choices=["mermaid", "svg", "svg-iso"],
|
|
104
|
+
choices=["mermaid", "svg", "svg-iso", "lldp-md"],
|
|
98
105
|
help="Output format",
|
|
99
106
|
)
|
|
100
107
|
parser.add_argument(
|
|
@@ -147,13 +154,20 @@ def _render_legend_only(args: argparse.Namespace, mermaid_theme: MermaidTheme) -
|
|
|
147
154
|
return content
|
|
148
155
|
|
|
149
156
|
|
|
150
|
-
def
|
|
157
|
+
def _load_devices_data(
|
|
151
158
|
args: argparse.Namespace, config: Config, site: str
|
|
152
|
-
) -> tuple[list[
|
|
159
|
+
) -> tuple[list[object], list[Device]]:
|
|
153
160
|
raw_devices = list(fetch_devices(config, site=site, detailed=True))
|
|
154
161
|
devices = normalize_devices(raw_devices)
|
|
155
162
|
if args.debug_dump:
|
|
156
163
|
debug_dump_devices(raw_devices, devices, sample_count=max(0, args.debug_sample))
|
|
164
|
+
return raw_devices, devices
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_topology_data(
|
|
168
|
+
args: argparse.Namespace, config: Config, site: str
|
|
169
|
+
) -> tuple[list[Device], list[str], object]:
|
|
170
|
+
_raw_devices, devices = _load_devices_data(args, config, site)
|
|
157
171
|
groups_for_rank = group_devices_by_type(devices)
|
|
158
172
|
gateways = groups_for_rank.get("gateway", [])
|
|
159
173
|
topology = build_topology(
|
|
@@ -176,7 +190,12 @@ def _build_edges_with_clients(
|
|
|
176
190
|
if args.include_clients:
|
|
177
191
|
clients = list(fetch_clients(config, site=site))
|
|
178
192
|
device_index = build_device_index(devices)
|
|
179
|
-
edges = edges + build_client_edges(
|
|
193
|
+
edges = edges + build_client_edges(
|
|
194
|
+
clients,
|
|
195
|
+
device_index,
|
|
196
|
+
include_ports=args.include_ports,
|
|
197
|
+
client_mode=args.client_scope,
|
|
198
|
+
)
|
|
180
199
|
return edges, clients
|
|
181
200
|
|
|
182
201
|
|
|
@@ -207,7 +226,7 @@ def _render_mermaid_output(
|
|
|
207
226
|
direction=args.direction,
|
|
208
227
|
groups=groups,
|
|
209
228
|
group_order=group_order,
|
|
210
|
-
node_types=build_node_type_map(devices, clients),
|
|
229
|
+
node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
|
|
211
230
|
theme=mermaid_theme,
|
|
212
231
|
)
|
|
213
232
|
if args.markdown:
|
|
@@ -233,13 +252,13 @@ def _render_svg_output(
|
|
|
233
252
|
|
|
234
253
|
return render_svg_isometric(
|
|
235
254
|
edges,
|
|
236
|
-
node_types=build_node_type_map(devices, clients),
|
|
255
|
+
node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
|
|
237
256
|
options=options,
|
|
238
257
|
theme=svg_theme,
|
|
239
258
|
)
|
|
240
259
|
return render_svg(
|
|
241
260
|
edges,
|
|
242
|
-
node_types=build_node_type_map(devices, clients),
|
|
261
|
+
node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
|
|
243
262
|
options=options,
|
|
244
263
|
theme=svg_theme,
|
|
245
264
|
)
|
|
@@ -259,6 +278,23 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
259
278
|
write_output(content, output_path=args.output, stdout=args.stdout)
|
|
260
279
|
return 0
|
|
261
280
|
|
|
281
|
+
if args.format == "lldp-md":
|
|
282
|
+
try:
|
|
283
|
+
_raw_devices, devices = _load_devices_data(args, config, site)
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
logging.error("Failed to load devices: %s", exc)
|
|
286
|
+
return 1
|
|
287
|
+
clients = list(fetch_clients(config, site=site))
|
|
288
|
+
content = render_lldp_md(
|
|
289
|
+
devices,
|
|
290
|
+
clients=clients,
|
|
291
|
+
include_ports=args.include_ports,
|
|
292
|
+
show_clients=args.include_clients,
|
|
293
|
+
client_mode=args.client_scope,
|
|
294
|
+
)
|
|
295
|
+
write_output(content, output_path=args.output, stdout=args.stdout)
|
|
296
|
+
return 0
|
|
297
|
+
|
|
262
298
|
try:
|
|
263
299
|
devices, _gateways, topology = _build_topology_data(args, config, site)
|
|
264
300
|
except Exception as exc:
|
{unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/model/topology.py
RENAMED
|
@@ -27,6 +27,7 @@ class Device:
|
|
|
27
27
|
poe_ports: dict[int, bool] = field(default_factory=dict)
|
|
28
28
|
uplink: UplinkInfo | None = None
|
|
29
29
|
last_uplink: UplinkInfo | None = None
|
|
30
|
+
version: str = ""
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass(frozen=True)
|
|
@@ -35,6 +36,7 @@ class Edge:
|
|
|
35
36
|
right: str
|
|
36
37
|
label: str | None = None
|
|
37
38
|
poe: bool = False
|
|
39
|
+
wireless: bool = False
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class DeviceLike(Protocol):
|
|
@@ -56,6 +58,8 @@ class DeviceLike(Protocol):
|
|
|
56
58
|
last_uplink_mac: object | None
|
|
57
59
|
uplink_device_name: object | None
|
|
58
60
|
uplink_remote_port: object | None
|
|
61
|
+
version: object | None
|
|
62
|
+
displayable_version: object | None
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
@dataclass(frozen=True)
|
|
@@ -242,6 +246,7 @@ def coerce_device(device: DeviceLike) -> Device:
|
|
|
242
246
|
mac = _get_attr(device, "mac")
|
|
243
247
|
ip = _get_attr(device, "ip") or _get_attr(device, "ip_address")
|
|
244
248
|
dev_type = _get_attr(device, "type") or _get_attr(device, "device_type")
|
|
249
|
+
version = _get_attr(device, "displayable_version") or _get_attr(device, "version")
|
|
245
250
|
lldp_info = _get_attr(device, "lldp_info")
|
|
246
251
|
if lldp_info is None:
|
|
247
252
|
lldp_info = _get_attr(device, "lldp")
|
|
@@ -271,6 +276,7 @@ def coerce_device(device: DeviceLike) -> Device:
|
|
|
271
276
|
poe_ports=poe_ports,
|
|
272
277
|
uplink=uplink,
|
|
273
278
|
last_uplink=last_uplink,
|
|
279
|
+
version=str(version or ""),
|
|
274
280
|
)
|
|
275
281
|
|
|
276
282
|
|
|
@@ -407,16 +413,26 @@ def _client_is_wired(client: object) -> bool:
|
|
|
407
413
|
return bool(_client_field(client, "is_wired"))
|
|
408
414
|
|
|
409
415
|
|
|
416
|
+
def _client_matches_mode(client: object, mode: str) -> bool:
|
|
417
|
+
wired = _client_is_wired(client)
|
|
418
|
+
if mode == "all":
|
|
419
|
+
return True
|
|
420
|
+
if mode == "wireless":
|
|
421
|
+
return not wired
|
|
422
|
+
return wired
|
|
423
|
+
|
|
424
|
+
|
|
410
425
|
def build_client_edges(
|
|
411
426
|
clients: Iterable[object],
|
|
412
427
|
device_index: dict[str, str],
|
|
413
428
|
*,
|
|
414
429
|
include_ports: bool = False,
|
|
430
|
+
client_mode: str = "wired",
|
|
415
431
|
) -> list[Edge]:
|
|
416
432
|
edges: list[Edge] = []
|
|
417
433
|
seen: set[tuple[str, str]] = set()
|
|
418
434
|
for client in clients:
|
|
419
|
-
if not
|
|
435
|
+
if not _client_matches_mode(client, client_mode):
|
|
420
436
|
continue
|
|
421
437
|
name = _client_display_name(client)
|
|
422
438
|
uplink_mac = _client_uplink_mac(client)
|
|
@@ -433,20 +449,30 @@ def build_client_edges(
|
|
|
433
449
|
key = (device_name, name)
|
|
434
450
|
if key in seen:
|
|
435
451
|
continue
|
|
436
|
-
edges.append(
|
|
452
|
+
edges.append(
|
|
453
|
+
Edge(
|
|
454
|
+
left=device_name,
|
|
455
|
+
right=name,
|
|
456
|
+
label=label,
|
|
457
|
+
wireless=not _client_is_wired(client),
|
|
458
|
+
)
|
|
459
|
+
)
|
|
437
460
|
seen.add(key)
|
|
438
461
|
return edges
|
|
439
462
|
|
|
440
463
|
|
|
441
464
|
def build_node_type_map(
|
|
442
|
-
devices: Iterable[Device],
|
|
465
|
+
devices: Iterable[Device],
|
|
466
|
+
clients: Iterable[object] | None = None,
|
|
467
|
+
*,
|
|
468
|
+
client_mode: str = "wired",
|
|
443
469
|
) -> dict[str, str]:
|
|
444
470
|
node_types: dict[str, str] = {}
|
|
445
471
|
for device in devices:
|
|
446
472
|
node_types[device.name] = classify_device_type(device)
|
|
447
473
|
if clients:
|
|
448
474
|
for client in clients:
|
|
449
|
-
if not
|
|
475
|
+
if not _client_matches_mode(client, client_mode):
|
|
450
476
|
continue
|
|
451
477
|
name = _client_display_name(client)
|
|
452
478
|
if name:
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Render LLDP data as Markdown tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from ..model.lldp import LLDPEntry, local_port_label
|
|
8
|
+
from ..model.topology import Device, build_device_index
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalize_mac(value: str) -> str:
|
|
12
|
+
return value.strip().lower()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client_field(client: object, name: str) -> object | None:
|
|
16
|
+
if isinstance(client, dict):
|
|
17
|
+
return client.get(name)
|
|
18
|
+
return getattr(client, name, None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _client_display_name(client: object) -> str | None:
|
|
22
|
+
for key in ("name", "hostname", "mac"):
|
|
23
|
+
value = _client_field(client, key)
|
|
24
|
+
if isinstance(value, str) and value.strip():
|
|
25
|
+
return value.strip()
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _client_uplink_mac(client: object) -> str | None:
|
|
30
|
+
for key in ("ap_mac", "sw_mac", "uplink_mac", "uplink_device_mac", "last_uplink_mac"):
|
|
31
|
+
value = _client_field(client, key)
|
|
32
|
+
if isinstance(value, str) and value.strip():
|
|
33
|
+
return value.strip()
|
|
34
|
+
for key in ("uplink", "last_uplink"):
|
|
35
|
+
nested = _client_field(client, key)
|
|
36
|
+
if isinstance(nested, dict):
|
|
37
|
+
value = nested.get("uplink_mac") or nested.get("uplink_device_mac")
|
|
38
|
+
if isinstance(value, str) and value.strip():
|
|
39
|
+
return value.strip()
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _client_uplink_port(client: object) -> int | None:
|
|
44
|
+
for key in ("uplink_remote_port", "sw_port", "ap_port"):
|
|
45
|
+
value = _client_field(client, key)
|
|
46
|
+
if isinstance(value, int):
|
|
47
|
+
return value
|
|
48
|
+
if isinstance(value, str) and value.isdigit():
|
|
49
|
+
return int(value)
|
|
50
|
+
for key in ("uplink", "last_uplink"):
|
|
51
|
+
nested = _client_field(client, key)
|
|
52
|
+
if isinstance(nested, dict):
|
|
53
|
+
value = nested.get("uplink_remote_port")
|
|
54
|
+
if isinstance(value, int):
|
|
55
|
+
return value
|
|
56
|
+
if isinstance(value, str) and value.isdigit():
|
|
57
|
+
return int(value)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _client_is_wired(client: object) -> bool:
|
|
62
|
+
return bool(_client_field(client, "is_wired"))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _client_matches_mode(client: object, mode: str) -> bool:
|
|
66
|
+
wired = _client_is_wired(client)
|
|
67
|
+
if mode == "all":
|
|
68
|
+
return True
|
|
69
|
+
if mode == "wireless":
|
|
70
|
+
return not wired
|
|
71
|
+
return wired
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _lldp_sort_key(entry: LLDPEntry) -> tuple[int, str, str]:
|
|
75
|
+
port_label = local_port_label(entry) or ""
|
|
76
|
+
port_number = "".join(ch for ch in port_label if ch.isdigit())
|
|
77
|
+
return (int(port_number or 0), port_label, entry.port_id)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _device_header_lines(device: Device) -> list[str]:
|
|
81
|
+
lines = [f"## {device.name}"]
|
|
82
|
+
meta = []
|
|
83
|
+
if device.model_name:
|
|
84
|
+
meta.append(f"Model: {device.model_name}")
|
|
85
|
+
if device.ip:
|
|
86
|
+
meta.append(f"IP: {device.ip}")
|
|
87
|
+
if device.mac:
|
|
88
|
+
meta.append(f"MAC: {device.mac}")
|
|
89
|
+
if meta:
|
|
90
|
+
lines.append(f"*{' | '.join(meta)}*")
|
|
91
|
+
return lines
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _port_summary(device: Device) -> str:
|
|
95
|
+
ports = [port for port in device.port_table if port.port_idx is not None]
|
|
96
|
+
if not ports:
|
|
97
|
+
return "-"
|
|
98
|
+
total_ports = len(ports)
|
|
99
|
+
poe_capable = sum(1 for port in ports if port.port_poe or port.poe_enable)
|
|
100
|
+
poe_active = sum(1 for port in ports if device.poe_ports.get(port.port_idx or -1))
|
|
101
|
+
total_power = sum(port.poe_power or 0.0 for port in ports)
|
|
102
|
+
summary = f"Total {total_ports}, PoE {poe_capable} (active {poe_active})"
|
|
103
|
+
if total_power > 0:
|
|
104
|
+
summary = f"{summary}, {total_power:.2f}W"
|
|
105
|
+
return summary
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _uplink_summary(device: Device) -> str:
|
|
109
|
+
uplink = device.uplink or device.last_uplink
|
|
110
|
+
if not uplink:
|
|
111
|
+
return "-"
|
|
112
|
+
name = uplink.name or uplink.mac or "Unknown"
|
|
113
|
+
if uplink.port is not None:
|
|
114
|
+
return f"{name} (Port {uplink.port})"
|
|
115
|
+
return name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _client_summary(
|
|
119
|
+
device: Device, client_rows: dict[str, list[tuple[str, str | None]]]
|
|
120
|
+
) -> tuple[str, str]:
|
|
121
|
+
rows = client_rows.get(device.name)
|
|
122
|
+
if rows is None:
|
|
123
|
+
return "-", "-"
|
|
124
|
+
count = len(rows)
|
|
125
|
+
names = [name for name, _port in rows]
|
|
126
|
+
sample = ", ".join(names[:3])
|
|
127
|
+
if len(names) > 3:
|
|
128
|
+
sample = f"{sample}, ..."
|
|
129
|
+
return str(count), sample or "-"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _details_table_lines(
|
|
133
|
+
device: Device,
|
|
134
|
+
client_rows: dict[str, list[tuple[str, str | None]]],
|
|
135
|
+
client_mode: str,
|
|
136
|
+
) -> list[str]:
|
|
137
|
+
wired_count, client_sample = _client_summary(device, client_rows)
|
|
138
|
+
client_label = f"Clients ({client_mode})"
|
|
139
|
+
lines = [
|
|
140
|
+
"### Details",
|
|
141
|
+
"",
|
|
142
|
+
"| Field | Value |",
|
|
143
|
+
"| --- | --- |",
|
|
144
|
+
f"| Firmware | {_escape_cell(device.version or '-')} |",
|
|
145
|
+
f"| Uplink | {_escape_cell(_uplink_summary(device))} |",
|
|
146
|
+
f"| Ports | {_escape_cell(_port_summary(device))} |",
|
|
147
|
+
f"| {client_label} | {_escape_cell(wired_count)} |",
|
|
148
|
+
f"| Client examples | {_escape_cell(client_sample)} |",
|
|
149
|
+
"",
|
|
150
|
+
]
|
|
151
|
+
return lines
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _lldp_rows(
|
|
155
|
+
entries: Iterable[LLDPEntry],
|
|
156
|
+
device_index: dict[str, str],
|
|
157
|
+
) -> list[list[str]]:
|
|
158
|
+
rows: list[list[str]] = []
|
|
159
|
+
for entry in sorted(entries, key=_lldp_sort_key):
|
|
160
|
+
local_label = local_port_label(entry) or "?"
|
|
161
|
+
peer_name = device_index.get(_normalize_mac(entry.chassis_id), "")
|
|
162
|
+
peer_port = entry.port_id or "?"
|
|
163
|
+
port_desc = entry.port_desc or ""
|
|
164
|
+
rows.append(
|
|
165
|
+
[
|
|
166
|
+
local_label,
|
|
167
|
+
peer_name or "-",
|
|
168
|
+
peer_port,
|
|
169
|
+
entry.chassis_id,
|
|
170
|
+
port_desc or "-",
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
return rows
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _escape_cell(value: str) -> str:
|
|
177
|
+
return value.replace("|", "\\|")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _client_rows(
|
|
181
|
+
clients: Iterable[object],
|
|
182
|
+
device_index: dict[str, str],
|
|
183
|
+
*,
|
|
184
|
+
include_ports: bool,
|
|
185
|
+
client_mode: str,
|
|
186
|
+
) -> dict[str, list[tuple[str, str | None]]]:
|
|
187
|
+
rows_by_device: dict[str, list[tuple[str, str | None]]] = {}
|
|
188
|
+
for client in clients:
|
|
189
|
+
if not _client_matches_mode(client, client_mode):
|
|
190
|
+
continue
|
|
191
|
+
name = _client_display_name(client)
|
|
192
|
+
uplink_mac = _client_uplink_mac(client)
|
|
193
|
+
if not name or not uplink_mac:
|
|
194
|
+
continue
|
|
195
|
+
device_name = device_index.get(_normalize_mac(uplink_mac))
|
|
196
|
+
if not device_name:
|
|
197
|
+
continue
|
|
198
|
+
port_label = None
|
|
199
|
+
if include_ports:
|
|
200
|
+
uplink_port = _client_uplink_port(client)
|
|
201
|
+
if uplink_port is not None:
|
|
202
|
+
port_label = f"Port {uplink_port}"
|
|
203
|
+
rows_by_device.setdefault(device_name, []).append((name, port_label))
|
|
204
|
+
return rows_by_device
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def render_lldp_md(
|
|
208
|
+
devices: list[Device],
|
|
209
|
+
*,
|
|
210
|
+
clients: Iterable[object] | None = None,
|
|
211
|
+
include_ports: bool = False,
|
|
212
|
+
show_clients: bool = False,
|
|
213
|
+
client_mode: str = "wired",
|
|
214
|
+
) -> str:
|
|
215
|
+
device_index = build_device_index(devices)
|
|
216
|
+
client_rows = (
|
|
217
|
+
_client_rows(clients, device_index, include_ports=include_ports, client_mode=client_mode)
|
|
218
|
+
if clients
|
|
219
|
+
else {}
|
|
220
|
+
)
|
|
221
|
+
lines: list[str] = ["# LLDP Neighbors", ""]
|
|
222
|
+
for device in sorted(devices, key=lambda item: item.name.lower()):
|
|
223
|
+
lines.extend(_device_header_lines(device))
|
|
224
|
+
lines.append("")
|
|
225
|
+
lines.extend(_details_table_lines(device, client_rows, client_mode))
|
|
226
|
+
if device.lldp_info:
|
|
227
|
+
lines.append("")
|
|
228
|
+
lines.append(
|
|
229
|
+
"| Local Port | Neighbor | Neighbor Port | Chassis ID | Port Description |"
|
|
230
|
+
)
|
|
231
|
+
lines.append("| --- | --- | --- | --- | --- |")
|
|
232
|
+
for row in _lldp_rows(device.lldp_info, device_index):
|
|
233
|
+
lines.append("| " + " | ".join(_escape_cell(cell) for cell in row) + " |")
|
|
234
|
+
lines.append("")
|
|
235
|
+
else:
|
|
236
|
+
lines.append("_No LLDP neighbors._")
|
|
237
|
+
lines.append("")
|
|
238
|
+
rows = client_rows.get(device.name)
|
|
239
|
+
if rows and show_clients:
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append("### Clients")
|
|
242
|
+
if include_ports:
|
|
243
|
+
lines.append("")
|
|
244
|
+
lines.append("| Client | Port |")
|
|
245
|
+
lines.append("| --- | --- |")
|
|
246
|
+
for client_name, port_label in rows:
|
|
247
|
+
lines.append(
|
|
248
|
+
f"| {_escape_cell(client_name)} | {_escape_cell(port_label or '-')} |"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
for client_name, _port_label in rows:
|
|
252
|
+
lines.append(f"- {_escape_cell(client_name)}")
|
|
253
|
+
lines.append("")
|
|
254
|
+
return "\n".join(lines).rstrip() + "\n"
|
{unifi_network_maps-1.3.0 → unifi_network_maps-1.3.1}/src/unifi_network_maps/render/mermaid.py
RENAMED
|
@@ -71,6 +71,7 @@ def render_mermaid(
|
|
|
71
71
|
id_map = _build_id_map(edge_list, group_nodes)
|
|
72
72
|
lines = [f"graph {direction}"]
|
|
73
73
|
poe_links: list[int] = []
|
|
74
|
+
wireless_links: list[int] = []
|
|
74
75
|
link_index = 0
|
|
75
76
|
if groups:
|
|
76
77
|
ordered = group_order or list(groups.keys())
|
|
@@ -99,6 +100,8 @@ def render_mermaid(
|
|
|
99
100
|
lines.append(f" {left} --- {right};")
|
|
100
101
|
if edge.poe:
|
|
101
102
|
poe_links.append(link_index)
|
|
103
|
+
if edge.wireless:
|
|
104
|
+
wireless_links.append(link_index)
|
|
102
105
|
link_index += 1
|
|
103
106
|
if node_types:
|
|
104
107
|
class_map = {
|
|
@@ -121,6 +124,8 @@ def render_mermaid(
|
|
|
121
124
|
f"{index} stroke:{theme.poe_link},stroke-width:{theme.poe_link_width}px,"
|
|
122
125
|
f"arrowhead:{theme.poe_link_arrow};"
|
|
123
126
|
)
|
|
127
|
+
for index in wireless_links:
|
|
128
|
+
lines.append(f" linkStyle {index} stroke-dasharray: 5 4;")
|
|
124
129
|
return "\n".join(lines) + "\n"
|
|
125
130
|
|
|
126
131
|
|