Split models and helpers from coordinator module in AVM Fritz!Box tools (#147412)

* split models from coordinator

* split helpers from coordinator
This commit is contained in:
Michael 2025-06-25 14:50:07 +02:00 committed by GitHub
parent c5f8acfe93
commit c54ce7eabd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 252 additions and 235 deletions

View File

@ -15,8 +15,9 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ConnectionInfo, FritzConfigEntry
from .coordinator import FritzConfigEntry
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
from .models import ConnectionInfo
_LOGGER = logging.getLogger(__name__)

View File

@ -19,15 +19,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles
from .coordinator import (
FRITZ_DATA_KEY,
AvmWrapper,
FritzConfigEntry,
FritzData,
FritzDevice,
_is_tracked,
)
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzDeviceBase
from .helpers import _is_tracked
from .models import FritzDevice
_LOGGER = logging.getLogger(__name__)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import Callable, Mapping, ValuesView
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from functools import partial
@ -34,7 +34,6 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from homeassistant.util.hass_dict import HassKey
from .const import (
@ -48,6 +47,15 @@ from .const import (
FRITZ_EXCEPTIONS,
MeshRoles,
)
from .helpers import _ha_is_stopping
from .models import (
ConnectionInfo,
Device,
FritzDevice,
HostAttributes,
HostInfo,
Interface,
)
_LOGGER = logging.getLogger(__name__)
@ -56,33 +64,13 @@ FRITZ_DATA_KEY: HassKey[FritzData] = HassKey(DOMAIN)
type FritzConfigEntry = ConfigEntry[AvmWrapper]
def _is_tracked(mac: str, current_devices: ValuesView[set[str]]) -> bool:
"""Check if device is already tracked."""
return any(mac in tracked for tracked in current_devices)
@dataclass
class FritzData:
"""Storage class for platform global data."""
def device_filter_out_from_trackers(
mac: str,
device: FritzDevice,
current_devices: ValuesView[set[str]],
) -> bool:
"""Check if device should be filtered out from trackers."""
reason: str | None = None
if device.ip_address == "":
reason = "Missing IP"
elif _is_tracked(mac, current_devices):
reason = "Already tracked"
if reason:
_LOGGER.debug(
"Skip adding device %s [%s], reason: %s", device.hostname, mac, reason
)
return bool(reason)
def _ha_is_stopping(activity: str) -> None:
"""Inform that HA is stopping."""
_LOGGER.warning("Cannot execute %s: HomeAssistant is shutting down", activity)
tracked: dict[str, set[str]] = field(default_factory=dict)
profile_switches: dict[str, set[str]] = field(default_factory=dict)
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
class ClassSetupMissing(Exception):
@ -93,68 +81,6 @@ class ClassSetupMissing(Exception):
super().__init__("Function called before Class setup")
@dataclass
class Device:
"""FRITZ!Box device class."""
connected: bool
connected_to: str
connection_type: str
ip_address: str
name: str
ssid: str | None
wan_access: bool | None = None
class Interface(TypedDict):
"""Interface details."""
device: str
mac: str
op_mode: str
ssid: str | None
type: str
HostAttributes = TypedDict(
"HostAttributes",
{
"Index": int,
"IPAddress": str,
"MACAddress": str,
"Active": bool,
"HostName": str,
"InterfaceType": str,
"X_AVM-DE_Port": int,
"X_AVM-DE_Speed": int,
"X_AVM-DE_UpdateAvailable": bool,
"X_AVM-DE_UpdateSuccessful": str,
"X_AVM-DE_InfoURL": str | None,
"X_AVM-DE_MACAddressList": str | None,
"X_AVM-DE_Model": str | None,
"X_AVM-DE_URL": str | None,
"X_AVM-DE_Guest": bool,
"X_AVM-DE_RequestClient": str,
"X_AVM-DE_VPN": bool,
"X_AVM-DE_WANAccess": str,
"X_AVM-DE_Disallow": bool,
"X_AVM-DE_IsMeshable": str,
"X_AVM-DE_Priority": str,
"X_AVM-DE_FriendlyName": str,
"X_AVM-DE_FriendlyNameIsWriteable": str,
},
)
class HostInfo(TypedDict):
"""FRITZ!Box host info class."""
mac: str
name: str
ip: str
status: bool
class UpdateCoordinatorDataType(TypedDict):
"""Update coordinator data type."""
@ -898,120 +824,3 @@ class AvmWrapper(FritzBoxTools):
"X_AVM-DE_WakeOnLANByMACAddress",
NewMACAddress=mac_address,
)
@dataclass
class FritzData:
"""Storage class for platform global data."""
tracked: dict[str, set[str]] = field(default_factory=dict)
profile_switches: dict[str, set[str]] = field(default_factory=dict)
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
class FritzDevice:
"""Representation of a device connected to the FRITZ!Box."""
def __init__(self, mac: str, name: str) -> None:
"""Initialize device info."""
self._connected = False
self._connected_to: str | None = None
self._connection_type: str | None = None
self._ip_address: str | None = None
self._last_activity: datetime | None = None
self._mac = mac
self._name = name
self._ssid: str | None = None
self._wan_access: bool | None = False
def update(self, dev_info: Device, consider_home: float) -> None:
"""Update device info."""
utc_point_in_time = dt_util.utcnow()
if self._last_activity:
consider_home_evaluated = (
utc_point_in_time - self._last_activity
).total_seconds() < consider_home
else:
consider_home_evaluated = dev_info.connected
if not self._name:
self._name = dev_info.name or self._mac.replace(":", "_")
self._connected = dev_info.connected or consider_home_evaluated
if dev_info.connected:
self._last_activity = utc_point_in_time
self._connected_to = dev_info.connected_to
self._connection_type = dev_info.connection_type
self._ip_address = dev_info.ip_address
self._ssid = dev_info.ssid
self._wan_access = dev_info.wan_access
@property
def connected_to(self) -> str | None:
"""Return connected status."""
return self._connected_to
@property
def connection_type(self) -> str | None:
"""Return connected status."""
return self._connection_type
@property
def is_connected(self) -> bool:
"""Return connected status."""
return self._connected
@property
def mac_address(self) -> str:
"""Get MAC address."""
return self._mac
@property
def hostname(self) -> str:
"""Get Name."""
return self._name
@property
def ip_address(self) -> str | None:
"""Get IP address."""
return self._ip_address
@property
def last_activity(self) -> datetime | None:
"""Return device last activity."""
return self._last_activity
@property
def ssid(self) -> str | None:
"""Return device connected SSID."""
return self._ssid
@property
def wan_access(self) -> bool | None:
"""Return device wan access."""
return self._wan_access
class SwitchInfo(TypedDict):
"""FRITZ!Box switch info class."""
description: str
friendly_name: str
icon: str
type: str
callback_update: Callable
callback_switch: Callable
init_state: bool
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool
ipv6_active: bool

View File

@ -10,15 +10,10 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import (
FRITZ_DATA_KEY,
AvmWrapper,
FritzConfigEntry,
FritzData,
FritzDevice,
device_filter_out_from_trackers,
)
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzDeviceBase
from .helpers import device_filter_out_from_trackers
from .models import FritzDevice
_LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,8 @@ from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_DEVICE_NAME, DOMAIN
from .coordinator import AvmWrapper, FritzDevice
from .coordinator import AvmWrapper
from .models import FritzDevice
class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):

