unifi-network-maps 1.4.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unifi_network_maps/__init__.py +1 -0
- unifi_network_maps/__main__.py +8 -0
- unifi_network_maps/adapters/__init__.py +1 -0
- unifi_network_maps/adapters/config.py +49 -0
- unifi_network_maps/adapters/unifi.py +457 -0
- unifi_network_maps/assets/__init__.py +0 -0
- unifi_network_maps/assets/icons/__init__.py +0 -0
- unifi_network_maps/assets/icons/access-point.svg +1 -0
- unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +7 -0
- unifi_network_maps/assets/icons/isometric/block.svg +23 -0
- unifi_network_maps/assets/icons/isometric/cache.svg +48 -0
- unifi_network_maps/assets/icons/isometric/cardterminal.svg +316 -0
- unifi_network_maps/assets/icons/isometric/cloud.svg +89 -0
- unifi_network_maps/assets/icons/isometric/cronjob.svg +409 -0
- unifi_network_maps/assets/icons/isometric/cube.svg +24 -0
- unifi_network_maps/assets/icons/isometric/desktop.svg +107 -0
- unifi_network_maps/assets/icons/isometric/diamond.svg +23 -0
- unifi_network_maps/assets/icons/isometric/dns.svg +46 -0
- unifi_network_maps/assets/icons/isometric/document.svg +62 -0
- unifi_network_maps/assets/icons/isometric/firewall.svg +200 -0
- unifi_network_maps/assets/icons/isometric/function-module.svg +215 -0
- unifi_network_maps/assets/icons/isometric/image.svg +65 -0
- unifi_network_maps/assets/icons/isometric/laptop.svg +37 -0
- unifi_network_maps/assets/icons/isometric/loadbalancer.svg +65 -0
- unifi_network_maps/assets/icons/isometric/lock.svg +155 -0
- unifi_network_maps/assets/icons/isometric/mail.svg +35 -0
- unifi_network_maps/assets/icons/isometric/mailmultiple.svg +91 -0
- unifi_network_maps/assets/icons/isometric/mobiledevice.svg +66 -0
- unifi_network_maps/assets/icons/isometric/office.svg +136 -0
- unifi_network_maps/assets/icons/isometric/package-module.svg +39 -0
- unifi_network_maps/assets/icons/isometric/paymentcard.svg +92 -0
- unifi_network_maps/assets/icons/isometric/plane.svg +1 -0
- unifi_network_maps/assets/icons/isometric/printer.svg +122 -0
- unifi_network_maps/assets/icons/isometric/pyramid.svg +28 -0
- unifi_network_maps/assets/icons/isometric/queue.svg +38 -0
- unifi_network_maps/assets/icons/isometric/router.svg +39 -0
- unifi_network_maps/assets/icons/isometric/server.svg +112 -0
- unifi_network_maps/assets/icons/isometric/speech.svg +70 -0
- unifi_network_maps/assets/icons/isometric/sphere.svg +15 -0
- unifi_network_maps/assets/icons/isometric/storage.svg +92 -0
- unifi_network_maps/assets/icons/isometric/switch-module.svg +45 -0
- unifi_network_maps/assets/icons/isometric/tower.svg +50 -0
- unifi_network_maps/assets/icons/isometric/truck-2.svg +1 -0
- unifi_network_maps/assets/icons/isometric/truck.svg +1 -0
- unifi_network_maps/assets/icons/isometric/user.svg +231 -0
- unifi_network_maps/assets/icons/isometric/vm.svg +50 -0
- unifi_network_maps/assets/icons/laptop.svg +1 -0
- unifi_network_maps/assets/icons/router-network.svg +1 -0
- unifi_network_maps/assets/icons/server-network.svg +1 -0
- unifi_network_maps/assets/icons/server.svg +1 -0
- unifi_network_maps/assets/themes/dark.yaml +50 -0
- unifi_network_maps/assets/themes/default.yaml +47 -0
- unifi_network_maps/cli/__init__.py +5 -0
- unifi_network_maps/cli/__main__.py +8 -0
- unifi_network_maps/cli/args.py +166 -0
- unifi_network_maps/cli/main.py +134 -0
- unifi_network_maps/cli/render.py +255 -0
- unifi_network_maps/cli/runtime.py +157 -0
- unifi_network_maps/io/__init__.py +1 -0
- unifi_network_maps/io/debug.py +60 -0
- unifi_network_maps/io/export.py +32 -0
- unifi_network_maps/io/mkdocs_assets.py +21 -0
- unifi_network_maps/io/mock_data.py +23 -0
- unifi_network_maps/io/mock_generate.py +7 -0
- unifi_network_maps/model/__init__.py +1 -0
- unifi_network_maps/model/labels.py +35 -0
- unifi_network_maps/model/lldp.py +99 -0
- unifi_network_maps/model/mock.py +307 -0
- unifi_network_maps/model/ports.py +23 -0
- unifi_network_maps/model/topology.py +909 -0
- unifi_network_maps/render/__init__.py +1 -0
- unifi_network_maps/render/device_ports_md.py +492 -0
- unifi_network_maps/render/legend.py +30 -0
- unifi_network_maps/render/lldp_md.py +352 -0
- unifi_network_maps/render/markdown_tables.py +21 -0
- unifi_network_maps/render/mermaid.py +273 -0
- unifi_network_maps/render/mermaid_theme.py +56 -0
- unifi_network_maps/render/mkdocs.py +167 -0
- unifi_network_maps/render/svg.py +1235 -0
- unifi_network_maps/render/svg_theme.py +64 -0
- unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
- unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
- unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
- unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
- unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
- unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
- unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
- unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
- unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
- unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
- unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
- unifi_network_maps/render/templating.py +19 -0
- unifi_network_maps/render/theme.py +109 -0
- unifi_network_maps-1.4.11.dist-info/METADATA +290 -0
- unifi_network_maps-1.4.11.dist-info/RECORD +99 -0
- unifi_network_maps-1.4.11.dist-info/WHEEL +5 -0
- unifi_network_maps-1.4.11.dist-info/entry_points.txt +2 -0
- unifi_network_maps-1.4.11.dist-info/licenses/LICENSE +21 -0
- unifi_network_maps-1.4.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared SVG defs and theming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class SvgTheme:
|
|
10
|
+
link_standard: tuple[str, str]
|
|
11
|
+
link_poe: tuple[str, str]
|
|
12
|
+
node_gateway: tuple[str, str]
|
|
13
|
+
node_switch: tuple[str, str]
|
|
14
|
+
node_ap: tuple[str, str]
|
|
15
|
+
node_client: tuple[str, str]
|
|
16
|
+
node_other: tuple[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_THEME = SvgTheme(
|
|
20
|
+
link_standard=("#16a085", "#2ecc71"),
|
|
21
|
+
link_poe=("#1e88e5", "#42a5f5"),
|
|
22
|
+
node_gateway=("#ffd199", "#ffb15a"),
|
|
23
|
+
node_switch=("#bfe4ff", "#8ac6ff"),
|
|
24
|
+
node_ap=("#c4f2d4", "#8ee3b4"),
|
|
25
|
+
node_client=("#e4ccff", "#c5a4ff"),
|
|
26
|
+
node_other=("#e3e3e3", "#cfcfcf"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def svg_defs(prefix: str, theme: SvgTheme = DEFAULT_THEME) -> str:
|
|
31
|
+
gradient_prefix = f"{prefix}-" if prefix else ""
|
|
32
|
+
node_prefix = f"{prefix}-node-" if prefix else "node-"
|
|
33
|
+
return (
|
|
34
|
+
"<defs>"
|
|
35
|
+
f'<linearGradient id="{gradient_prefix}link-standard" x1="0%" y1="0%" x2="100%" y2="0%">'
|
|
36
|
+
f'<stop offset="0%" stop-color="{theme.link_standard[0]}"/>'
|
|
37
|
+
f'<stop offset="100%" stop-color="{theme.link_standard[1]}"/>'
|
|
38
|
+
"</linearGradient>"
|
|
39
|
+
f'<linearGradient id="{gradient_prefix}link-poe" x1="0%" y1="0%" x2="100%" y2="0%">'
|
|
40
|
+
f'<stop offset="0%" stop-color="{theme.link_poe[0]}"/>'
|
|
41
|
+
f'<stop offset="100%" stop-color="{theme.link_poe[1]}"/>'
|
|
42
|
+
"</linearGradient>"
|
|
43
|
+
f'<linearGradient id="{node_prefix}gateway" x1="0%" y1="0%" x2="100%" y2="100%">'
|
|
44
|
+
f'<stop offset="0%" stop-color="{theme.node_gateway[0]}"/>'
|
|
45
|
+
f'<stop offset="100%" stop-color="{theme.node_gateway[1]}"/>'
|
|
46
|
+
"</linearGradient>"
|
|
47
|
+
f'<linearGradient id="{node_prefix}switch" x1="0%" y1="0%" x2="100%" y2="100%">'
|
|
48
|
+
f'<stop offset="0%" stop-color="{theme.node_switch[0]}"/>'
|
|
49
|
+
f'<stop offset="100%" stop-color="{theme.node_switch[1]}"/>'
|
|
50
|
+
"</linearGradient>"
|
|
51
|
+
f'<linearGradient id="{node_prefix}ap" x1="0%" y1="0%" x2="100%" y2="100%">'
|
|
52
|
+
f'<stop offset="0%" stop-color="{theme.node_ap[0]}"/>'
|
|
53
|
+
f'<stop offset="100%" stop-color="{theme.node_ap[1]}"/>'
|
|
54
|
+
"</linearGradient>"
|
|
55
|
+
f'<linearGradient id="{node_prefix}client" x1="0%" y1="0%" x2="100%" y2="100%">'
|
|
56
|
+
f'<stop offset="0%" stop-color="{theme.node_client[0]}"/>'
|
|
57
|
+
f'<stop offset="100%" stop-color="{theme.node_client[1]}"/>'
|
|
58
|
+
"</linearGradient>"
|
|
59
|
+
f'<linearGradient id="{node_prefix}other" x1="0%" y1="0%" x2="100%" y2="100%">'
|
|
60
|
+
f'<stop offset="0%" stop-color="{theme.node_other[0]}"/>'
|
|
61
|
+
f'<stop offset="100%" stop-color="{theme.node_other[1]}"/>'
|
|
62
|
+
"</linearGradient>"
|
|
63
|
+
"</defs>"
|
|
64
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<table class="unifi-legend-table">
|
|
2
|
+
<tbody>
|
|
3
|
+
{% for row in rows %}
|
|
4
|
+
<tr><td>
|
|
5
|
+
{% if row.kind == "swatch" %}
|
|
6
|
+
<span style="display:inline-block;width:12px;height:12px;background:{{ row.fill }};border:1px solid {{ row.stroke }};border-radius:2px;margin-right:6px;"></span>{{ row.label }}
|
|
7
|
+
{% else %}
|
|
8
|
+
<span style="display:inline-flex;align-items:center;gap:6px;">
|
|
9
|
+
<svg width="42" height="10" viewBox="0 0 42 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="2" y1="5" x2="40" y2="5" stroke="{{ row.color }}" stroke-width="{{ row.width }}"{% if row.dashed %} stroke-dasharray="5 4"{% endif %} /></svg>{{ row.label }}{%- if row.bolt %} ⚡{%- endif %}</span>
|
|
10
|
+
{% endif %}
|
|
11
|
+
</td></tr>
|
|
12
|
+
{% endfor %}
|
|
13
|
+
</tbody>
|
|
14
|
+
</table>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
%%{init: {"flowchart": {"nodeSpacing": {{ node_spacing }}, "rankSpacing": {{ rank_spacing }}}, "themeVariables": {"fontSize": "{{ legend_font_size }}px", "nodePadding": {{ node_padding }}}}}%%
|
|
2
|
+
graph TB
|
|
3
|
+
subgraph legend["Legend"];
|
|
4
|
+
legend_gateway["Gateway"];
|
|
5
|
+
legend_switch["Switch"];
|
|
6
|
+
legend_ap["AP"];
|
|
7
|
+
legend_client["Client"];
|
|
8
|
+
legend_other["Other"];
|
|
9
|
+
legend_poe_a["PoE Link A"];
|
|
10
|
+
legend_poe_b["PoE Link B"];
|
|
11
|
+
legend_no_poe_a["Link A"];
|
|
12
|
+
legend_no_poe_b["Link B"];
|
|
13
|
+
legend_poe_a ---|⚡| legend_poe_b;
|
|
14
|
+
legend_no_poe_a --- legend_no_poe_b;
|
|
15
|
+
linkStyle 0 arrowhead:none;
|
|
16
|
+
linkStyle 1 arrowhead:none;
|
|
17
|
+
end
|
|
18
|
+
class legend_gateway node_gateway;
|
|
19
|
+
class legend_switch node_switch;
|
|
20
|
+
class legend_ap node_ap;
|
|
21
|
+
class legend_client node_client;
|
|
22
|
+
class legend_other node_other;
|
|
23
|
+
class legend_poe_a node_legend;
|
|
24
|
+
class legend_poe_b node_legend;
|
|
25
|
+
class legend_no_poe_a node_legend;
|
|
26
|
+
class legend_no_poe_b node_legend;
|
|
27
|
+
{{ class_defs }}
|
|
28
|
+
classDef node_legend font-size:{{ legend_font_size }}px;
|
|
29
|
+
linkStyle 0 stroke:{{ poe_link }},stroke-width:{{ poe_link_width }}px,arrowhead:{{ poe_link_arrow }};
|
|
30
|
+
linkStyle 1 stroke:{{ standard_link }},stroke-width:{{ standard_link_width }}px,arrowhead:{{ standard_link_arrow }};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# {{ title }}
|
|
2
|
+
|
|
3
|
+
{% if timestamp_line -%}
|
|
4
|
+
{{ timestamp_line }}
|
|
5
|
+
|
|
6
|
+
{% endif -%}
|
|
7
|
+
{% if dual_style -%}
|
|
8
|
+
{{ dual_style }}{% endif -%}
|
|
9
|
+
{% set title = "Map" %}
|
|
10
|
+
{% set body = map_block %}
|
|
11
|
+
{% include "markdown_section.md.j2" %}
|
|
12
|
+
|
|
13
|
+
{% if legend_title -%}
|
|
14
|
+
{% set title = legend_title %}
|
|
15
|
+
{% set body = legend_block %}
|
|
16
|
+
{% include "markdown_section.md.j2" %}
|
|
17
|
+
|
|
18
|
+
{% else -%}
|
|
19
|
+
{{ legend_block }}
|
|
20
|
+
|
|
21
|
+
{% endif -%}
|
|
22
|
+
|
|
23
|
+
{{ device_overview }}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.unifi-mermaid--light,.unifi-legend--light{display:none;}
|
|
3
|
+
.unifi-mermaid--dark,.unifi-legend--dark{display:none;}
|
|
4
|
+
[data-md-color-scheme="default"] .unifi-mermaid--light{display:block;}
|
|
5
|
+
[data-md-color-scheme="default"] .unifi-legend--light{display:block;}
|
|
6
|
+
[data-md-color-scheme="slate"] .unifi-mermaid--dark{display:block;}
|
|
7
|
+
[data-md-color-scheme="slate"] .unifi-legend--dark{display:block;}
|
|
8
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.unifi-legend-hidden,
|
|
2
|
+
.unifi-legend-hidden.unifi-legend,
|
|
3
|
+
.unifi-legend-hidden.unifi-legend--light,
|
|
4
|
+
.unifi-legend-hidden.unifi-legend--dark {
|
|
5
|
+
display: none !important;
|
|
6
|
+
}
|
|
7
|
+
.unifi-legend-sidebar {
|
|
8
|
+
margin-top: 1rem;
|
|
9
|
+
padding: 0.5rem 0.75rem;
|
|
10
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
11
|
+
border-radius: 6px;
|
|
12
|
+
font-size: 0.75rem;
|
|
13
|
+
}
|
|
14
|
+
.unifi-legend-title {
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
margin-bottom: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
.unifi-legend-sidebar table {
|
|
19
|
+
width: 100%;
|
|
20
|
+
border-collapse: collapse;
|
|
21
|
+
}
|
|
22
|
+
.unifi-legend-sidebar td,
|
|
23
|
+
.unifi-legend-sidebar th {
|
|
24
|
+
border: 0;
|
|
25
|
+
padding: 0.15rem 0;
|
|
26
|
+
}
|
|
27
|
+
.unifi-legend-sidebar svg {
|
|
28
|
+
display: block;
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2
|
+
const legends = document.querySelectorAll("[data-unifi-legend]");
|
|
3
|
+
const sidebar = document.querySelector(".md-sidebar--secondary .md-sidebar__scrollwrap");
|
|
4
|
+
if (!legends.length || !sidebar) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const wrapper = document.createElement("div");
|
|
8
|
+
wrapper.className = "unifi-legend-sidebar";
|
|
9
|
+
const title = document.createElement("div");
|
|
10
|
+
title.className = "unifi-legend-title";
|
|
11
|
+
title.textContent = "Legend";
|
|
12
|
+
wrapper.appendChild(title);
|
|
13
|
+
legends.forEach((legend) => {
|
|
14
|
+
wrapper.appendChild(legend.cloneNode(true));
|
|
15
|
+
legend.classList.add("unifi-legend-hidden");
|
|
16
|
+
});
|
|
17
|
+
sidebar.appendChild(wrapper);
|
|
18
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Jinja2 helpers for Markdown/HTML rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from jinja2 import Environment, PackageLoader, StrictUndefined, select_autoescape
|
|
6
|
+
|
|
7
|
+
_ENV = Environment(
|
|
8
|
+
loader=PackageLoader("unifi_network_maps.render", "templates"),
|
|
9
|
+
autoescape=select_autoescape(enabled_extensions=("html", "xml")),
|
|
10
|
+
trim_blocks=True,
|
|
11
|
+
lstrip_blocks=True,
|
|
12
|
+
keep_trailing_newline=True,
|
|
13
|
+
undefined=StrictUndefined,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_template(name: str, **context: object) -> str:
|
|
18
|
+
template = _ENV.get_template(name)
|
|
19
|
+
return template.render(**context)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Theme loading for Mermaid and SVG rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .mermaid_theme import DEFAULT_THEME as DEFAULT_MERMAID_THEME
|
|
10
|
+
from .mermaid_theme import MermaidTheme
|
|
11
|
+
from .svg_theme import DEFAULT_THEME as DEFAULT_SVG_THEME
|
|
12
|
+
from .svg_theme import SvgTheme
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _coerce_pair(value: object, default: tuple[str, str]) -> tuple[str, str]:
|
|
16
|
+
if isinstance(value, list | tuple) and len(value) == 2:
|
|
17
|
+
left, right = value
|
|
18
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
19
|
+
return (left, right)
|
|
20
|
+
if isinstance(value, dict):
|
|
21
|
+
left = value.get("from") or value.get("start")
|
|
22
|
+
right = value.get("to") or value.get("end")
|
|
23
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
24
|
+
return (left, right)
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _coerce_color(value: object, default: str) -> str:
|
|
29
|
+
return value if isinstance(value, str) else default
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_optional_color(value: object, default: str | None) -> str | None:
|
|
33
|
+
return value if isinstance(value, str) else default
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _coerce_optional_int(value: object, default: int | None) -> int | None:
|
|
37
|
+
if isinstance(value, int):
|
|
38
|
+
return value
|
|
39
|
+
if isinstance(value, float):
|
|
40
|
+
return int(value)
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mermaid_theme_from_dict(data: dict, base: MermaidTheme) -> MermaidTheme:
|
|
45
|
+
nodes = data.get("nodes", {}) if isinstance(data.get("nodes"), dict) else {}
|
|
46
|
+
|
|
47
|
+
def _node(name: str) -> tuple[str, str]:
|
|
48
|
+
return (
|
|
49
|
+
_coerce_color(nodes.get(name, {}).get("fill"), getattr(base, f"node_{name}")[0]),
|
|
50
|
+
_coerce_color(nodes.get(name, {}).get("stroke"), getattr(base, f"node_{name}")[1]),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return MermaidTheme(
|
|
54
|
+
node_gateway=_node("gateway"),
|
|
55
|
+
node_switch=_node("switch"),
|
|
56
|
+
node_ap=_node("ap"),
|
|
57
|
+
node_client=_node("client"),
|
|
58
|
+
node_other=_node("other"),
|
|
59
|
+
poe_link=_coerce_color(data.get("poe_link"), base.poe_link),
|
|
60
|
+
poe_link_width=int(data.get("poe_link_width", base.poe_link_width)),
|
|
61
|
+
poe_link_arrow=_coerce_color(data.get("poe_link_arrow"), base.poe_link_arrow),
|
|
62
|
+
standard_link=_coerce_color(data.get("standard_link"), base.standard_link),
|
|
63
|
+
standard_link_width=int(data.get("standard_link_width", base.standard_link_width)),
|
|
64
|
+
standard_link_arrow=_coerce_color(
|
|
65
|
+
data.get("standard_link_arrow"), base.standard_link_arrow
|
|
66
|
+
),
|
|
67
|
+
node_text=_coerce_optional_color(data.get("node_text"), base.node_text),
|
|
68
|
+
edge_label_border=_coerce_optional_color(
|
|
69
|
+
data.get("edge_label_border"), base.edge_label_border
|
|
70
|
+
),
|
|
71
|
+
edge_label_border_width=_coerce_optional_int(
|
|
72
|
+
data.get("edge_label_border_width"), base.edge_label_border_width
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _svg_theme_from_dict(data: dict, base: SvgTheme) -> SvgTheme:
|
|
78
|
+
nodes = data.get("nodes", {}) if isinstance(data.get("nodes"), dict) else {}
|
|
79
|
+
links = data.get("links", {}) if isinstance(data.get("links"), dict) else {}
|
|
80
|
+
|
|
81
|
+
return SvgTheme(
|
|
82
|
+
link_standard=_coerce_pair(links.get("standard"), base.link_standard),
|
|
83
|
+
link_poe=_coerce_pair(links.get("poe"), base.link_poe),
|
|
84
|
+
node_gateway=_coerce_pair(nodes.get("gateway"), base.node_gateway),
|
|
85
|
+
node_switch=_coerce_pair(nodes.get("switch"), base.node_switch),
|
|
86
|
+
node_ap=_coerce_pair(nodes.get("ap"), base.node_ap),
|
|
87
|
+
node_client=_coerce_pair(nodes.get("client"), base.node_client),
|
|
88
|
+
node_other=_coerce_pair(nodes.get("other"), base.node_other),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_theme(path: str | Path) -> tuple[MermaidTheme, SvgTheme]:
|
|
93
|
+
theme_path = Path(path)
|
|
94
|
+
payload = yaml.safe_load(theme_path.read_text(encoding="utf-8"))
|
|
95
|
+
if not isinstance(payload, dict):
|
|
96
|
+
raise ValueError("Theme file must contain a YAML mapping")
|
|
97
|
+
|
|
98
|
+
mermaid_data = payload.get("mermaid", {})
|
|
99
|
+
svg_data = payload.get("svg", {})
|
|
100
|
+
|
|
101
|
+
mermaid_theme = _mermaid_theme_from_dict(mermaid_data, DEFAULT_MERMAID_THEME)
|
|
102
|
+
svg_theme = _svg_theme_from_dict(svg_data, DEFAULT_SVG_THEME)
|
|
103
|
+
return mermaid_theme, svg_theme
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_themes(theme_file: str | Path | None) -> tuple[MermaidTheme, SvgTheme]:
|
|
107
|
+
if theme_file:
|
|
108
|
+
return load_theme(theme_file)
|
|
109
|
+
return DEFAULT_MERMAID_THEME, DEFAULT_SVG_THEME
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unifi-network-maps
|
|
3
|
+
Version: 1.4.11
|
|
4
|
+
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
|
+
Author: Merlijn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/merlijntishauser/unifi-network-maps
|
|
8
|
+
Project-URL: Repository, https://github.com/merlijntishauser/unifi-network-maps
|
|
9
|
+
Project-URL: Issues, https://github.com/merlijntishauser/unifi-network-maps/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/merlijntishauser/unifi-network-maps/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: unifi,mermaid,network,topology,diagram,svg
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Documentation
|
|
20
|
+
Classifier: Topic :: System :: Networking
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: unifi-controller-api==0.3.2
|
|
25
|
+
Requires-Dist: python-dotenv==1.2.1
|
|
26
|
+
Requires-Dist: PyYAML==6.0.3
|
|
27
|
+
Requires-Dist: Jinja2==3.1.6
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: Faker==40.1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: behave==1.3.3; extra == "dev"
|
|
31
|
+
Requires-Dist: pre-commit==4.5.1; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov==7.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pyright==1.1.408; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff==0.14.13; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# unifi-network-maps
|
|
39
|
+
|
|
40
|
+
[](https://github.com/merlijntishauser/unifi-network-maps/actions/workflows/ci.yml)
|
|
41
|
+
[](https://github.com/merlijntishauser/unifi-network-maps/actions/workflows/codeql.yml)
|
|
42
|
+
[](https://github.com/merlijntishauser/unifi-network-maps/actions/workflows/publish.yml)
|
|
43
|
+
[](https://pypi.org/project/unifi-network-maps/)
|
|
44
|
+
|
|
45
|
+
Dynamic UniFi network maps generated from LLDP topology. Output can be a range of options including Markdown,
|
|
46
|
+
Mermaid, SVG (including an Isometric view), and MkDocs.
|
|
47
|
+
|
|
48
|
+
Python 3.12+ is supported (3.13 preferred).
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
PyPI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install unifi-network-maps
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
From source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m venv .venv
|
|
62
|
+
source .venv/bin/activate
|
|
63
|
+
pip install -r requirements-build.txt
|
|
64
|
+
pip install .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Set environment variables (no secrets in code). The CLI loads `.env` automatically if present:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export UNIFI_URL=https://192.168.1.1
|
|
73
|
+
export UNIFI_SITE=default
|
|
74
|
+
export UNIFI_USER=local_admin
|
|
75
|
+
export UNIFI_PASS=********
|
|
76
|
+
export UNIFI_VERIFY_SSL=false
|
|
77
|
+
export UNIFI_REQUEST_TIMEOUT_SECONDS=10
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use a custom env file:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
unifi-network-maps --env-file ./site.env --stdout
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Quickstart
|
|
87
|
+
|
|
88
|
+
Basic map (tree layout by LLDP hops):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
unifi-network-maps --stdout
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Write Markdown for notes tools:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
unifi-network-maps --markdown --output ./network.md
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
Show ports + clients:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
unifi-network-maps --include-ports --include-clients --stdout
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
SVG output (orthogonal layout + icons):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
unifi-network-maps --format svg --output ./network.svg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Isometric SVG output:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
unifi-network-maps --format svg-iso --output ./network.svg
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
MkDocs output (ports included, no clients):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
MkDocs output with clients:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
unifi-network-maps --format mkdocs --include-clients --output ./docs/unifi-network.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
MkDocs output with dual Mermaid blocks for Material theme switching:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
unifi-network-maps --format mkdocs --mkdocs-dual-theme --output ./docs/unifi-network.md
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
LLDP tables for troubleshooting:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
unifi-network-maps --format lldp-md --output ./lldp.md
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Legend only:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
unifi-network-maps --legend-only --stdout
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Home Assistant integration
|
|
151
|
+
|
|
152
|
+
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
153
|
+
- https://github.com/merlijntishauser/unifi-network-maps-ha
|
|
154
|
+
|
|
155
|
+
## Examples (mock data)
|
|
156
|
+
|
|
157
|
+
These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
|
|
158
|
+
Mock generation requires dev dependencies (`pip install -r requirements-dev.txt -c constraints.txt`).
|
|
159
|
+
Regenerate the fixture + SVG with `make mock-data`.
|
|
160
|
+
|
|
161
|
+
Generate mock data (dev-only, uses Faker):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
unifi-network-maps --generate-mock examples/mock_data.json --mock-seed 1337
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Generate the isometric SVG:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
unifi-network-maps --mock-data examples/mock_data.json --include-ports --include-clients --format svg-iso --output examples/output/network_ports_clients_iso.svg
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+

|
|
174
|
+
|
|
175
|
+
Mermaid example with ports:
|
|
176
|
+
|
|
177
|
+
```mermaid
|
|
178
|
+
graph TB
|
|
179
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 7 (AP Attic) <-> AP Attic: Port 1 (Core Switch)"| ap_attic["AP Attic"];
|
|
180
|
+
core_switch["Core Switch"] ---|"Core Switch: Port 3 (AP Living Room) <-> AP Living Room: Port 1 (Core Switch)"| ap_living_room["AP Living Room"];
|
|
181
|
+
cloud_gateway["Cloud Gateway"] ---|"Cloud Gateway: Port 9 (Core Switch) <-> Core Switch: Port 1 (Cloud Gateway)"| core_switch["Core Switch"];
|
|
182
|
+
class cloud_gateway node_gateway;
|
|
183
|
+
class core_switch node_switch;
|
|
184
|
+
class ap_living_room node_ap;
|
|
185
|
+
class ap_attic node_ap;
|
|
186
|
+
classDef node_gateway fill:#ffe3b3,stroke:#d98300,stroke-width:1px;
|
|
187
|
+
classDef node_switch fill:#d6ecff,stroke:#3a7bd5,stroke-width:1px;
|
|
188
|
+
classDef node_ap fill:#d7f5e7,stroke:#27ae60,stroke-width:1px;
|
|
189
|
+
classDef node_client fill:#f2e5ff,stroke:#7f3fbf,stroke-width:1px;
|
|
190
|
+
classDef node_other fill:#eeeeee,stroke:#8f8f8f,stroke-width:1px;
|
|
191
|
+
classDef node_legend font-size:10px;
|
|
192
|
+
linkStyle 0 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
193
|
+
linkStyle 1 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## MkDocs Material example
|
|
197
|
+
|
|
198
|
+
See `examples/mkdocs/` for a ready-to-use setup that renders Mermaid diagrams
|
|
199
|
+
with Material for MkDocs, including a sample `unifi-network` page and legend.
|
|
200
|
+
|
|
201
|
+
The built-in themes live at `src/unifi_network_maps/assets/themes/default.yaml` and
|
|
202
|
+
`src/unifi_network_maps/assets/themes/dark.yaml`.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
## Options
|
|
206
|
+
|
|
207
|
+
The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `Output`, `Debug`).
|
|
208
|
+
|
|
209
|
+
Source:
|
|
210
|
+
- `--site`: override `UNIFI_SITE`.
|
|
211
|
+
- `--env-file`: load environment variables from a specific `.env` file.
|
|
212
|
+
- `--mock-data`: use mock data JSON instead of the UniFi API.
|
|
213
|
+
|
|
214
|
+
Mock:
|
|
215
|
+
- `--generate-mock`: write mock data JSON and exit.
|
|
216
|
+
- `--mock-seed`: seed for deterministic mock generation.
|
|
217
|
+
- `--mock-switches`: number of switches to generate.
|
|
218
|
+
- `--mock-aps`: number of access points to generate.
|
|
219
|
+
- `--mock-wired-clients`: number of wired clients to generate.
|
|
220
|
+
- `--mock-wireless-clients`: number of wireless clients to generate.
|
|
221
|
+
|
|
222
|
+
Functional:
|
|
223
|
+
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
224
|
+
- `--include-clients`: add active wired clients as leaf nodes.
|
|
225
|
+
- `--client-scope wired|wireless|all`: which client types to include (default wired).
|
|
226
|
+
- `--only-unifi`: only include neighbors that are UniFi devices.
|
|
227
|
+
- `--no-cache`: disable UniFi API cache reads and writes.
|
|
228
|
+
|
|
229
|
+
Mermaid:
|
|
230
|
+
- `--direction LR|TB`: diagram direction for Mermaid (default TB).
|
|
231
|
+
- `--group-by-type`: group nodes by gateway/switch/AP in Mermaid subgraphs.
|
|
232
|
+
- `--legend-scale`: scale legend font/link sizes for Mermaid outputs (default 1.0).
|
|
233
|
+
- `--legend-style auto|compact|diagram`: legend rendering mode (auto uses compact for mkdocs).
|
|
234
|
+
- `--legend-only`: render just the legend as a separate Mermaid graph (Mermaid only).
|
|
235
|
+
|
|
236
|
+
SVG:
|
|
237
|
+
- `--svg-width/--svg-height`: override SVG output dimensions.
|
|
238
|
+
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
239
|
+
|
|
240
|
+
Output:
|
|
241
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
242
|
+
- `--stdout`: write output to stdout.
|
|
243
|
+
- `--markdown`: wrap Mermaid output in a code fence.
|
|
244
|
+
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
245
|
+
- `--mkdocs-dual-theme`: render light/dark Mermaid blocks for Material theme switching.
|
|
246
|
+
- `--mkdocs-timestamp-zone`: timezone for mkdocs timestamp (`Europe/Amsterdam` default; use `off` to disable).
|
|
247
|
+
|
|
248
|
+
Debug:
|
|
249
|
+
- `--debug-dump`: dump gateway + sample devices to stderr for debugging.
|
|
250
|
+
- `--debug-sample N`: number of non-gateway devices in debug dump (default 2).
|
|
251
|
+
|
|
252
|
+
## Theme file
|
|
253
|
+
|
|
254
|
+
Example theme YAML (override only the values you want):
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
mermaid:
|
|
258
|
+
nodes:
|
|
259
|
+
gateway:
|
|
260
|
+
fill: "#ffe3b3"
|
|
261
|
+
stroke: "#d98300"
|
|
262
|
+
poe_link: "#1e88e5"
|
|
263
|
+
svg:
|
|
264
|
+
links:
|
|
265
|
+
standard:
|
|
266
|
+
from: "#2ecc71"
|
|
267
|
+
to: "#1b8f4a"
|
|
268
|
+
poe:
|
|
269
|
+
from: "#1e88e5"
|
|
270
|
+
to: "#0d47a1"
|
|
271
|
+
nodes:
|
|
272
|
+
switch:
|
|
273
|
+
from: "#d6ecff"
|
|
274
|
+
to: "#b6dcff"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Notes
|
|
278
|
+
|
|
279
|
+
- Default output is top-to-bottom (TB) and rendered as a hop-based tree from the gateway(s).
|
|
280
|
+
- Nodes are color-coded by type (gateway/switch/AP/client) with a sensible default palette.
|
|
281
|
+
- PoE links are highlighted in blue and annotated with a power icon when detected from `port_table`.
|
|
282
|
+
- Wireless client links render as dashed lines to indicate the last-known upstream.
|
|
283
|
+
- SVG output uses vendored device glyphs from `src/unifi_network_maps/assets/icons`.
|
|
284
|
+
- Isometric SVG output uses MIT-licensed icons from `markmanx/isopacks`.
|
|
285
|
+
- SVG port labels render inside child nodes for readability.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
## AI Disclosure
|
|
289
|
+
|
|
290
|
+
This project used OpenAI Codex as a coding assistant for portions of the implementation and documentation.
|