diff --git a/napalm_fritzbox/fritzbox.py b/napalm_fritzbox/fritzbox.py index 334b08b..030d095 100644 --- a/napalm_fritzbox/fritzbox.py +++ b/napalm_fritzbox/fritzbox.py @@ -34,7 +34,7 @@ management methods (``load_merge_candidate``, ``commit_config``, ...). from __future__ import annotations import socket -from typing import Any, Dict, List, Optional +from typing import Any from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( @@ -45,7 +45,7 @@ from fritzconnection.core.exceptions import ( ) 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 ( HostDict, NATTranslationDict, @@ -66,6 +66,12 @@ class FritzBoxDriver(ResidentialGatewayDriver): """NAPALM driver for AVM FritzBox (read-only, TR-064).""" VENDOR = "AVM" + DRIVER_NAME = "fritzbox" + HTTP_FINGERPRINT = [ + FingerprintRule("fritz!box", weight=10.0, mandatory=True), + FingerprintRule("fritzbox", weight=4.0), + FingerprintRule("avm fritz", weight=4.0), + ] _WAN_IP_SERVICES = ("WANIPConnection1", "WANPPPConnection1") _WLAN_SERVICES = ("WLANConfiguration1", "WLANConfiguration2", "WLANConfiguration3") @@ -76,7 +82,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): username: str, password: str, timeout: int = 60, - optional_args: Optional[Dict[str, Any]] = None, + optional_args: dict[str, Any] | None = None, ) -> None: self.hostname = hostname self.username = username @@ -91,7 +97,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): self.use_tls = bool(self.optional_args.get("use_tls", True)) self.port = int(self.optional_args.get("port") or (49443 if self.use_tls else 49000)) - self.fc: Optional[FritzConnection] = None + self.fc: FritzConnection | None = None # ------------------------------------------------------------------ # Connection management @@ -121,7 +127,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): """Drop the TR-064 connection.""" self.fc = None - def is_alive(self) -> Dict[str, bool]: + def is_alive(self) -> dict[str, bool]: """Return whether the connection is usable. Performs a lightweight socket-level check without sending a full @@ -140,7 +146,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): # Internal helpers # ------------------------------------------------------------------ - def _call(self, service: str, action: str, **kwargs: Any) -> Dict[str, Any]: + def _call(self, service: str, action: str, **kwargs: Any) -> dict[str, Any]: """Invoke a TR-064 SOAP action and return its output arguments. :raises ConnectionClosedException: if called before :meth:`open`. @@ -149,7 +155,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): raise ConnectionClosedException("Not connected – call open() first.") return self.fc.call_action(service, action, **kwargs) - def _wlan_services(self) -> List[str]: + def _wlan_services(self) -> list[str]: """Return the WLAN service names actually present on this device.""" if self.fc is None: return [] @@ -182,7 +188,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): # NAPALM standard getters # ------------------------------------------------------------------ - def get_facts(self) -> Dict[str, Any]: + def get_facts(self) -> dict[str, Any]: """Return a dictionary of general device facts. Calls ``DeviceInfo1.GetInfo``. @@ -205,14 +211,14 @@ class FritzBoxDriver(ResidentialGatewayDriver): "interface_list": interface_list, } - def get_interfaces(self) -> Dict[str, Dict[str, Any]]: + def get_interfaces(self) -> dict[str, dict[str, Any]]: """Return interface details keyed by interface name. Combines ``LANEthernetInterfaceConfig1.GetInfo``, ``WANCommonInterfaceConfig1.GetCommonLinkProperties`` and ``WLANConfiguration{1,2,3}.GetInfo``. """ - interfaces: Dict[str, Dict[str, Any]] = {} + interfaces: dict[str, dict[str, Any]] = {} try: lan = self._call("LANEthernetInterfaceConfig1", "GetInfo") @@ -259,7 +265,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): return interfaces - def get_interfaces_ip(self) -> Dict[str, Dict[str, Any]]: + def get_interfaces_ip(self) -> dict[str, dict[str, Any]]: """Return IP addresses keyed by interface name. WAN address comes from ``GetExternalIPAddress`` (and, if available, @@ -267,7 +273,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): service. The LAN address is the address used to reach the device (TR-064 does not expose the router's own LAN IP generically). """ - result: Dict[str, Dict[str, Any]] = {} + result: dict[str, dict[str, Any]] = {} try: service = self._wan_ip_service() @@ -283,12 +289,12 @@ class FritzBoxDriver(ResidentialGatewayDriver): result["lan"] = {"ipv4": {self.hostname: {"prefix_length": 24}}} 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 :meth:`get_hosts`. Each entry contains: ``interface``, ``mac``, ``ip``, ``age``. """ - arp_table: List[Dict[str, Any]] = [] + arp_table: list[dict[str, Any]] = [] for host in self.get_hosts(): if not host["is_active"] or not host["mac"] or not host["ip"]: continue @@ -342,14 +348,14 @@ class FritzBoxDriver(ResidentialGatewayDriver): return result - def get_port_forwards(self) -> List[PortForwardDict]: + def get_port_forwards(self) -> list[PortForwardDict]: """Return configured port forwarding rules. Iterates ``GetGenericPortMappingEntry`` on the active WAN connection service until the device reports an out-of-range index. """ service = self._wan_ip_service() - forwards: List[PortForwardDict] = [] + forwards: list[PortForwardDict] = [] index = 0 while True: try: @@ -373,7 +379,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): return forwards - def get_hosts(self) -> List[HostDict]: + def get_hosts(self) -> list[HostDict]: """Return hosts known to the FritzBox. Iterates ``Hosts1.GetGenericHostEntry`` for @@ -386,7 +392,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): except _OPTIONAL_SERVICE_ERRORS: return [] - hosts: List[HostDict] = [] + hosts: list[HostDict] = [] for index in range(count): try: entry = self._call("Hosts1", "GetGenericHostEntry", NewIndex=index) @@ -407,7 +413,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): return hosts - def get_nat_translations(self) -> List[NATTranslationDict]: + def get_nat_translations(self) -> list[NATTranslationDict]: """Return NAT translation entries derived from active port forwards. TR-064 does not expose a live connection-tracking table; this @@ -415,7 +421,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): rules, which is the closest equivalent FritzBox provides. """ ext_ip = self._wan_external_ip() - translations: List[NATTranslationDict] = [] + translations: list[NATTranslationDict] = [] for forward in self.get_port_forwards(): if not forward["enabled"]: continue @@ -432,7 +438,7 @@ class FritzBoxDriver(ResidentialGatewayDriver): ) return translations - def get_vpn_tunnels(self) -> Dict[str, VPNTunnelDict]: + def get_vpn_tunnels(self) -> dict[str, VPNTunnelDict]: """Return VPN tunnel status. FritzOS does not expose VPN (IPsec/WireGuard) tunnel state through @@ -440,13 +446,13 @@ class FritzBoxDriver(ResidentialGatewayDriver): """ return {} - def get_wireless_clients(self) -> List[WirelessClientDict]: + def get_wireless_clients(self) -> list[WirelessClientDict]: """Return wireless clients associated with the built-in access point(s). Iterates ``GetGenericAssociatedDeviceInfo`` for each ``WLANConfiguration{1,2,3}`` service present on the device. """ - clients: List[WirelessClientDict] = [] + clients: list[WirelessClientDict] = [] for service in self._wlan_services(): try: ssid = self._call(service, "GetInfo").get("NewSSID", "") @@ -481,13 +487,13 @@ class FritzBoxDriver(ResidentialGatewayDriver): return clients - def get_ssids(self) -> Dict[str, SSIDDict]: + def get_ssids(self) -> dict[str, SSIDDict]: """Return configured wireless networks (SSIDs). Calls ``GetInfo`` and ``GetTotalAssociations`` on each ``WLANConfiguration{1,2,3}`` service present on the device. """ - ssids: Dict[str, SSIDDict] = {} + ssids: dict[str, SSIDDict] = {} for service in self._wlan_services(): try: info = self._call(service, "GetInfo") @@ -516,14 +522,14 @@ class FritzBoxDriver(ResidentialGatewayDriver): return ssids - def get_radio_status(self) -> Dict[str, RadioStatusDict]: + def get_radio_status(self) -> dict[str, RadioStatusDict]: """Return wireless radio status. Calls ``GetInfo`` on each ``WLANConfiguration{1,2,3}`` service present on the device. ``channel_width``, ``tx_power`` and ``frequency`` are not exposed via TR-064 and are reported as ``0``. """ - radios: Dict[str, RadioStatusDict] = {} + radios: dict[str, RadioStatusDict] = {} for service in self._wlan_services(): try: info = self._call(service, "GetInfo")