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.
@@ -1 +1 @@
1
- __version__ = "1.4.5"
1
+ __version__ = "1.4.7"
@@ -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
- mermaid_theme, svg_theme = resolve_themes(args.theme_file)
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(
@@ -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
- path = f"M {src_cx} {src_bottom} L {src_cx} {mid_y} L {dst_cx} {mid_y} L {dst_cx} {dst_top}"
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.5
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.0; extra == "dev"
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=Fv2JHnR-ADpzdVitwHAsUrkhe-BpoDkmx7MxtXxlm1k,22
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=8r4zxhDYzDNJ-JMzX3HjPD1QTIUIyrrQeCGxcqe2vqc,4015
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=RHmSmlKxrS8m3-zYsTJu-GZugsZt8X8TDsPpK7DA6tg,38807
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.5.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
95
- unifi_network_maps-1.4.5.dist-info/METADATA,sha256=AVMHFRRAIPqB5nH1lObiLgXDGnwx_Ia38TkZaUzi60k,9558
96
- unifi_network_maps-1.4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- unifi_network_maps-1.4.5.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
98
- unifi_network_maps-1.4.5.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
99
- unifi_network_maps-1.4.5.dist-info/RECORD,,
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,,