unifi-network-maps 1.4.14__py3-none-any.whl → 1.5.0__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.
Files changed (74) hide show
  1. unifi_network_maps/__init__.py +1 -1
  2. unifi_network_maps/adapters/unifi.py +83 -101
  3. unifi_network_maps/assets/icons/modern/ap.svg +9 -0
  4. unifi_network_maps/assets/icons/modern/camera.svg +9 -0
  5. unifi_network_maps/assets/icons/modern/client.svg +9 -0
  6. unifi_network_maps/assets/icons/modern/game_console.svg +10 -0
  7. unifi_network_maps/assets/icons/modern/gateway.svg +17 -0
  8. unifi_network_maps/assets/icons/modern/iot.svg +9 -0
  9. unifi_network_maps/assets/icons/modern/nas.svg +9 -0
  10. unifi_network_maps/assets/icons/modern/other.svg +10 -0
  11. unifi_network_maps/assets/icons/modern/phone.svg +10 -0
  12. unifi_network_maps/assets/icons/modern/printer.svg +9 -0
  13. unifi_network_maps/assets/icons/modern/speaker.svg +10 -0
  14. unifi_network_maps/assets/icons/modern/switch.svg +10 -0
  15. unifi_network_maps/assets/icons/modern/tv.svg +10 -0
  16. unifi_network_maps/assets/icons/modern-flat/ap.svg +5 -0
  17. unifi_network_maps/assets/icons/modern-flat/camera.svg +5 -0
  18. unifi_network_maps/assets/icons/modern-flat/client.svg +5 -0
  19. unifi_network_maps/assets/icons/modern-flat/game_console.svg +6 -0
  20. unifi_network_maps/assets/icons/modern-flat/gateway.svg +13 -0
  21. unifi_network_maps/assets/icons/modern-flat/iot.svg +5 -0
  22. unifi_network_maps/assets/icons/modern-flat/nas.svg +5 -0
  23. unifi_network_maps/assets/icons/modern-flat/other.svg +6 -0
  24. unifi_network_maps/assets/icons/modern-flat/phone.svg +6 -0
  25. unifi_network_maps/assets/icons/modern-flat/printer.svg +5 -0
  26. unifi_network_maps/assets/icons/modern-flat/speaker.svg +6 -0
  27. unifi_network_maps/assets/icons/modern-flat/switch.svg +6 -0
  28. unifi_network_maps/assets/icons/modern-flat/tv.svg +6 -0
  29. unifi_network_maps/assets/themes/dark.yaml +53 -10
  30. unifi_network_maps/assets/themes/default.yaml +34 -0
  31. unifi_network_maps/assets/themes/minimal-dark.yaml +98 -0
  32. unifi_network_maps/assets/themes/minimal.yaml +92 -0
  33. unifi_network_maps/assets/themes/unifi-dark.yaml +97 -0
  34. unifi_network_maps/assets/themes/unifi.yaml +92 -0
  35. unifi_network_maps/cli/args.py +54 -0
  36. unifi_network_maps/cli/main.py +18 -7
  37. unifi_network_maps/cli/render.py +79 -27
  38. unifi_network_maps/cli/runtime.py +29 -15
  39. unifi_network_maps/io/debug.py +2 -1
  40. unifi_network_maps/io/export.py +19 -13
  41. unifi_network_maps/io/mock_data.py +5 -3
  42. unifi_network_maps/io/paths.py +5 -3
  43. unifi_network_maps/model/classify.py +199 -0
  44. unifi_network_maps/model/clients.py +271 -0
  45. unifi_network_maps/model/connection.py +37 -0
  46. unifi_network_maps/model/diff.py +544 -0
  47. unifi_network_maps/model/edges.py +558 -0
  48. unifi_network_maps/model/helpers.py +64 -0
  49. unifi_network_maps/model/lldp.py +20 -25
  50. unifi_network_maps/model/mock.py +110 -23
  51. unifi_network_maps/model/snapshot.py +294 -0
  52. unifi_network_maps/model/topology.py +143 -931
  53. unifi_network_maps/model/topology_coerce.py +339 -0
  54. unifi_network_maps/model/vlans.py +32 -46
  55. unifi_network_maps/model/wan.py +132 -0
  56. unifi_network_maps/render/device_ports_md.py +39 -97
  57. unifi_network_maps/render/device_summary.py +53 -0
  58. unifi_network_maps/render/lldp_md.py +29 -219
  59. unifi_network_maps/render/markdown_tables.py +8 -0
  60. unifi_network_maps/render/mermaid.py +11 -2
  61. unifi_network_maps/render/mkdocs.py +2 -1
  62. unifi_network_maps/render/svg.py +566 -908
  63. unifi_network_maps/render/svg_icons.py +231 -0
  64. unifi_network_maps/render/svg_isometric.py +1196 -0
  65. unifi_network_maps/render/svg_labels.py +184 -0
  66. unifi_network_maps/render/svg_theme.py +166 -32
  67. unifi_network_maps/render/theme.py +86 -1
  68. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/METADATA +107 -31
  69. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/RECORD +73 -31
  70. unifi_network_maps/assets/icons/isometric/printer.svg +0 -122
  71. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/WHEEL +0 -0
  72. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/entry_points.txt +0 -0
  73. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/licenses/LICENSE +0 -0
  74. {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,231 @@
1
+ """Icon loading and color management for SVG rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from pathlib import Path
7
+
8
+ from .svg_theme import SvgTheme
9
+
10
+ # Icon file mappings per icon set
11
+ # Isometric set uses existing icons from root and isometric/ directories
12
+ # New device types fall back to generic icons in isometric set
13
+ _ICON_FILES_ISOMETRIC = {
14
+ "gateway": "router-network.svg",
15
+ "switch": "server-network.svg",
16
+ "ap": "access-point.svg",
17
+ "camera": "laptop.svg",
18
+ "tv": "laptop.svg",
19
+ "phone": "laptop.svg",
20
+ "printer": "laptop.svg",
21
+ "nas": "server.svg",
22
+ "speaker": "laptop.svg",
23
+ "game_console": "laptop.svg",
24
+ "iot": "server.svg",
25
+ "client": "laptop.svg",
26
+ "client_cluster": "laptop.svg",
27
+ "other": "server.svg",
28
+ }
29
+
30
+ _ISO_ICON_FILES_ISOMETRIC = {
31
+ "gateway": "router.svg",
32
+ "switch": "switch-module.svg",
33
+ "ap": "tower.svg",
34
+ "camera": "laptop.svg",
35
+ "tv": "laptop.svg",
36
+ "phone": "laptop.svg",
37
+ "printer": "laptop.svg",
38
+ "nas": "server.svg",
39
+ "speaker": "laptop.svg",
40
+ "game_console": "laptop.svg",
41
+ "iot": "server.svg",
42
+ "client": "laptop.svg",
43
+ "client_cluster": "laptop.svg",
44
+ "other": "server.svg",
45
+ }
46
+
47
+ # Modern set uses consistent naming in modern/ directory
48
+ _ICON_FILES_MODERN = {
49
+ "gateway": "gateway.svg",
50
+ "switch": "switch.svg",
51
+ "ap": "ap.svg",
52
+ "camera": "camera.svg",
53
+ "tv": "tv.svg",
54
+ "phone": "phone.svg",
55
+ "printer": "printer.svg",
56
+ "nas": "nas.svg",
57
+ "speaker": "speaker.svg",
58
+ "game_console": "game_console.svg",
59
+ "iot": "iot.svg",
60
+ "client": "client.svg",
61
+ "client_cluster": "client.svg",
62
+ "other": "other.svg",
63
+ }
64
+
65
+ # Icon set registry: maps set names to (flat_dir, iso_dir, flat_files, iso_files)
66
+ _ICON_SETS = {
67
+ "isometric": (
68
+ "", # Flat icons in root icons/ directory
69
+ "isometric", # Isometric icons in isometric/ subdirectory
70
+ _ICON_FILES_ISOMETRIC,
71
+ _ISO_ICON_FILES_ISOMETRIC,
72
+ ),
73
+ "modern": (
74
+ "modern-flat", # Flat icons for orthogonal SVG
75
+ "modern", # Isometric icons for iso SVG
76
+ _ICON_FILES_MODERN,
77
+ _ICON_FILES_MODERN,
78
+ ),
79
+ }
80
+
81
+ # Node type fill/stroke colors for orthogonal rendering
82
+ _TYPE_COLORS = {
83
+ "gateway": ("#ffd199", "#f08a00"),
84
+ "switch": ("#bfe4ff", "#1c6dd0"),
85
+ "ap": ("#c4f2d4", "#1f9a50"),
86
+ "camera": ("#b3e5fc", "#0277bd"),
87
+ "tv": ("#d1c4e9", "#512da8"),
88
+ "phone": ("#c8e6c9", "#388e3c"),
89
+ "printer": ("#cfd8dc", "#546e7a"),
90
+ "nas": ("#ffe0b2", "#e65100"),
91
+ "speaker": ("#b2dfdb", "#00796b"),
92
+ "game_console": ("#e1bee7", "#7b1fa2"),
93
+ "iot": ("#b2ebf2", "#00838f"),
94
+ "client": ("#e4ccff", "#6b2fb4"),
95
+ "client_cluster": ("#d4b8ff", "#5a25a0"),
96
+ "other": ("#e3e3e3", "#7b7b7b"),
97
+ }
98
+
99
+ # Type ordering for layout sorting
100
+ _TYPE_ORDER = [
101
+ "gateway",
102
+ "switch",
103
+ "ap",
104
+ "camera",
105
+ "tv",
106
+ "phone",
107
+ "printer",
108
+ "nas",
109
+ "speaker",
110
+ "game_console",
111
+ "iot",
112
+ "client",
113
+ "client_cluster",
114
+ "other",
115
+ ]
116
+
117
+
118
+ def _darken_hex(color: str, factor: float = 0.35) -> str:
119
+ """Darken a hex color by *factor* (0..1). Returns 6-digit hex."""
120
+ c = color.lstrip("#")
121
+ if len(c) != 6:
122
+ return color
123
+ r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
124
+ m = 1.0 - factor
125
+ return f"#{int(r * m):02x}{int(g * m):02x}{int(b * m):02x}"
126
+
127
+
128
+ def _build_decal_colors(theme: SvgTheme, factor: float = 0.35) -> dict[str, str]:
129
+ """Derive per-type icon decal colors by darkening each node's gradient end."""
130
+ node_attrs = {
131
+ "gateway": theme.node_gateway,
132
+ "switch": theme.node_switch,
133
+ "ap": theme.node_ap,
134
+ "client": theme.node_client,
135
+ "other": theme.node_other,
136
+ "camera": theme.node_camera,
137
+ "tv": theme.node_tv,
138
+ "phone": theme.node_phone,
139
+ "printer": theme.node_printer,
140
+ "nas": theme.node_nas,
141
+ "speaker": theme.node_speaker,
142
+ "game_console": theme.node_game_console,
143
+ "iot": theme.node_iot,
144
+ "client_cluster": theme.node_client_cluster,
145
+ }
146
+ return {name: _darken_hex(pair[1], factor) for name, pair in node_attrs.items()}
147
+
148
+
149
+ def _load_icons(icon_set: str = "isometric", decal_color: str = "#1a1a1a") -> dict[str, str]:
150
+ """Load flat (non-isometric) icons for the specified icon set.
151
+
152
+ Falls back to isometric icons if the requested icon is not found in the set.
153
+ Modern icons use #DECAL0 as placeholder which gets replaced with decal_color.
154
+ """
155
+ base = Path(__file__).resolve().parents[1] / "assets" / "icons"
156
+ icons: dict[str, str] = {}
157
+
158
+ set_config = _ICON_SETS.get(icon_set, _ICON_SETS["isometric"])
159
+ subdir, _, file_map, _ = set_config
160
+
161
+ fallback_config = _ICON_SETS["isometric"]
162
+ fallback_subdir, _, fallback_files, _ = fallback_config
163
+
164
+ for node_type in _ICON_FILES_ISOMETRIC.keys():
165
+ filename = file_map.get(node_type)
166
+ if filename:
167
+ path = base / subdir / filename if subdir else base / filename
168
+ if path.exists():
169
+ data = path.read_text(encoding="utf-8")
170
+ data = data.replace("#DECAL0", decal_color)
171
+ encoded = base64.b64encode(data.encode("utf-8")).decode("ascii")
172
+ icons[node_type] = f"data:image/svg+xml;base64,{encoded}"
173
+ continue
174
+
175
+ fallback_filename = fallback_files.get(node_type)
176
+ if fallback_filename:
177
+ fallback_path = (
178
+ base / fallback_subdir / fallback_filename
179
+ if fallback_subdir
180
+ else base / fallback_filename
181
+ )
182
+ if fallback_path.exists():
183
+ data = fallback_path.read_bytes()
184
+ encoded = base64.b64encode(data).decode("ascii")
185
+ icons[node_type] = f"data:image/svg+xml;base64,{encoded}"
186
+
187
+ return icons
188
+
189
+
190
+ def _load_isometric_icons(
191
+ icon_set: str = "isometric",
192
+ decal_color: str = "#5A6878",
193
+ decal_colors: dict[str, str] | None = None,
194
+ ) -> dict[str, str]:
195
+ """Load isometric icons for the specified icon set.
196
+
197
+ Falls back to isometric icons if the requested icon is not found in the set.
198
+ Modern icons use #DECAL0 as placeholder which gets replaced with a per-type
199
+ color from *decal_colors* (falling back to *decal_color*).
200
+ """
201
+ base = Path(__file__).resolve().parents[1] / "assets" / "icons"
202
+ icons: dict[str, str] = {}
203
+
204
+ set_config = _ICON_SETS.get(icon_set, _ICON_SETS["isometric"])
205
+ _, iso_subdir, _, iso_file_map = set_config
206
+
207
+ fallback_config = _ICON_SETS["isometric"]
208
+ _, fallback_iso_subdir, _, fallback_iso_files = fallback_config
209
+
210
+ for node_type in _ISO_ICON_FILES_ISOMETRIC.keys():
211
+ filename = iso_file_map.get(node_type)
212
+ if filename:
213
+ path = base / iso_subdir / filename
214
+ if path.exists():
215
+ content = path.read_text(encoding="utf-8")
216
+ color = decal_colors.get(node_type, decal_color) if decal_colors else decal_color
217
+ content = content.replace("#DECAL0", color)
218
+ data = content.encode("utf-8")
219
+ encoded = base64.b64encode(data).decode("ascii")
220
+ icons[node_type] = f"data:image/svg+xml;base64,{encoded}"
221
+ continue
222
+
223
+ fallback_filename = fallback_iso_files.get(node_type)
224
+ if fallback_filename:
225
+ fallback_path = base / fallback_iso_subdir / fallback_filename
226
+ if fallback_path.exists():
227
+ data = fallback_path.read_bytes()
228
+ encoded = base64.b64encode(data).decode("ascii")
229
+ icons[node_type] = f"data:image/svg+xml;base64,{encoded}"
230
+
231
+ return icons