unifi-network-maps 1.4.5__py3-none-any.whl → 1.4.6__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 +34 -3
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/METADATA +6 -1
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/RECORD +9 -9
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.5.dist-info → unifi_network_maps-1.4.6.dist-info}/top_level.txt +0 -0
unifi_network_maps/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.4.
|
|
1
|
+
__version__ = "1.4.6"
|
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,15 @@ 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
|
+
extra_attrs = _svg_node_attrs(node_data, name)
|
|
613
|
+
if extra_attrs:
|
|
614
|
+
lines.append(f"<g{extra_attrs}>")
|
|
597
615
|
lines.append(
|
|
598
616
|
f'<rect x="{x}" y="{y}" width="{options.node_width}" height="{options.node_height}" '
|
|
599
617
|
f'rx="6" ry="6" fill="{fill}" stroke="{stroke}" stroke-width="1"/>'
|
|
@@ -631,6 +649,18 @@ def _render_svg_nodes(
|
|
|
631
649
|
lines.append(
|
|
632
650
|
f'<text x="{text_x}" y="{text_y}" fill="#1f1f1f" text-anchor="start">{safe_name}</text>'
|
|
633
651
|
)
|
|
652
|
+
if extra_attrs:
|
|
653
|
+
lines.append("</g>")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _svg_node_attrs(node_data: dict[str, dict[str, str]] | None, name: str) -> str:
|
|
657
|
+
if not node_data:
|
|
658
|
+
return ""
|
|
659
|
+
attrs = node_data.get(name)
|
|
660
|
+
if not attrs:
|
|
661
|
+
return ""
|
|
662
|
+
rendered = [f' {key}="{_escape_attr(value, quote=True)}"' for key, value in attrs.items()]
|
|
663
|
+
return "".join(rendered)
|
|
634
664
|
|
|
635
665
|
|
|
636
666
|
def _iso_project(layout: IsoLayout, gx: float, gy: float) -> tuple[float, float]:
|
|
@@ -759,6 +789,8 @@ def _render_iso_edges(
|
|
|
759
789
|
node_port_prefix: dict[str, str],
|
|
760
790
|
) -> None:
|
|
761
791
|
for edge in edges:
|
|
792
|
+
_record_iso_edge_label(edge, node_types, node_port_labels, node_port_prefix)
|
|
793
|
+
for edge in sorted(edges, key=lambda item: item.poe):
|
|
762
794
|
if edge.left not in positions or edge.right not in positions:
|
|
763
795
|
continue
|
|
764
796
|
src_grid = grid_positions.get(edge.left)
|
|
@@ -800,7 +832,6 @@ def _render_iso_edges(
|
|
|
800
832
|
f'<text x="{icon_x}" y="{icon_y}" text-anchor="middle" fill="#1e88e5" '
|
|
801
833
|
f'font-size="{max(options.font_size, 10)}">⚡</text>'
|
|
802
834
|
)
|
|
803
|
-
_record_iso_edge_label(edge, node_types, node_port_labels, node_port_prefix)
|
|
804
835
|
|
|
805
836
|
|
|
806
837
|
def _iso_edge_path(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.6
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -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=0optyte4cOxIRRV4Vdh_XYGrxTLJSuPWvhUDQkXvb4U,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=KLsQKmk1ActaMYnVnhta3BNCF4HrlcGJYhV8F8VTMmg,39924
|
|
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.6.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
|
|
95
|
+
unifi_network_maps-1.4.6.dist-info/METADATA,sha256=pQZefF7brMQc9vGxphkG7AzFaQNrotEy3amCA3sFrYc,9754
|
|
96
|
+
unifi_network_maps-1.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
97
|
+
unifi_network_maps-1.4.6.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
|
|
98
|
+
unifi_network_maps-1.4.6.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
|
|
99
|
+
unifi_network_maps-1.4.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|