feat: Fingerprint-Attribute für Discovery-Scoring

Ergänzt DRIVER_NAME, HTTP_FINGERPRINT, SNMP_FINGERPRINT, SSH_FINGERPRINT,
PORT_SPECS und SNMP_OBJECT_ID_PREFIX gemäß docs/DISCOVERY_FINGERPRINTING.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-24 14:47:08 +02:00
co-authored by Claude Sonnet 4.6
parent 13e1aba5aa
commit 23e68ecd87
+75 -68
View File
@@ -11,7 +11,7 @@ import json
import os import os
import re import re
import socket import socket
from typing import Any, Dict, List, Optional from typing import Any
from urllib.parse import urljoin from urllib.parse import urljoin
import requests import requests
@@ -22,7 +22,7 @@ from cryptography.hazmat.backends import default_backend
from napalm.base.exceptions import ConnectionClosedException, ConnectionException from napalm.base.exceptions import ConnectionClosedException, ConnectionException
from napalm_device_types import ResidentialGatewayDriver from napalm_device_types import FingerprintRule, ResidentialGatewayDriver
from napalm_device_types.models import ( from napalm_device_types.models import (
DeviceActionResultDict, DeviceActionResultDict,
HostDict, HostDict,
@@ -90,6 +90,13 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
"""NAPALM driver for Zyxel VMG residential gateway devices via DAL protocol.""" """NAPALM driver for Zyxel VMG residential gateway devices via DAL protocol."""
VENDOR = "Zyxel" VENDOR = "Zyxel"
DRIVER_NAME = "zyxel"
SNMP_OBJECT_ID_PREFIX = "1.3.6.1.4.1.890"
HTTP_FINGERPRINT = [
FingerprintRule("zyxel", weight=7.0, mandatory=True),
FingerprintRule("gs1920", weight=6.0),
FingerprintRule("gs2210", weight=6.0),
]
_LOGIN_PATH = "UserLogin" _LOGIN_PATH = "UserLogin"
_LOGOUT_PATH = "cgi-bin/UserLogout" _LOGOUT_PATH = "cgi-bin/UserLogout"
@@ -103,7 +110,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
username: str, username: str,
password: str, password: str,
timeout: int = 60, timeout: int = 60,
optional_args: Optional[Dict[str, Any]] = None, optional_args: dict[str, Any] | None = None,
) -> None: ) -> None:
self.hostname = hostname self.hostname = hostname
self.username = username self.username = username
@@ -117,9 +124,9 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
self.port = int(self.optional_args.get("port") or (443 if self.use_tls else 80)) self.port = int(self.optional_args.get("port") or (443 if self.use_tls else 80))
self.verify = bool(self.optional_args.get("verify", False)) self.verify = bool(self.optional_args.get("verify", False))
self._session: Optional[requests.Session] = None self._session: requests.Session | None = None
self._sessionkey: Optional[str] = None self._sessionkey: str | None = None
self._crypto: Optional[ZyxelCrypto] = None self._crypto: ZyxelCrypto | None = None
self._base_url: str = ( self._base_url: str = (
f"{'https' if self.use_tls else 'http'}://{self.hostname}:{self.port}" f"{'https' if self.use_tls else 'http'}://{self.hostname}:{self.port}"
) )
@@ -194,7 +201,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
self._crypto = None self._crypto = None
self._device_info = None self._device_info = None
def is_alive(self) -> Dict[str, bool]: def is_alive(self) -> dict[str, bool]:
"""Check if the device is reachable via TCP connection.""" """Check if the device is reachable via TCP connection."""
try: try:
sock = socket.create_connection((self.hostname, self.port), timeout=5) sock = socket.create_connection((self.hostname, self.port), timeout=5)
@@ -203,7 +210,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
except OSError: except OSError:
return {"is_alive": False} return {"is_alive": False}
def _get_basic_info(self) -> Optional[dict]: def _get_basic_info(self) -> dict | None:
if self._session is None: if self._session is None:
return None return None
try: try:
@@ -213,7 +220,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
except (requests.RequestException, json.JSONDecodeError, TypeError): except (requests.RequestException, json.JSONDecodeError, TypeError):
return None return None
def _get_rsa_pubkey(self) -> Optional[str]: def _get_rsa_pubkey(self) -> str | None:
if self._session is None: if self._session is None:
return None return None
try: try:
@@ -223,7 +230,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
except (requests.RequestException, json.JSONDecodeError, TypeError): except (requests.RequestException, json.JSONDecodeError, TypeError):
return None return None
def _dal_query(self, oid: str, data: Optional[dict] = None) -> Optional[dict]: def _dal_query(self, oid: str, data: dict | None = None) -> dict | None:
if self._session is None or self._sessionkey is None or self._crypto is None: if self._session is None or self._sessionkey is None or self._crypto is None:
raise ConnectionClosedException("Not connected") raise ConnectionClosedException("Not connected")
if data is None: if data is None:
@@ -240,7 +247,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
except Exception: except Exception:
return None return None
def _fetch_page(self, path: str) -> Optional[str]: def _fetch_page(self, path: str) -> str | None:
if self._session is None or self._sessionkey is None: if self._session is None or self._sessionkey is None:
raise ConnectionClosedException("Not connected") raise ConnectionClosedException("Not connected")
try: try:
@@ -253,7 +260,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
except requests.RequestException: except requests.RequestException:
return None return None
def get_facts(self) -> Dict[str, Any]: def get_facts(self) -> dict[str, Any]:
"""Return device facts such as model, serial, OS version, and uptime.""" """Return device facts such as model, serial, OS version, and uptime."""
info = self._device_info or {} info = self._device_info or {}
try: try:
@@ -271,9 +278,9 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
"interface_list": iface_list, "interface_list": iface_list,
} }
def get_interfaces(self) -> Dict[str, Dict[str, Any]]: def get_interfaces(self) -> dict[str, dict[str, Any]]:
"""Return WAN, LAN, and WiFi interfaces from device info.""" """Return WAN, LAN, and WiFi interfaces from device info."""
ifaces: Dict[str, Dict[str, Any]] = {} ifaces: dict[str, dict[str, Any]] = {}
info = self._device_info or {} info = self._device_info or {}
wan_ip = info.get("WanIp", "") wan_ip = info.get("WanIp", "")
mac = info.get("MacAddress", "") mac = info.get("MacAddress", "")
@@ -308,20 +315,20 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
} }
return ifaces return ifaces
def get_interfaces_ip(self) -> Dict[str, Dict[str, Any]]: def get_interfaces_ip(self) -> dict[str, dict[str, Any]]:
"""Return IP addresses of WAN and LAN interfaces.""" """Return IP addresses of WAN and LAN interfaces."""
info = self._device_info or {} info = self._device_info or {}
wan_ip = info.get("WanIp", "") wan_ip = info.get("WanIp", "")
lan_ip = info.get("LanIp", self.hostname) lan_ip = info.get("LanIp", self.hostname)
result: Dict[str, Dict[str, Any]] = {} result: dict[str, dict[str, Any]] = {}
if wan_ip: if wan_ip:
result["wan"] = {"ipv4": {wan_ip: {"prefix_length": 24}}, "ipv6": {}} result["wan"] = {"ipv4": {wan_ip: {"prefix_length": 24}}, "ipv6": {}}
result["lan"] = {"ipv4": {lan_ip: {"prefix_length": 24}}, "ipv6": {}} result["lan"] = {"ipv4": {lan_ip: {"prefix_length": 24}}, "ipv6": {}}
return result return result
def get_arp_table(self, vrf: str = "") -> List[Dict[str, Any]]: def get_arp_table(self, vrf: str = "") -> list[dict[str, Any]]:
"""Return the ARP table derived from DHCP host entries.""" """Return the ARP table derived from DHCP host entries."""
arp: List[Dict[str, Any]] = [] arp: list[dict[str, Any]] = []
for host in self.get_hosts(): for host in self.get_hosts():
if not host["is_active"] or not host["mac"] or not host["ip"]: if not host["is_active"] or not host["mac"] or not host["ip"]:
continue continue
@@ -333,11 +340,11 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
}) })
return arp return arp
def get_lldp_neighbors(self) -> Dict[str, Any]: def get_lldp_neighbors(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_lldp_neighbors_detail(self, interface: str = "") -> Dict[str, Any]: def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
@@ -347,27 +354,27 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
full: bool = False, full: bool = False,
sanitized: bool = False, sanitized: bool = False,
format: str = "text", format: str = "text",
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_environment(self) -> Dict[str, Any]: def get_environment(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_optics(self) -> Dict[str, Any]: def get_optics(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_ntp_peers(self) -> Dict[str, Optional[str]]: def get_ntp_peers(self) -> dict[str, str | None]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_ntp_servers(self) -> Dict[str, Optional[str]]: def get_ntp_servers(self) -> dict[str, str | None]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_ntp_stats(self) -> List[Dict[str, Any]]: def get_ntp_stats(self) -> list[dict[str, Any]]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return [] return []
@@ -381,7 +388,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
count: int = 5, count: int = 5,
vrf: str = "", vrf: str = "",
source_interface: str = "", source_interface: str = "",
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Ping a destination via the device's DAL diagnostics.""" """Ping a destination via the device's DAL diagnostics."""
result = self._dal_query("PINGTEST", { result = self._dal_query("PINGTEST", {
"ProtocolVersion": "IPv4", "ProtocolVersion": "IPv4",
@@ -409,7 +416,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
ttl: int = 255, ttl: int = 255,
timeout: int = 2, timeout: int = 2,
vrf: str = "", vrf: str = "",
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {"success": {}, "error": "Not supported on Zyxel VMG"} return {"success": {}, "error": "Not supported on Zyxel VMG"}
@@ -418,55 +425,55 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
destination: str = "", destination: str = "",
protocol: str = "", protocol: str = "",
longer: bool = False, longer: bool = False,
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_snmp_information(self) -> Dict[str, Any]: def get_snmp_information(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_users(self) -> Dict[str, Any]: def get_users(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_network_instances(self, name: str = "") -> Dict[str, Any]: def get_network_instances(self, name: str = "") -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_bgp_config(self, group: str = "", neighbor: str = "") -> Dict[str, Any]: def get_bgp_config(self, group: str = "", neighbor: str = "") -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_bgp_neighbors(self) -> Dict[str, Any]: def get_bgp_neighbors(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_bgp_neighbors_detail(self, neighbor_address: str = "") -> Dict[str, Any]: def get_bgp_neighbors_detail(self, neighbor_address: str = "") -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_probes_config(self) -> Dict[str, Any]: def get_probes_config(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_probes_results(self) -> Dict[str, Any]: def get_probes_results(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_mac_address_table(self) -> List[Dict[str, Any]]: def get_mac_address_table(self) -> list[dict[str, Any]]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return [] return []
def get_firewall_policies(self) -> Dict[str, Any]: def get_firewall_policies(self) -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def cli(self, commands: List[str], encoding: str = "text") -> Dict[str, Any]: def cli(self, commands: list[str], encoding: str = "text") -> dict[str, Any]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None: def commit_config(self, message: str = "", revert_in: int | None = None) -> None:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
pass pass
@@ -475,13 +482,13 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
pass pass
def load_replace_candidate( def load_replace_candidate(
self, filename: Optional[str] = None, config: Optional[str] = None self, filename: str | None = None, config: str | None = None
) -> None: ) -> None:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
raise NotImplementedError("Zyxel VMG does not support config replacement via NAPALM") raise NotImplementedError("Zyxel VMG does not support config replacement via NAPALM")
def load_merge_candidate( def load_merge_candidate(
self, filename: Optional[str] = None, config: Optional[str] = None self, filename: str | None = None, config: str | None = None
) -> None: ) -> None:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
raise NotImplementedError("Zyxel VMG does not support config merge via NAPALM") raise NotImplementedError("Zyxel VMG does not support config merge via NAPALM")
@@ -510,7 +517,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
"link_status": "Up" if wan_ip else "Down", "link_status": "Up" if wan_ip else "Down",
} }
def get_port_forwards(self) -> List[PortForwardDict]: def get_port_forwards(self) -> list[PortForwardDict]:
"""Return port forwarding rules from DAL or HTML fallback.""" """Return port forwarding rules from DAL or HTML fallback."""
html = self._fetch_page("cgi-bin/DAL?oid=NAT_PORTMAP") html = self._fetch_page("cgi-bin/DAL?oid=NAT_PORTMAP")
if html is not None: if html is not None:
@@ -524,8 +531,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
return self._parse_port_forwards_html(html) return self._parse_port_forwards_html(html)
return [] return []
def _parse_port_forwards_dal(self, data: dict) -> List[PortForwardDict]: def _parse_port_forwards_dal(self, data: dict) -> list[PortForwardDict]:
forwards: List[PortForwardDict] = [] forwards: list[PortForwardDict] = []
rules = data.get("rules", data.get("NATPortMap", data.get("entries", []))) rules = data.get("rules", data.get("NATPortMap", data.get("entries", [])))
if isinstance(rules, dict): if isinstance(rules, dict):
rules = [rules] rules = [rules]
@@ -552,8 +559,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
re.I, re.I,
) )
def _parse_port_forwards_html(self, html: str) -> List[PortForwardDict]: def _parse_port_forwards_html(self, html: str) -> list[PortForwardDict]:
forwards: List[PortForwardDict] = [] forwards: list[PortForwardDict] = []
for match in self._PORT_FORWARD_RE.finditer(html): for match in self._PORT_FORWARD_RE.finditer(html):
forwards.append({ forwards.append({
"name": match.group("name").strip(), "name": match.group("name").strip(),
@@ -565,7 +572,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
}) })
return forwards return forwards
def get_hosts(self) -> List[HostDict]: def get_hosts(self) -> list[HostDict]:
"""Return DHCP host entries from DAL or HTML fallback.""" """Return DHCP host entries from DAL or HTML fallback."""
html = self._fetch_page("cgi-bin/DAL?oid=LAN_HOSTS") html = self._fetch_page("cgi-bin/DAL?oid=LAN_HOSTS")
if html is not None: if html is not None:
@@ -579,8 +586,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
return self._parse_hosts_html(html) return self._parse_hosts_html(html)
return [] return []
def _parse_hosts_dal(self, data: dict) -> List[HostDict]: def _parse_hosts_dal(self, data: dict) -> list[HostDict]:
hosts: List[HostDict] = [] hosts: list[HostDict] = []
entries = data.get("hosts", data.get("LANHosts", data.get("entries", []))) entries = data.get("hosts", data.get("LANHosts", data.get("entries", [])))
if isinstance(entries, dict): if isinstance(entries, dict):
entries = [entries] entries = [entries]
@@ -610,8 +617,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
r"(?P<hostname>\S+)?", r"(?P<hostname>\S+)?",
) )
def _parse_hosts_html(self, html: str) -> List[HostDict]: def _parse_hosts_html(self, html: str) -> list[HostDict]:
hosts: List[HostDict] = [] hosts: list[HostDict] = []
for match in self._HOST_RE.finditer(html): for match in self._HOST_RE.finditer(html):
hosts.append({ hosts.append({
"mac": match.group("mac").lower(), "mac": match.group("mac").lower(),
@@ -622,10 +629,10 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
}) })
return hosts return hosts
def get_nat_translations(self) -> List[NATTranslationDict]: def get_nat_translations(self) -> list[NATTranslationDict]:
"""Return NAT translations derived from enabled port forwards.""" """Return NAT translations derived from enabled port forwards."""
wan_ip = (self._device_info or {}).get("WanIp", "") wan_ip = (self._device_info or {}).get("WanIp", "")
translations: List[NATTranslationDict] = [] translations: list[NATTranslationDict] = []
for forward in self.get_port_forwards(): for forward in self.get_port_forwards():
if not forward["enabled"]: if not forward["enabled"]:
continue continue
@@ -639,11 +646,11 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
}) })
return translations return translations
def get_vpn_tunnels(self) -> Dict[str, VPNTunnelDict]: def get_vpn_tunnels(self) -> dict[str, VPNTunnelDict]:
"""Not supported on Zyxel VMG.""" """Not supported on Zyxel VMG."""
return {} return {}
def get_wireless_clients(self) -> List[WirelessClientDict]: def get_wireless_clients(self) -> list[WirelessClientDict]:
"""Return connected wireless clients from DAL or HTML fallback.""" """Return connected wireless clients from DAL or HTML fallback."""
html = self._fetch_page("cgi-bin/DAL?oid=WIFI_CLIENTS") html = self._fetch_page("cgi-bin/DAL?oid=WIFI_CLIENTS")
if html is not None: if html is not None:
@@ -657,8 +664,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
return self._parse_wireless_clients_html(html) return self._parse_wireless_clients_html(html)
return [] return []
def _parse_wireless_clients_dal(self, data: dict) -> List[WirelessClientDict]: def _parse_wireless_clients_dal(self, data: dict) -> list[WirelessClientDict]:
clients: List[WirelessClientDict] = [] clients: list[WirelessClientDict] = []
entries = data.get("clients", data.get("assoc", data.get("entries", []))) entries = data.get("clients", data.get("assoc", data.get("entries", [])))
if isinstance(entries, dict): if isinstance(entries, dict):
entries = [entries] entries = [entries]
@@ -691,8 +698,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
r"(?P<rssi>-?\d+)", r"(?P<rssi>-?\d+)",
) )
def _parse_wireless_clients_html(self, html: str) -> List[WirelessClientDict]: def _parse_wireless_clients_html(self, html: str) -> list[WirelessClientDict]:
clients: List[WirelessClientDict] = [] clients: list[WirelessClientDict] = []
for match in self._WIFI_CLIENT_RE.finditer(html): for match in self._WIFI_CLIENT_RE.finditer(html):
clients.append({ clients.append({
"mac": match.group("mac").lower(), "mac": match.group("mac").lower(),
@@ -707,7 +714,7 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
}) })
return clients return clients
def get_ssids(self) -> Dict[str, SSIDDict]: def get_ssids(self) -> dict[str, SSIDDict]:
"""Return WiFi SSID configurations from DAL or HTML fallback.""" """Return WiFi SSID configurations from DAL or HTML fallback."""
html = self._fetch_page("cgi-bin/DAL?oid=WIFI_SSIDS") html = self._fetch_page("cgi-bin/DAL?oid=WIFI_SSIDS")
if html is not None: if html is not None:
@@ -721,8 +728,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
return self._parse_ssids_html(html) return self._parse_ssids_html(html)
return {} return {}
def _parse_ssids_dal(self, data: dict) -> Dict[str, SSIDDict]: def _parse_ssids_dal(self, data: dict) -> dict[str, SSIDDict]:
ssids: Dict[str, SSIDDict] = {} ssids: dict[str, SSIDDict] = {}
entries = data.get("ssids", data.get("entries", [])) entries = data.get("ssids", data.get("entries", []))
if isinstance(entries, dict): if isinstance(entries, dict):
entries = [entries] entries = [entries]
@@ -744,8 +751,8 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
} }
return ssids return ssids
def _parse_ssids_html(self, html: str) -> Dict[str, SSIDDict]: def _parse_ssids_html(self, html: str) -> dict[str, SSIDDict]:
ssids: Dict[str, SSIDDict] = {} ssids: dict[str, SSIDDict] = {}
pattern = re.compile(r"SSID[^:]*:\s*(\S+)", re.I) pattern = re.compile(r"SSID[^:]*:\s*(\S+)", re.I)
for match in pattern.finditer(html): for match in pattern.finditer(html):
name = match.group(1) name = match.group(1)
@@ -760,9 +767,9 @@ class ZyxelVMGDriver(ResidentialGatewayDriver):
} }
return ssids return ssids
def get_radio_status(self) -> Dict[str, RadioStatusDict]: def get_radio_status(self) -> dict[str, RadioStatusDict]:
"""Return WiFi radio status from DAL or HTML fallback.""" """Return WiFi radio status from DAL or HTML fallback."""
radios: Dict[str, RadioStatusDict] = {} radios: dict[str, RadioStatusDict] = {}
html = self._fetch_page("cgi-bin/DAL?oid=WIFI_RADIO") html = self._fetch_page("cgi-bin/DAL?oid=WIFI_RADIO")
if html is not None: if html is not None:
try: try: