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.
- unifi_network_maps/__init__.py +1 -1
- unifi_network_maps/adapters/unifi.py +83 -101
- unifi_network_maps/assets/icons/modern/ap.svg +9 -0
- unifi_network_maps/assets/icons/modern/camera.svg +9 -0
- unifi_network_maps/assets/icons/modern/client.svg +9 -0
- unifi_network_maps/assets/icons/modern/game_console.svg +10 -0
- unifi_network_maps/assets/icons/modern/gateway.svg +17 -0
- unifi_network_maps/assets/icons/modern/iot.svg +9 -0
- unifi_network_maps/assets/icons/modern/nas.svg +9 -0
- unifi_network_maps/assets/icons/modern/other.svg +10 -0
- unifi_network_maps/assets/icons/modern/phone.svg +10 -0
- unifi_network_maps/assets/icons/modern/printer.svg +9 -0
- unifi_network_maps/assets/icons/modern/speaker.svg +10 -0
- unifi_network_maps/assets/icons/modern/switch.svg +10 -0
- unifi_network_maps/assets/icons/modern/tv.svg +10 -0
- unifi_network_maps/assets/icons/modern-flat/ap.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/camera.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/client.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/game_console.svg +6 -0
- unifi_network_maps/assets/icons/modern-flat/gateway.svg +13 -0
- unifi_network_maps/assets/icons/modern-flat/iot.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/nas.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/other.svg +6 -0
- unifi_network_maps/assets/icons/modern-flat/phone.svg +6 -0
- unifi_network_maps/assets/icons/modern-flat/printer.svg +5 -0
- unifi_network_maps/assets/icons/modern-flat/speaker.svg +6 -0
- unifi_network_maps/assets/icons/modern-flat/switch.svg +6 -0
- unifi_network_maps/assets/icons/modern-flat/tv.svg +6 -0
- unifi_network_maps/assets/themes/dark.yaml +53 -10
- unifi_network_maps/assets/themes/default.yaml +34 -0
- unifi_network_maps/assets/themes/minimal-dark.yaml +98 -0
- unifi_network_maps/assets/themes/minimal.yaml +92 -0
- unifi_network_maps/assets/themes/unifi-dark.yaml +97 -0
- unifi_network_maps/assets/themes/unifi.yaml +92 -0
- unifi_network_maps/cli/args.py +54 -0
- unifi_network_maps/cli/main.py +18 -7
- unifi_network_maps/cli/render.py +79 -27
- unifi_network_maps/cli/runtime.py +29 -15
- unifi_network_maps/io/debug.py +2 -1
- unifi_network_maps/io/export.py +19 -13
- unifi_network_maps/io/mock_data.py +5 -3
- unifi_network_maps/io/paths.py +5 -3
- unifi_network_maps/model/classify.py +199 -0
- unifi_network_maps/model/clients.py +271 -0
- unifi_network_maps/model/connection.py +37 -0
- unifi_network_maps/model/diff.py +544 -0
- unifi_network_maps/model/edges.py +558 -0
- unifi_network_maps/model/helpers.py +64 -0
- unifi_network_maps/model/lldp.py +20 -25
- unifi_network_maps/model/mock.py +110 -23
- unifi_network_maps/model/snapshot.py +294 -0
- unifi_network_maps/model/topology.py +143 -931
- unifi_network_maps/model/topology_coerce.py +339 -0
- unifi_network_maps/model/vlans.py +32 -46
- unifi_network_maps/model/wan.py +132 -0
- unifi_network_maps/render/device_ports_md.py +39 -97
- unifi_network_maps/render/device_summary.py +53 -0
- unifi_network_maps/render/lldp_md.py +29 -219
- unifi_network_maps/render/markdown_tables.py +8 -0
- unifi_network_maps/render/mermaid.py +11 -2
- unifi_network_maps/render/mkdocs.py +2 -1
- unifi_network_maps/render/svg.py +566 -908
- unifi_network_maps/render/svg_icons.py +231 -0
- unifi_network_maps/render/svg_isometric.py +1196 -0
- unifi_network_maps/render/svg_labels.py +184 -0
- unifi_network_maps/render/svg_theme.py +166 -32
- unifi_network_maps/render/theme.py +86 -1
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/METADATA +107 -31
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/RECORD +73 -31
- unifi_network_maps/assets/icons/isometric/printer.svg +0 -122
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.14.dist-info → unifi_network_maps-1.5.0.dist-info}/top_level.txt +0 -0
unifi_network_maps/model/mock.py
CHANGED
|
@@ -112,8 +112,12 @@ def _build_clients(
|
|
|
112
112
|
aps: list[dict[str, Any]],
|
|
113
113
|
) -> list[dict[str, Any]]:
|
|
114
114
|
clients = []
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
wired = _build_wired_clients(options.wired_client_count, state, core_switch, start_index=0)
|
|
116
|
+
wireless = _build_wireless_clients(
|
|
117
|
+
options.wireless_client_count, state, aps, start_index=len(wired)
|
|
118
|
+
)
|
|
119
|
+
clients.extend(wired)
|
|
120
|
+
clients.extend(wireless)
|
|
117
121
|
return clients
|
|
118
122
|
|
|
119
123
|
|
|
@@ -125,39 +129,62 @@ def _build_networks() -> list[dict[str, Any]]:
|
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
def _build_wired_clients(
|
|
128
|
-
count: int, state: _MockState, core_switch: dict[str, Any]
|
|
132
|
+
count: int, state: _MockState, core_switch: dict[str, Any], start_index: int = 0
|
|
129
133
|
) -> list[dict[str, Any]]:
|
|
130
134
|
clients = []
|
|
131
|
-
for
|
|
135
|
+
for i in range(max(0, count)):
|
|
132
136
|
port_idx = _next_core_port(state)
|
|
133
137
|
_add_port(core_switch, port_idx, poe_enabled=False, rng=state.rng)
|
|
134
|
-
name = _unique_client_name(state)
|
|
138
|
+
name = _unique_client_name(state, client_index=start_index + i)
|
|
139
|
+
# Assign VLANs to wired clients based on index
|
|
140
|
+
vlan_options = [1, 10, 20, 30, 100]
|
|
141
|
+
client_vlan = vlan_options[port_idx % len(vlan_options)]
|
|
135
142
|
clients.append(
|
|
136
143
|
{
|
|
137
144
|
"name": name,
|
|
138
145
|
"is_wired": True,
|
|
139
146
|
"sw_mac": core_switch["mac"],
|
|
140
147
|
"sw_port": port_idx,
|
|
148
|
+
"vlan": client_vlan,
|
|
141
149
|
}
|
|
142
150
|
)
|
|
143
151
|
return clients
|
|
144
152
|
|
|
145
153
|
|
|
146
154
|
def _build_wireless_clients(
|
|
147
|
-
count: int, state: _MockState, aps: list[dict[str, Any]]
|
|
155
|
+
count: int, state: _MockState, aps: list[dict[str, Any]], start_index: int = 0
|
|
148
156
|
) -> list[dict[str, Any]]:
|
|
149
157
|
if not aps:
|
|
150
158
|
return []
|
|
151
159
|
clients = []
|
|
160
|
+
# Wireless VLANs - typically IoT or guest networks
|
|
161
|
+
wireless_vlans = [10, 20]
|
|
162
|
+
# Wireless channels (2.4GHz and 5GHz)
|
|
163
|
+
channels = [1, 6, 11, 36, 44, 149]
|
|
152
164
|
for idx in range(max(0, count)):
|
|
153
165
|
ap = aps[idx % len(aps)]
|
|
154
|
-
name = _unique_client_name(state)
|
|
166
|
+
name = _unique_client_name(state, client_index=start_index + idx)
|
|
167
|
+
client_vlan = wireless_vlans[idx % len(wireless_vlans)]
|
|
168
|
+
channel = channels[idx % len(channels)]
|
|
169
|
+
# Generate realistic signal quality metrics
|
|
170
|
+
signal = state.rng.randint(-80, -40)
|
|
171
|
+
noise = state.rng.randint(-100, -90)
|
|
172
|
+
tx_rate = state.rng.choice([72, 144, 288, 433, 866, 1200])
|
|
173
|
+
rx_rate = state.rng.choice([72, 144, 288, 433, 866, 1200])
|
|
174
|
+
satisfaction = state.rng.randint(70, 100)
|
|
155
175
|
clients.append(
|
|
156
176
|
{
|
|
157
177
|
"name": name,
|
|
158
178
|
"is_wired": False,
|
|
159
179
|
"ap_mac": ap["mac"],
|
|
160
180
|
"ap_port": 1,
|
|
181
|
+
"vlan": client_vlan,
|
|
182
|
+
"channel": channel,
|
|
183
|
+
"signal": signal,
|
|
184
|
+
"noise": noise,
|
|
185
|
+
"tx_rate": tx_rate,
|
|
186
|
+
"rx_rate": rx_rate,
|
|
187
|
+
"satisfaction": satisfaction,
|
|
161
188
|
}
|
|
162
189
|
)
|
|
163
190
|
return clients
|
|
@@ -192,7 +219,7 @@ def _pick_version(state: _MockState, dev_type: str) -> str:
|
|
|
192
219
|
def _link_gateway_to_switch(
|
|
193
220
|
state: _MockState, gateway: dict[str, Any], core_switch: dict[str, Any]
|
|
194
221
|
) -> None:
|
|
195
|
-
_add_port(core_switch, 1, poe_enabled=False, rng=state.rng)
|
|
222
|
+
_add_port(core_switch, 1, poe_enabled=False, rng=state.rng, is_trunk=True)
|
|
196
223
|
_add_lldp_link(
|
|
197
224
|
gateway,
|
|
198
225
|
core_switch,
|
|
@@ -259,7 +286,15 @@ def _add_port(
|
|
|
259
286
|
*,
|
|
260
287
|
poe_enabled: bool,
|
|
261
288
|
rng: random.Random,
|
|
289
|
+
is_trunk: bool = False,
|
|
262
290
|
) -> None:
|
|
291
|
+
# Assign VLAN based on port index for variety
|
|
292
|
+
vlan_options = [1, 10, 20, 30, 100]
|
|
293
|
+
native_vlan = vlan_options[port_idx % len(vlan_options)]
|
|
294
|
+
tagged_vlans: list[int] = []
|
|
295
|
+
if is_trunk:
|
|
296
|
+
# Trunk ports carry multiple VLANs
|
|
297
|
+
tagged_vlans = [v for v in vlan_options if v != native_vlan]
|
|
263
298
|
device["port_table"].append(
|
|
264
299
|
{
|
|
265
300
|
"port_idx": port_idx,
|
|
@@ -273,6 +308,8 @@ def _add_port(
|
|
|
273
308
|
"poe_good": poe_enabled,
|
|
274
309
|
"poe_voltage": round(rng.uniform(44.0, 52.0), 1) if poe_enabled else 0.0,
|
|
275
310
|
"poe_current": round(rng.uniform(0.05, 0.12), 3) if poe_enabled else 0.0,
|
|
311
|
+
"native_vlan": native_vlan,
|
|
312
|
+
"tagged_vlans": tagged_vlans,
|
|
276
313
|
}
|
|
277
314
|
)
|
|
278
315
|
|
|
@@ -283,41 +320,91 @@ def _next_core_port(state: _MockState) -> int:
|
|
|
283
320
|
return port_idx
|
|
284
321
|
|
|
285
322
|
|
|
286
|
-
def _unique_name(state: _MockState, prefix: str) -> str:
|
|
323
|
+
def _unique_name(state: _MockState, prefix: str, max_attempts: int = 100) -> str:
|
|
287
324
|
name = prefix
|
|
288
|
-
|
|
289
|
-
name
|
|
325
|
+
for _ in range(max_attempts):
|
|
326
|
+
if name not in state.used_names:
|
|
327
|
+
state.used_names.add(name)
|
|
328
|
+
return name
|
|
329
|
+
name = f"{prefix} {state.rng.randint(2, 99)}"
|
|
330
|
+
# Fallback with random suffix
|
|
331
|
+
name = f"{prefix}_{state.rng.randint(1000, 9999)}"
|
|
290
332
|
state.used_names.add(name)
|
|
291
333
|
return name
|
|
292
334
|
|
|
293
335
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
336
|
+
# Device name templates for testing device categorization
|
|
337
|
+
# Ordered by category to ensure variety in mock data
|
|
338
|
+
_DEVICE_NAME_TEMPLATES = [
|
|
339
|
+
"Living Room TV", # tv
|
|
340
|
+
"Sonos One", # speaker
|
|
341
|
+
"Ring Doorbell", # camera
|
|
342
|
+
"HP LaserJet", # printer
|
|
343
|
+
"Synology NAS", # nas
|
|
344
|
+
"PlayStation 5", # game_console
|
|
345
|
+
"iPhone", # phone
|
|
346
|
+
"Hue Bridge", # iot
|
|
347
|
+
"Samsung Smart TV", # tv
|
|
348
|
+
"HomePod Mini", # speaker
|
|
349
|
+
"Front Door Camera", # camera
|
|
350
|
+
"Brother Printer", # printer
|
|
351
|
+
"Xbox Series X", # game_console
|
|
352
|
+
"Nest Thermostat", # iot
|
|
353
|
+
"Echo Dot", # speaker
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _unique_client_name(state: _MockState, client_index: int = 0, max_attempts: int = 100) -> str:
|
|
358
|
+
# Use device names for first N clients to ensure variety in mock output
|
|
359
|
+
# This guarantees different device types appear in smoketests
|
|
360
|
+
if client_index < len(_DEVICE_NAME_TEMPLATES):
|
|
361
|
+
name = _DEVICE_NAME_TEMPLATES[client_index]
|
|
362
|
+
if name not in state.used_names:
|
|
363
|
+
state.used_names.add(name)
|
|
364
|
+
return name
|
|
365
|
+
# Fall back to person names for additional clients
|
|
366
|
+
for _ in range(max_attempts):
|
|
297
367
|
name = state.fake.first_name()
|
|
368
|
+
if name not in state.used_names:
|
|
369
|
+
state.used_names.add(name)
|
|
370
|
+
return name
|
|
371
|
+
# Fallback with random suffix
|
|
372
|
+
name = f"Client_{state.rng.randint(1000, 9999)}"
|
|
298
373
|
state.used_names.add(name)
|
|
299
374
|
return name
|
|
300
375
|
|
|
301
376
|
|
|
302
|
-
def _unique_room(state: _MockState) -> str:
|
|
303
|
-
|
|
304
|
-
while room in state.used_rooms:
|
|
377
|
+
def _unique_room(state: _MockState, max_attempts: int = 100) -> str:
|
|
378
|
+
for _ in range(max_attempts):
|
|
305
379
|
room = state.fake.word().title()
|
|
380
|
+
if room not in state.used_rooms:
|
|
381
|
+
state.used_rooms.add(room)
|
|
382
|
+
return room
|
|
383
|
+
# Fallback with random suffix
|
|
384
|
+
room = f"Room_{state.rng.randint(1000, 9999)}"
|
|
306
385
|
state.used_rooms.add(room)
|
|
307
386
|
return room
|
|
308
387
|
|
|
309
388
|
|
|
310
|
-
def _unique_mac(state: _MockState) -> str:
|
|
311
|
-
|
|
312
|
-
while mac in state.used_macs:
|
|
389
|
+
def _unique_mac(state: _MockState, max_attempts: int = 1000) -> str:
|
|
390
|
+
for _ in range(max_attempts):
|
|
313
391
|
mac = state.fake.mac_address()
|
|
392
|
+
if mac not in state.used_macs:
|
|
393
|
+
state.used_macs.add(mac)
|
|
394
|
+
return mac
|
|
395
|
+
# Fallback - extremely unlikely to reach here
|
|
396
|
+
mac = f"99:{state.rng.randint(10, 99)}:{state.rng.randint(10, 99)}:{state.rng.randint(10, 99)}:{state.rng.randint(10, 99)}:{state.rng.randint(10, 99)}"
|
|
314
397
|
state.used_macs.add(mac)
|
|
315
398
|
return mac
|
|
316
399
|
|
|
317
400
|
|
|
318
|
-
def _unique_ip(state: _MockState) -> str:
|
|
319
|
-
|
|
320
|
-
while ip in state.used_ips:
|
|
401
|
+
def _unique_ip(state: _MockState, max_attempts: int = 1000) -> str:
|
|
402
|
+
for _ in range(max_attempts):
|
|
321
403
|
ip = state.fake.ipv4_private()
|
|
404
|
+
if ip not in state.used_ips:
|
|
405
|
+
state.used_ips.add(ip)
|
|
406
|
+
return ip
|
|
407
|
+
# Fallback - extremely unlikely to reach here
|
|
408
|
+
ip = f"10.{state.rng.randint(0, 255)}.{state.rng.randint(0, 255)}.{state.rng.randint(1, 254)}"
|
|
322
409
|
state.used_ips.add(ip)
|
|
323
410
|
return ip
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Serialization helpers for topology data structures.
|
|
2
|
+
|
|
3
|
+
Provides to_dict/from_dict conversions for persistence and transmission.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import fields, is_dataclass
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from .connection import ConnectionInfo
|
|
12
|
+
from .lldp import LLDPEntry
|
|
13
|
+
from .topology import Device, Edge, PortInfo, UplinkInfo, WanInfo, WanInterface
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _serialize_value(value: Any) -> Any:
|
|
19
|
+
"""Recursively serialize a value to JSON-compatible form."""
|
|
20
|
+
if value is None:
|
|
21
|
+
return None
|
|
22
|
+
if isinstance(value, str | int | float | bool):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, tuple):
|
|
25
|
+
return list(_serialize_value(v) for v in value)
|
|
26
|
+
if isinstance(value, list):
|
|
27
|
+
return [_serialize_value(v) for v in value]
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {k: _serialize_value(v) for k, v in value.items()}
|
|
30
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
31
|
+
return _dataclass_to_dict(value)
|
|
32
|
+
return str(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _dataclass_to_dict(obj: Any) -> dict[str, Any]:
|
|
36
|
+
"""Convert a dataclass instance to a dictionary."""
|
|
37
|
+
result: dict[str, Any] = {}
|
|
38
|
+
for field in fields(obj):
|
|
39
|
+
value = getattr(obj, field.name)
|
|
40
|
+
result[field.name] = _serialize_value(value)
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- PortInfo ---
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def port_info_to_dict(port: PortInfo) -> dict[str, Any]:
|
|
48
|
+
"""Serialize a PortInfo to a dictionary."""
|
|
49
|
+
return _dataclass_to_dict(port)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def port_info_from_dict(data: dict[str, Any]) -> PortInfo:
|
|
53
|
+
"""Deserialize a PortInfo from a dictionary."""
|
|
54
|
+
return PortInfo(
|
|
55
|
+
port_idx=data.get("port_idx"),
|
|
56
|
+
name=data.get("name"),
|
|
57
|
+
ifname=data.get("ifname"),
|
|
58
|
+
speed=data.get("speed"),
|
|
59
|
+
aggregation_group=data.get("aggregation_group"),
|
|
60
|
+
port_poe=data.get("port_poe", False),
|
|
61
|
+
poe_enable=data.get("poe_enable", False),
|
|
62
|
+
poe_good=data.get("poe_good", False),
|
|
63
|
+
poe_power=data.get("poe_power"),
|
|
64
|
+
native_vlan=data.get("native_vlan"),
|
|
65
|
+
tagged_vlans=tuple(data.get("tagged_vlans", [])),
|
|
66
|
+
wan_networkconf_id=data.get("wan_networkconf_id"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- UplinkInfo ---
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def uplink_info_to_dict(uplink: UplinkInfo) -> dict[str, Any]:
|
|
74
|
+
"""Serialize an UplinkInfo to a dictionary."""
|
|
75
|
+
return _dataclass_to_dict(uplink)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def uplink_info_from_dict(data: dict[str, Any]) -> UplinkInfo:
|
|
79
|
+
"""Deserialize an UplinkInfo from a dictionary."""
|
|
80
|
+
return UplinkInfo(
|
|
81
|
+
mac=data.get("mac"),
|
|
82
|
+
name=data.get("name"),
|
|
83
|
+
port=data.get("port"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- LLDPEntry ---
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def lldp_entry_to_dict(entry: LLDPEntry) -> dict[str, Any]:
|
|
91
|
+
"""Serialize an LLDPEntry to a dictionary."""
|
|
92
|
+
return _dataclass_to_dict(entry)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def lldp_entry_from_dict(data: dict[str, Any]) -> LLDPEntry:
|
|
96
|
+
"""Deserialize an LLDPEntry from a dictionary."""
|
|
97
|
+
return LLDPEntry(
|
|
98
|
+
chassis_id=data.get("chassis_id", ""),
|
|
99
|
+
port_id=data.get("port_id", ""),
|
|
100
|
+
port_desc=data.get("port_desc"),
|
|
101
|
+
local_port_name=data.get("local_port_name"),
|
|
102
|
+
local_port_idx=data.get("local_port_idx"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# --- WanInterface ---
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def wan_interface_to_dict(wan: WanInterface) -> dict[str, Any]:
|
|
110
|
+
"""Serialize a WanInterface to a dictionary."""
|
|
111
|
+
return _dataclass_to_dict(wan)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def wan_interface_from_dict(data: dict[str, Any]) -> WanInterface:
|
|
115
|
+
"""Deserialize a WanInterface from a dictionary."""
|
|
116
|
+
return WanInterface(
|
|
117
|
+
port_idx=data.get("port_idx", 0),
|
|
118
|
+
link_speed=data.get("link_speed"),
|
|
119
|
+
ip_address=data.get("ip_address"),
|
|
120
|
+
enabled=data.get("enabled", False),
|
|
121
|
+
label=data.get("label"),
|
|
122
|
+
isp_speed=data.get("isp_speed"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --- WanInfo ---
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def wan_info_to_dict(wan_info: WanInfo) -> dict[str, Any]:
|
|
130
|
+
"""Serialize a WanInfo to a dictionary."""
|
|
131
|
+
result: dict[str, Any] = {}
|
|
132
|
+
if wan_info.wan1:
|
|
133
|
+
result["wan1"] = wan_interface_to_dict(wan_info.wan1)
|
|
134
|
+
else:
|
|
135
|
+
result["wan1"] = None
|
|
136
|
+
if wan_info.wan2:
|
|
137
|
+
result["wan2"] = wan_interface_to_dict(wan_info.wan2)
|
|
138
|
+
else:
|
|
139
|
+
result["wan2"] = None
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def wan_info_from_dict(data: dict[str, Any]) -> WanInfo:
|
|
144
|
+
"""Deserialize a WanInfo from a dictionary."""
|
|
145
|
+
wan1 = None
|
|
146
|
+
wan2 = None
|
|
147
|
+
if data.get("wan1"):
|
|
148
|
+
wan1 = wan_interface_from_dict(data["wan1"])
|
|
149
|
+
if data.get("wan2"):
|
|
150
|
+
wan2 = wan_interface_from_dict(data["wan2"])
|
|
151
|
+
return WanInfo(wan1=wan1, wan2=wan2)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --- Device ---
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def device_to_dict(device: Device) -> dict[str, Any]:
|
|
158
|
+
"""Serialize a Device to a dictionary."""
|
|
159
|
+
return {
|
|
160
|
+
"name": device.name,
|
|
161
|
+
"model_name": device.model_name,
|
|
162
|
+
"model": device.model,
|
|
163
|
+
"mac": device.mac,
|
|
164
|
+
"ip": device.ip,
|
|
165
|
+
"type": device.type,
|
|
166
|
+
"lldp_info": [lldp_entry_to_dict(e) for e in device.lldp_info],
|
|
167
|
+
"port_table": [port_info_to_dict(p) for p in device.port_table],
|
|
168
|
+
"poe_ports": {str(k): v for k, v in device.poe_ports.items()},
|
|
169
|
+
"uplink": uplink_info_to_dict(device.uplink) if device.uplink else None,
|
|
170
|
+
"last_uplink": uplink_info_to_dict(device.last_uplink) if device.last_uplink else None,
|
|
171
|
+
"version": device.version,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def device_from_dict(data: dict[str, Any]) -> Device:
|
|
176
|
+
"""Deserialize a Device from a dictionary."""
|
|
177
|
+
lldp_info = [lldp_entry_from_dict(e) for e in data.get("lldp_info", [])]
|
|
178
|
+
port_table = [port_info_from_dict(p) for p in data.get("port_table", [])]
|
|
179
|
+
poe_ports = {int(k): v for k, v in data.get("poe_ports", {}).items()}
|
|
180
|
+
uplink = uplink_info_from_dict(data["uplink"]) if data.get("uplink") else None
|
|
181
|
+
last_uplink = uplink_info_from_dict(data["last_uplink"]) if data.get("last_uplink") else None
|
|
182
|
+
return Device(
|
|
183
|
+
name=data.get("name", ""),
|
|
184
|
+
model_name=data.get("model_name", ""),
|
|
185
|
+
model=data.get("model", ""),
|
|
186
|
+
mac=data.get("mac", ""),
|
|
187
|
+
ip=data.get("ip", ""),
|
|
188
|
+
type=data.get("type", ""),
|
|
189
|
+
lldp_info=lldp_info,
|
|
190
|
+
port_table=port_table,
|
|
191
|
+
poe_ports=poe_ports,
|
|
192
|
+
uplink=uplink,
|
|
193
|
+
last_uplink=last_uplink,
|
|
194
|
+
version=data.get("version", ""),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# --- ConnectionInfo ---
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def connection_info_to_dict(conn: ConnectionInfo) -> dict[str, Any]:
|
|
202
|
+
"""Serialize a ConnectionInfo to a dictionary."""
|
|
203
|
+
return _dataclass_to_dict(conn)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def connection_info_from_dict(data: dict[str, Any]) -> ConnectionInfo:
|
|
207
|
+
"""Deserialize a ConnectionInfo from a dictionary."""
|
|
208
|
+
return ConnectionInfo(
|
|
209
|
+
signal_dbm=data.get("signal_dbm"),
|
|
210
|
+
noise_dbm=data.get("noise_dbm"),
|
|
211
|
+
tx_rate_mbps=data.get("tx_rate_mbps"),
|
|
212
|
+
rx_rate_mbps=data.get("rx_rate_mbps"),
|
|
213
|
+
satisfaction=data.get("satisfaction"),
|
|
214
|
+
quality=data.get("quality"),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --- Edge ---
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def edge_to_dict(edge: Edge) -> dict[str, Any]:
|
|
222
|
+
"""Serialize an Edge to a dictionary."""
|
|
223
|
+
return {
|
|
224
|
+
"left": edge.left,
|
|
225
|
+
"right": edge.right,
|
|
226
|
+
"label": edge.label,
|
|
227
|
+
"poe": edge.poe,
|
|
228
|
+
"wireless": edge.wireless,
|
|
229
|
+
"speed": edge.speed,
|
|
230
|
+
"channel": edge.channel,
|
|
231
|
+
"vlans": list(edge.vlans),
|
|
232
|
+
"active_vlans": list(edge.active_vlans),
|
|
233
|
+
"is_trunk": edge.is_trunk,
|
|
234
|
+
"connection": connection_info_to_dict(edge.connection) if edge.connection else None,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def edge_from_dict(data: dict[str, Any]) -> Edge:
|
|
239
|
+
"""Deserialize an Edge from a dictionary."""
|
|
240
|
+
connection = None
|
|
241
|
+
if data.get("connection"):
|
|
242
|
+
connection = connection_info_from_dict(data["connection"])
|
|
243
|
+
return Edge(
|
|
244
|
+
left=data.get("left", ""),
|
|
245
|
+
right=data.get("right", ""),
|
|
246
|
+
label=data.get("label"),
|
|
247
|
+
poe=data.get("poe", False),
|
|
248
|
+
wireless=data.get("wireless", False),
|
|
249
|
+
speed=data.get("speed"),
|
|
250
|
+
channel=data.get("channel"),
|
|
251
|
+
vlans=tuple(data.get("vlans", [])),
|
|
252
|
+
active_vlans=tuple(data.get("active_vlans", [])),
|
|
253
|
+
is_trunk=data.get("is_trunk", False),
|
|
254
|
+
connection=connection,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --- Client (dict-based, since clients come from API as dicts) ---
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def client_to_dict(client: dict[str, Any]) -> dict[str, Any]:
|
|
262
|
+
"""Serialize a client dict, keeping only relevant fields."""
|
|
263
|
+
relevant_keys = {
|
|
264
|
+
"mac",
|
|
265
|
+
"name",
|
|
266
|
+
"hostname",
|
|
267
|
+
"ip",
|
|
268
|
+
"vlan",
|
|
269
|
+
"vlan_id",
|
|
270
|
+
"is_wired",
|
|
271
|
+
"is_unifi",
|
|
272
|
+
"is_unifi_device",
|
|
273
|
+
"ap_mac",
|
|
274
|
+
"sw_mac",
|
|
275
|
+
"uplink_mac",
|
|
276
|
+
"uplink_device_mac",
|
|
277
|
+
"sw_port",
|
|
278
|
+
"uplink_remote_port",
|
|
279
|
+
"channel",
|
|
280
|
+
"signal",
|
|
281
|
+
"noise",
|
|
282
|
+
"tx_rate",
|
|
283
|
+
"rx_rate",
|
|
284
|
+
"satisfaction",
|
|
285
|
+
"oui",
|
|
286
|
+
"vendor",
|
|
287
|
+
"unifi_device_info_from_ucore",
|
|
288
|
+
}
|
|
289
|
+
return {k: v for k, v in client.items() if k in relevant_keys}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def client_from_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
293
|
+
"""Deserialize a client dict (identity for now, but validates structure)."""
|
|
294
|
+
return dict(data)
|