1616from ..models .firewall_rule import FirewallRule
1717from ..models .qos_rule import QoSRule
1818from ..models .vpn_config import VPNConfig
19+ from ..models .network import NetworkConf
1920from ..models .port_profile import PortProfile
2021from ..const import DOMAIN
2122
@@ -36,9 +37,6 @@ def get_rule_enabled(rule: Any) -> bool:
3637 # Check different rule types and return appropriate enabled status
3738 if isinstance (rule , (PortForward , TrafficRoute , FirewallPolicy , TrafficRule , Wlan , QoSRule , VPNConfig )):
3839 return getattr (rule , "enabled" , False )
39- # Port Profile enabled state (computed)
40- if isinstance (rule , PortProfile ):
41- return rule .enabled
4240
4341 # Special handling for Device LED state
4442 if isinstance (rule , Device ):
@@ -50,6 +48,14 @@ def get_rule_enabled(rule: Any) -> bool:
5048 return led_state != 'off' # True if not explicitly turned off
5149 return True # Default to enabled if no override info
5250
51+ # Networks enabled (corporate LAN typically has 'enabled')
52+ if isinstance (rule , NetworkConf ):
53+ return rule .enabled
54+
55+ # Port profile enabled state
56+ if isinstance (rule , PortProfile ):
57+ return rule .enabled
58+
5359 # For dictionaries, try common enabled attributes
5460 if isinstance (rule , dict ):
5561 return rule .get ("enabled" , False )
@@ -158,7 +164,15 @@ def get_rule_id(rule: Any) -> str | None:
158164 LOGGER .warning ("Device without mac attribute: %s" , rule )
159165 return None
160166
161- # Port Profile unique id
167+ # Handle NetworkConf
168+ if isinstance (rule , NetworkConf ):
169+ if rule .id :
170+ return f"unr_network_{ rule .id } "
171+ else :
172+ LOGGER .warning ("NetworkConf without id attribute: %s" , rule )
173+ return None
174+
175+ # Handle PortProfile
162176 if isinstance (rule , PortProfile ):
163177 if rule .id :
164178 return f"unr_port_profile_{ rule .id } "
@@ -199,7 +213,10 @@ def get_rule_prefix(rule_type: str) -> str:
199213 "qos_rules" : "QoS" ,
200214 "wlans" : "WLAN" ,
201215 "devices" : "Device" ,
202- "port_profiles" : "Port Profile"
216+ "port_profiles" : "Port Profile" ,
217+ # For networks we return an empty prefix because the descriptive
218+ # name will already include the desired label (e.g., WAN1, VLAN 1).
219+ "networks" : ""
203220 }
204221
205222 return rule_types .get (rule_type , "Rule" )
@@ -345,13 +362,6 @@ def extract_descriptive_name(rule: Any, coordinator=None) -> str | None:
345362 return f"{ vpn_type } VPN"
346363
347364 return None
348-
349- elif isinstance (rule , PortProfile ):
350- # For port profiles, prefer the name
351- name = rule .name
352- if name :
353- return name
354- return None
355365
356366 elif isinstance (rule , Device ):
357367 # For devices, return the device name for LED switches
@@ -366,6 +376,41 @@ def extract_descriptive_name(rule: Any, coordinator=None) -> str | None:
366376 elif isinstance (rule , dict ):
367377 # For dictionaries, try common name attributes
368378 return rule .get ("name" ) or rule .get ("description" )
379+
380+ elif isinstance (rule , NetworkConf ):
381+ # Build specialized names for networks:
382+ # - WAN: "WAN<idx> <name>" when attr_hidden_id starts with WAN or purpose==wan
383+ # - LAN/Corporate: "VLAN <vlan_id> <name>" when vlan_enabled and vlan id available
384+ # - Special case: name exactly "WAN Magic" becomes "UniFi WAN Magic"
385+ raw = getattr (rule , "raw" , {}) if hasattr (rule , "raw" ) else {}
386+ name = raw .get ("name" ) or rule .name
387+ hidden_id = raw .get ("attr_hidden_id" , "" ) or ""
388+ purpose = raw .get ("purpose" , "" ) or ""
389+
390+ # Special case first
391+ if name == "WAN Magic" :
392+ return "UniFi WAN Magic"
393+
394+ # WAN naming
395+ if purpose == "wan" or (isinstance (hidden_id , str ) and hidden_id .upper ().startswith ("WAN" )):
396+ # Extract index from WAN/WAN2/WAN3 ... when present
397+ suffix = ""
398+ if isinstance (hidden_id , str ) and len (hidden_id ) > 3 and hidden_id .upper ().startswith ("WAN" ):
399+ suffix = hidden_id [3 :] # characters after WAN
400+ wan_label = f"WAN{ suffix } " if suffix else "WAN"
401+ return f"{ wan_label } { name } " .strip ()
402+
403+ # LAN/VLAN naming
404+ vlan_id = raw .get ("vlan" ) or raw .get ("vlan_id" )
405+ if raw .get ("vlan_enabled" ) and vlan_id is not None :
406+ return f"VLAN { vlan_id } { name } " .strip ()
407+
408+ # Default LAN (no VLAN) naming
409+ if (purpose == "corporate" or (isinstance (hidden_id , str ) and hidden_id .upper () == "LAN" )) and not raw .get ("vlan_enabled" ):
410+ return f"LAN { name } " .strip ()
411+
412+ # Default: return name as-is
413+ return name
369414
370415 # For other types, try common attributes
371416 if hasattr (rule , "name" ):
@@ -394,6 +439,25 @@ def get_rule_name(rule: Any, coordinator=None) -> str | None:
394439 rule_type = "qos_rules"
395440 elif isinstance (rule , Device ):
396441 rule_type = "devices"
442+ elif isinstance (rule , NetworkConf ):
443+ # Decide if this network should be exposed as a switch entity.
444+ # Omit VPN networks since we already have VPN switches.
445+ raw = getattr (rule , "raw" , {}) if hasattr (rule , "raw" ) else {}
446+ purpose = str (raw .get ("purpose" , "" )).lower ()
447+ vpn_type = str (raw .get ("vpn_type" , "" )).lower ()
448+ is_vpn = (
449+ purpose .startswith ("vpn" )
450+ or purpose in {"remote-user-vpn" , "vpn-client" , "vpn-server" }
451+ or "vpn" in vpn_type
452+ or "wireguard" in vpn_type
453+ or "openvpn" in vpn_type
454+ )
455+ if is_vpn :
456+ rule_type = None # signal to caller there is no switch type
457+ else :
458+ rule_type = "networks"
459+ elif isinstance (rule , PortProfile ):
460+ rule_type = "port_profiles"
397461 elif isinstance (rule , dict ) and "type" in rule :
398462 rule_type = rule .get ("type" )
399463
@@ -602,4 +666,31 @@ def is_our_entity(entity_entry, domain=DOMAIN) -> bool:
602666 """
603667 # Check if entity's platform matches our domain
604668 # This property cannot be changed by users
605- return entity_entry .platform == domain
669+ return entity_entry .platform == domain
670+
671+
672+ # --- Network helpers ---
673+ def is_vpn_network (network : Any ) -> bool :
674+ """Return True if a network (dict or NetworkConf) represents a VPN entity.
675+
676+ Detects both purpose values and vpn_type variants (OpenVPN/WireGuard).
677+ """
678+ raw = getattr (network , "raw" , {}) if hasattr (network , "raw" ) else (network if isinstance (network , dict ) else {})
679+ purpose = str (raw .get ("purpose" , "" )).lower ()
680+ vpn_type = str (raw .get ("vpn_type" , "" )).lower ()
681+ return (
682+ purpose .startswith ("vpn" )
683+ or purpose in {"remote-user-vpn" , "vpn-client" , "vpn-server" }
684+ or "vpn" in vpn_type
685+ or "wireguard" in vpn_type
686+ or "openvpn" in vpn_type
687+ )
688+
689+
690+ def filter_switchable_networks (networks : list [Any ]) -> list [Any ]:
691+ """Filter out VPN networks; keep networks suitable for switch entities."""
692+ try :
693+ return [n for n in networks if not is_vpn_network (n )]
694+ except Exception :
695+ # Fail-safe: if anything goes wrong, return original list
696+ return networks
0 commit comments