unifi-network-maps 1.4.5__py3-none-any.whl → 1.4.7__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 -1
- unifi_network_maps/cli/main.py +5 -1
- unifi_network_maps/render/svg.py +55 -3
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/METADATA +7 -2
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/RECORD +9 -9
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.7.dist-info}/top_level.txt +0 -0
unifi_network_maps/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.4.
|
|
1
|
+
__version__ = "1.4.7"
|
unifi_network_maps/cli/main.py
CHANGED
|
@@ -90,7 +90,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
90
90
|
except ValueError as exc:
|
|
91
91
|
logging.error(str(exc))
|
|
92
92
|
return 2
|
|
93
|
-
|
|
93
|
+
try:
|
|
94
|
+
mermaid_theme, svg_theme = resolve_themes(args.theme_file)
|
|
95
|
+
except Exception as exc: # noqa: BLE001
|
|
96
|
+
logging.error("Failed to load theme file: %s", exc)
|
|
97
|
+
return 2
|
|
94
98
|
|
|
95
99
|
if args.legend_only:
|
|
96
100
|
legend_style = resolve_legend_style(
|
unifi_network_maps/render/svg.py
CHANGED
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import math
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from html import escape as _escape_attr
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from ..model.topology import Edge
|
|
@@ -472,6 +473,7 @@ def render_svg(
|
|
|
472
473
|
edges: list[Edge],
|
|
473
474
|
*,
|
|
474
475
|
node_types: dict[str, str],
|
|
476
|
+
node_data: dict[str, dict[str, str]] | None = None,
|
|
475
477
|
options: SvgOptions | None = None,
|
|
476
478
|
theme: SvgTheme = DEFAULT_THEME,
|
|
477
479
|
) -> str:
|
|
@@ -503,6 +505,7 @@ def render_svg(
|
|
|
503
505
|
node_port_prefix,
|
|
504
506
|
icons,
|
|
505
507
|
options,
|
|
508
|
+
node_data,
|
|
506
509
|
)
|
|
507
510
|
|
|
508
511
|
lines.append("</svg>")
|
|
@@ -519,6 +522,8 @@ def _render_svg_edges(
|
|
|
519
522
|
node_port_labels: dict[str, str] = {}
|
|
520
523
|
node_port_prefix: dict[str, str] = {}
|
|
521
524
|
for edge in edges:
|
|
525
|
+
_record_edge_labels(edge, node_types, node_port_labels, node_port_prefix)
|
|
526
|
+
for edge in sorted(edges, key=lambda item: item.poe):
|
|
522
527
|
if edge.left not in positions or edge.right not in positions:
|
|
523
528
|
continue
|
|
524
529
|
src_x, src_y = positions[edge.left]
|
|
@@ -530,7 +535,17 @@ def _render_svg_edges(
|
|
|
530
535
|
mid_y = (src_bottom + dst_top) / 2
|
|
531
536
|
color = "url(#link-poe)" if edge.poe else "url(#link-standard)"
|
|
532
537
|
width_px = 2 if edge.poe else 1
|
|
533
|
-
|
|
538
|
+
if math.isclose(src_cx, dst_cx, abs_tol=0.01):
|
|
539
|
+
elbow_x = src_cx + 0.5
|
|
540
|
+
path = (
|
|
541
|
+
f"M {src_cx} {src_bottom} L {src_cx} {mid_y} "
|
|
542
|
+
f"L {elbow_x} {mid_y} L {dst_cx} {mid_y} L {dst_cx} {dst_top}"
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
path = (
|
|
546
|
+
f"M {src_cx} {src_bottom} L {src_cx} {mid_y} "
|
|
547
|
+
f"L {dst_cx} {mid_y} L {dst_cx} {dst_top}"
|
|
548
|
+
)
|
|
534
549
|
dash = ' stroke-dasharray="6 4"' if edge.wireless else ""
|
|
535
550
|
lines.append(
|
|
536
551
|
f'<path d="{path}" stroke="{color}" stroke-width="{width_px}" fill="none"{dash}/>'
|
|
@@ -542,7 +557,6 @@ def _render_svg_edges(
|
|
|
542
557
|
f'<text x="{icon_x}" y="{icon_y}" text-anchor="middle" fill="#1e88e5" '
|
|
543
558
|
f'font-size="{max(options.font_size, 10)}">⚡</text>'
|
|
544
559
|
)
|
|
545
|
-
_record_edge_labels(edge, node_types, node_port_labels, node_port_prefix)
|
|
546
560
|
return node_port_labels, node_port_prefix
|
|
547
561
|
|
|
548
562
|
|
|
@@ -589,11 +603,19 @@ def _render_svg_nodes(
|
|
|
589
603
|
node_port_prefix: dict[str, str],
|
|
590
604
|
icons: dict[str, str],
|
|
591
605
|
options: SvgOptions,
|
|
606
|
+
node_data: dict[str, dict[str, str]] | None,
|
|
592
607
|
) -> None:
|
|
593
608
|
for name, (x, y) in positions.items():
|
|
594
609
|
node_type = node_types.get(name, "other")
|
|
595
610
|
fill, stroke = _TYPE_COLORS.get(node_type, _TYPE_COLORS["other"])
|
|
596
611
|
fill = f"url(#node-{node_type})"
|
|
612
|
+
group_attrs = _svg_node_group_attrs(node_data, name, node_type)
|
|
613
|
+
lines.append(f"<g{group_attrs}>")
|
|
614
|
+
lines.append(f"<title>{_escape_text(name)}</title>")
|
|
615
|
+
lines.append(
|
|
616
|
+
f'<rect x="{x}" y="{y}" width="{options.node_width}" height="{options.node_height}" '
|
|
617
|
+
'fill="transparent" pointer-events="all" class="node-hitbox"/>'
|
|
618
|
+
)
|
|
597
619
|
lines.append(
|
|
598
620
|
f'<rect x="{x}" y="{y}" width="{options.node_width}" height="{options.node_height}" '
|
|
599
621
|
f'rx="6" ry="6" fill="{fill}" stroke="{stroke}" stroke-width="1"/>'
|
|
@@ -631,6 +653,27 @@ def _render_svg_nodes(
|
|
|
631
653
|
lines.append(
|
|
632
654
|
f'<text x="{text_x}" y="{text_y}" fill="#1f1f1f" text-anchor="start">{safe_name}</text>'
|
|
633
655
|
)
|
|
656
|
+
lines.append("</g>")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _svg_node_group_attrs(
|
|
660
|
+
node_data: dict[str, dict[str, str]] | None,
|
|
661
|
+
name: str,
|
|
662
|
+
node_type: str,
|
|
663
|
+
) -> str:
|
|
664
|
+
attrs: dict[str, str] = {
|
|
665
|
+
"class": "unm-node",
|
|
666
|
+
"data-node-id": name,
|
|
667
|
+
"data-node-type": node_type,
|
|
668
|
+
}
|
|
669
|
+
if node_data and (extra := node_data.get(name)):
|
|
670
|
+
for key, value in extra.items():
|
|
671
|
+
if key == "class":
|
|
672
|
+
attrs["class"] = f"{attrs['class']} {value}".strip()
|
|
673
|
+
else:
|
|
674
|
+
attrs[key] = value
|
|
675
|
+
rendered = [f' {key}="{_escape_attr(value, quote=True)}"' for key, value in attrs.items()]
|
|
676
|
+
return "".join(rendered)
|
|
634
677
|
|
|
635
678
|
|
|
636
679
|
def _iso_project(layout: IsoLayout, gx: float, gy: float) -> tuple[float, float]:
|
|
@@ -759,6 +802,8 @@ def _render_iso_edges(
|
|
|
759
802
|
node_port_prefix: dict[str, str],
|
|
760
803
|
) -> None:
|
|
761
804
|
for edge in edges:
|
|
805
|
+
_record_iso_edge_label(edge, node_types, node_port_labels, node_port_prefix)
|
|
806
|
+
for edge in sorted(edges, key=lambda item: item.poe):
|
|
762
807
|
if edge.left not in positions or edge.right not in positions:
|
|
763
808
|
continue
|
|
764
809
|
src_grid = grid_positions.get(edge.left)
|
|
@@ -800,7 +845,6 @@ def _render_iso_edges(
|
|
|
800
845
|
f'<text x="{icon_x}" y="{icon_y}" text-anchor="middle" fill="#1e88e5" '
|
|
801
846
|
f'font-size="{max(options.font_size, 10)}">⚡</text>'
|
|
802
847
|
)
|
|
803
|
-
_record_iso_edge_label(edge, node_types, node_port_labels, node_port_prefix)
|
|
804
848
|
|
|
805
849
|
|
|
806
850
|
def _iso_edge_path(
|
|
@@ -1019,7 +1063,14 @@ def _render_iso_node(
|
|
|
1019
1063
|
node_depth = 0.0
|
|
1020
1064
|
tile_w = layout.tile_width
|
|
1021
1065
|
tile_h = layout.tile_height
|
|
1066
|
+
group_attrs = _svg_node_group_attrs(None, name, node_type)
|
|
1067
|
+
lines.append(f"<g{group_attrs}>")
|
|
1068
|
+
lines.append(f"<title>{_escape_text(name)}</title>")
|
|
1022
1069
|
top, left, right = _iso_node_polygons(x, y, tile_w, tile_h, node_depth)
|
|
1070
|
+
lines.append(
|
|
1071
|
+
f'<polygon points="{_points_to_svg(top)}" fill="transparent" '
|
|
1072
|
+
'pointer-events="all" class="node-hitbox"/>'
|
|
1073
|
+
)
|
|
1023
1074
|
left_fill = "#d0d0d0" if node_type == "other" else "#dcdcdc"
|
|
1024
1075
|
right_fill = "#c2c2c2" if node_type == "other" else "#c8c8c8"
|
|
1025
1076
|
_iso_render_faces(
|
|
@@ -1085,6 +1136,7 @@ def _render_iso_node(
|
|
|
1085
1136
|
f'<text x="{name_x}" y="{name_y}" text-anchor="middle" fill="#1f1f1f" '
|
|
1086
1137
|
f'font-size="{name_font_size}" transform="{name_transform}">{_escape_text(name)}</text>'
|
|
1087
1138
|
)
|
|
1139
|
+
lines.append("</g>")
|
|
1088
1140
|
|
|
1089
1141
|
|
|
1090
1142
|
def _render_iso_nodes(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.7
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: python-dotenv==1.2.1
|
|
|
25
25
|
Requires-Dist: PyYAML==6.0.3
|
|
26
26
|
Requires-Dist: Jinja2==3.1.6
|
|
27
27
|
Provides-Extra: dev
|
|
28
|
-
Requires-Dist: Faker==40.1.
|
|
28
|
+
Requires-Dist: Faker==40.1.2; extra == "dev"
|
|
29
29
|
Requires-Dist: behave==1.3.3; extra == "dev"
|
|
30
30
|
Requires-Dist: pre-commit==4.5.1; extra == "dev"
|
|
31
31
|
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
@@ -144,6 +144,11 @@ Legend only:
|
|
|
144
144
|
unifi-network-maps --legend-only --stdout
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## Home Assistant integration
|
|
148
|
+
|
|
149
|
+
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
150
|
+
- https://github.com/merlijntishauser/unifi-network-maps-ha
|
|
151
|
+
|
|
147
152
|
## Examples (mock data)
|
|
148
153
|
|
|
149
154
|
These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
unifi_network_maps/__init__.py,sha256=
|
|
1
|
+
unifi_network_maps/__init__.py,sha256=pHClr_RD94RdAO4nIJOBug2vXKKjcRzEGxsyrQcqnHY,22
|
|
2
2
|
unifi_network_maps/__main__.py,sha256=XsOjaqslAVgyVlOTokjVddZ2iT8apZXpJ_OB-9WEEe4,179
|
|
3
3
|
unifi_network_maps/adapters/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
|
|
4
4
|
unifi_network_maps/adapters/config.py,sha256=Bx9JDZxxY7Gjxyb8FDT0dxiKfgXt_TmzTDbgvpwB53s,1548
|
|
@@ -53,7 +53,7 @@ unifi_network_maps/assets/themes/default.yaml,sha256=F2Jj18NmdaJ_zyERvGAn8NEWBwa
|
|
|
53
53
|
unifi_network_maps/cli/__init__.py,sha256=cds9GvFNZmYAR22Ab3TSzfriSAW--kf9jvC5U-21AoA,70
|
|
54
54
|
unifi_network_maps/cli/__main__.py,sha256=nK_jh78VW3h3DRvSpjzpcf64zkCqniP2k82xUR9Hw2I,147
|
|
55
55
|
unifi_network_maps/cli/args.py,sha256=lIgQDeob_SIhjXg76hJsnpgNOKupSjSYum_MqarWOkE,5552
|
|
56
|
-
unifi_network_maps/cli/main.py,sha256=
|
|
56
|
+
unifi_network_maps/cli/main.py,sha256=jQXesuHJLTQl4lBk1DD6em67Wj9oEjBmH9X-X1zA6MI,4150
|
|
57
57
|
unifi_network_maps/cli/render.py,sha256=sUyDWm_I_zbEcKuNEpKUXDxhe1XptgOYsmdMP9BJ3Eg,7040
|
|
58
58
|
unifi_network_maps/cli/runtime.py,sha256=Hln4LMpuTrEsy6gIBmqkOrUpMb4nTeZ-AH72KyxpZwA,4723
|
|
59
59
|
unifi_network_maps/io/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
|
|
@@ -76,7 +76,7 @@ unifi_network_maps/render/markdown_tables.py,sha256=VvM0fSnSmpeeDPcD5pXaL_j_PTF0
|
|
|
76
76
|
unifi_network_maps/render/mermaid.py,sha256=xsC57Xg-nKhmlVATzEbwLkMM2BOeDYlBjZuxBIPhHeI,8324
|
|
77
77
|
unifi_network_maps/render/mermaid_theme.py,sha256=7nqLlvhaUA4z0YOs0ByEx_yHWcQD_hJJjhDtRcbSpg4,1781
|
|
78
78
|
unifi_network_maps/render/mkdocs.py,sha256=EOST9_eP1ZoZQax-p-2fjlelrl3AKEJ9Gn-KXW8TI-o,5389
|
|
79
|
-
unifi_network_maps/render/svg.py,sha256=
|
|
79
|
+
unifi_network_maps/render/svg.py,sha256=W0tiuCkicTxbHTJbeRbnWzFfaO9yawL7e6EJiP7TLnU,40791
|
|
80
80
|
unifi_network_maps/render/svg_theme.py,sha256=Si1ArM3v_-wAvHZyLFPiOZ0ohQRd6ezIckwC3_b-WIw,2684
|
|
81
81
|
unifi_network_maps/render/templating.py,sha256=VJbXzZFBPjL8LFFPcLf_EU5Eu53GN9_vpten2Mf9A-k,576
|
|
82
82
|
unifi_network_maps/render/theme.py,sha256=vKYdPhcGEOV1o_irwqzJlIXPgRvZqQEzYYV2_TxZn4E,4301
|
|
@@ -91,9 +91,9 @@ unifi_network_maps/render/templates/mkdocs_html_block.html.j2,sha256=5l5-BbNujOc
|
|
|
91
91
|
unifi_network_maps/render/templates/mkdocs_legend.css.j2,sha256=tkTI-RagBSgdjUygVenlTsQFenU09ePbXOfDt_Q7YRM,612
|
|
92
92
|
unifi_network_maps/render/templates/mkdocs_legend.js.j2,sha256=qMYyCKsJ84uXf1wGgzbc7Bc49RU4oyuaGK9KrgQDQEI,685
|
|
93
93
|
unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2,sha256=9IncllWQpoI8BN3A7b2zOQ5cksj97ddsjHJ-aBhpw7o,66
|
|
94
|
-
unifi_network_maps-1.4.
|
|
95
|
-
unifi_network_maps-1.4.
|
|
96
|
-
unifi_network_maps-1.4.
|
|
97
|
-
unifi_network_maps-1.4.
|
|
98
|
-
unifi_network_maps-1.4.
|
|
99
|
-
unifi_network_maps-1.4.
|
|
94
|
+
unifi_network_maps-1.4.7.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
|
|
95
|
+
unifi_network_maps-1.4.7.dist-info/METADATA,sha256=kHKNrAptcX2vYnP1c4-0sxA9l6bmTBXSHLLOCskIlm0,9754
|
|
96
|
+
unifi_network_maps-1.4.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
97
|
+
unifi_network_maps-1.4.7.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
|
|
98
|
+
unifi_network_maps-1.4.7.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
|
|
99
|
+
unifi_network_maps-1.4.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|