View File

@ -0,0 +1,39 @@
"""Helpers for AVM FRITZ!Box."""
from __future__ import annotations
from collections.abc import ValuesView
import logging
from .models import FritzDevice
_LOGGER = logging.getLogger(__name__)
def _is_tracked(mac: str, current_devices: ValuesView[set[str]]) -> bool:
"""Check if device is already tracked."""
return any(mac in tracked for tracked in current_devices)
def device_filter_out_from_trackers(
mac: str,
device: FritzDevice,
current_devices: ValuesView[set[str]],
) -> bool:
"""Check if device should be filtered out from trackers."""
reason: str | None = None
if device.ip_address == "":
reason = "Missing IP"
elif _is_tracked(mac, current_devices):
reason = "Already tracked"
if reason:
_LOGGER.debug(
"Skip adding device %s [%s], reason: %s", device.hostname, mac, reason
)
return bool(reason)
def _ha_is_stopping(activity: str) -> None:
"""Inform that HA is stopping."""
_LOGGER.warning("Cannot execute %s: HomeAssistant is shutting down", activity)

View File

@ -0,0 +1,182 @@
"""Models for AVM FRITZ!Box."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import TypedDict
from homeassistant.util import dt as dt_util
from .const import MeshRoles
@dataclass
class Device:
"""FRITZ!Box device class."""
connected: bool
connected_to: str
connection_type: str
ip_address: str
name: str
ssid: str | None
wan_access: bool | None = None
class Interface(TypedDict):
"""Interface details."""
device: str
mac: str
op_mode: str
ssid: str | None
type: str
HostAttributes = TypedDict(
"HostAttributes",
{
"Index": int,
"IPAddress": str,
"MACAddress": str,
"Active": bool,
"HostName": str,
"InterfaceType": str,
"X_AVM-DE_Port": int,
"X_AVM-DE_Speed": int,
"X_AVM-DE_UpdateAvailable": bool,
"X_AVM-DE_UpdateSuccessful": str,
"X_AVM-DE_InfoURL": str | None,
"X_AVM-DE_MACAddressList": str | None,
"X_AVM-DE_Model": str | None,
"X_AVM-DE_URL": str | None,
"X_AVM-DE_Guest": bool,
"X_AVM-DE_RequestClient": str,
"X_AVM-DE_VPN": bool,
"X_AVM-DE_WANAccess": str,
"X_AVM-DE_Disallow": bool,
"X_AVM-DE_IsMeshable": str,
"X_AVM-DE_Priority": str,
"X_AVM-DE_FriendlyName": str,
"X_AVM-DE_FriendlyNameIsWriteable": str,
},
)
class HostInfo(TypedDict):
"""FRITZ!Box host info class."""
mac: str
name: str
ip: str
status: bool
class FritzDevice:
"""Representation of a device connected to the FRITZ!Box."""
def __init__(self, mac: str, name: str) -> None:
"""Initialize device info."""
self._connected = False
self._connected_to: str | None = None
self._connection_type: str | None = None
self._ip_address: str | None = None
self._last_activity: datetime | None = None
self._mac = mac
self._name = name
self._ssid: str | None = None
self._wan_access: bool | None = False
def update(self, dev_info: Device, consider_home: float) -> None:
"""Update device info."""
utc_point_in_time = dt_util.utcnow()
if self._last_activity:
consider_home_evaluated = (
utc_point_in_time - self._last_activity
).total_seconds() < consider_home
else:
consider_home_evaluated = dev_info.connected
if not self._name:
self._name = dev_info.name or self._mac.replace(":", "_")
self._connected = dev_info.connected or consider_home_evaluated
if dev_info.connected:
self._last_activity = utc_point_in_time
self._connected_to = dev_info.connected_to
self._connection_type = dev_info.connection_type
self._ip_address = dev_info.ip_address
self._ssid = dev_info.ssid
self._wan_access = dev_info.wan_access
@property
def connected_to(self) -> str | None:
"""Return connected status."""
return self._connected_to
@property
def connection_type(self) -> str | None:
"""Return connected status."""
return self._connection_type
@property
def is_connected(self) -> bool:
"""Return connected status."""
return self._connected
@property
def mac_address(self) -> str:
"""Get MAC address."""
return self._mac
@property
def hostname(self) -> str:
"""Get Name."""
return self._name
@property
def ip_address(self) -> str | None:
"""Get IP address."""
return self._ip_address
@property
def last_activity(self) -> datetime | None:
"""Return device last activity."""
return self._last_activity
@property
def ssid(self) -> str | None:
"""Return device connected SSID."""
return self._ssid
@property
def wan_access(self) -> bool | None:
"""Return device wan access."""
return self._wan_access
class SwitchInfo(TypedDict):
"""FRITZ!Box switch info class."""
description: str
friendly_name: str
icon: str
type: str
callback_update: Callable
callback_switch: Callable
init_state: bool
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool
ipv6_active: bool

View File

@ -27,8 +27,9 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from .const import DSL_CONNECTION, UPTIME_DEVIATION
from .coordinator import ConnectionInfo, FritzConfigEntry
from .coordinator import FritzConfigEntry
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
from .models import ConnectionInfo
_LOGGER = logging.getLogger(__name__)

View File

@ -25,16 +25,10 @@ from .const import (
WIFI_STANDARD,
MeshRoles,
)
from .coordinator import (
FRITZ_DATA_KEY,
AvmWrapper,
FritzConfigEntry,
FritzData,
FritzDevice,
SwitchInfo,
device_filter_out_from_trackers,
)
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzBoxBaseEntity, FritzDeviceBase
from .helpers import device_filter_out_from_trackers
from .models import FritzDevice, SwitchInfo
_LOGGER = logging.getLogger(__name__)