2021-04-25 10:10:33 +00:00
|
|
|
"""Support for AVM FRITZ!Box classes."""
|
2021-04-25 13:48:03 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-09-29 14:19:06 +00:00
|
|
|
from collections.abc import Callable, ValuesView
|
2021-06-24 15:02:41 +00:00
|
|
|
from dataclasses import dataclass, field
|
2021-04-25 10:10:33 +00:00
|
|
|
from datetime import datetime, timedelta
|
2022-01-20 11:43:32 +00:00
|
|
|
from functools import partial
|
2021-04-25 10:10:33 +00:00
|
|
|
import logging
|
2021-06-24 15:02:41 +00:00
|
|
|
from types import MappingProxyType
|
2021-12-16 12:25:06 +00:00
|
|
|
from typing import Any, TypedDict, cast
|
2021-04-25 10:10:33 +00:00
|
|
|
|
|
|
|
from fritzconnection import FritzConnection
|
2021-05-11 20:56:52 +00:00
|
|
|
from fritzconnection.core.exceptions import (
|
|
|
|
FritzActionError,
|
|
|
|
FritzConnectionException,
|
2021-12-16 12:24:32 +00:00
|
|
|
FritzSecurityError,
|
2021-05-11 20:56:52 +00:00
|
|
|
FritzServiceError,
|
|
|
|
)
|
2021-04-25 10:10:33 +00:00
|
|
|
from fritzconnection.lib.fritzhosts import FritzHosts
|
|
|
|
from fritzconnection.lib.fritzstatus import FritzStatus
|
2022-02-04 07:57:14 +00:00
|
|
|
from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2022-09-13 00:50:44 +00:00
|
|
|
from homeassistant.components.device_tracker import (
|
2021-05-28 10:02:35 +00:00
|
|
|
CONF_CONSIDER_HOME,
|
|
|
|
DEFAULT_CONSIDER_HOME,
|
2022-09-13 00:50:44 +00:00
|
|
|
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
2021-05-28 10:02:35 +00:00
|
|
|
)
|
2021-10-27 10:01:06 +00:00
|
|
|
from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-12-16 12:24:32 +00:00
|
|
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
2021-05-11 20:56:52 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2022-01-08 18:23:12 +00:00
|
|
|
from homeassistant.helpers import (
|
|
|
|
device_registry as dr,
|
|
|
|
entity_registry as er,
|
|
|
|
update_coordinator,
|
2021-10-27 10:01:06 +00:00
|
|
|
)
|
2021-12-16 12:24:32 +00:00
|
|
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
|
|
|
from homeassistant.helpers.entity import DeviceInfo
|
2021-04-25 10:10:33 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
|
|
|
from .const import (
|
2022-02-23 00:35:48 +00:00
|
|
|
CONF_OLD_DISCOVERY,
|
|
|
|
DEFAULT_CONF_OLD_DISCOVERY,
|
2021-07-16 11:38:37 +00:00
|
|
|
DEFAULT_DEVICE_NAME,
|
2021-04-25 10:10:33 +00:00
|
|
|
DEFAULT_HOST,
|
|
|
|
DEFAULT_PORT,
|
|
|
|
DEFAULT_USERNAME,
|
|
|
|
DOMAIN,
|
2022-01-31 23:28:11 +00:00
|
|
|
FRITZ_EXCEPTIONS,
|
2021-10-27 10:01:06 +00:00
|
|
|
SERVICE_CLEANUP,
|
2021-05-11 20:56:52 +00:00
|
|
|
SERVICE_REBOOT,
|
|
|
|
SERVICE_RECONNECT,
|
2022-02-04 07:57:14 +00:00
|
|
|
SERVICE_SET_GUEST_WIFI_PW,
|
2021-12-30 22:23:55 +00:00
|
|
|
MeshRoles,
|
2021-04-25 10:10:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-09-29 03:59:03 +00:00
|
|
|
def _is_tracked(mac: str, current_devices: ValuesView) -> bool:
|
|
|
|
"""Check if device is already tracked."""
|
|
|
|
for tracked in current_devices:
|
|
|
|
if mac in tracked:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def device_filter_out_from_trackers(
|
|
|
|
mac: str,
|
|
|
|
device: FritzDevice,
|
|
|
|
current_devices: ValuesView,
|
|
|
|
) -> 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)
|
|
|
|
|
|
|
|
|
2022-01-08 18:23:12 +00:00
|
|
|
def _cleanup_entity_filter(device: er.RegistryEntry) -> bool:
|
2021-10-27 10:01:06 +00:00
|
|
|
"""Filter only relevant entities."""
|
|
|
|
return device.domain == DEVICE_TRACKER_DOMAIN or (
|
|
|
|
device.domain == DEVICE_SWITCH_DOMAIN and "_internet_access" in device.entity_id
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-04-04 09:04:54 +00:00
|
|
|
def _ha_is_stopping(activity: str) -> None:
|
|
|
|
"""Inform that HA is stopping."""
|
|
|
|
_LOGGER.info("Cannot execute %s: HomeAssistant is shutting down", activity)
|
|
|
|
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
class ClassSetupMissing(Exception):
|
|
|
|
"""Raised when a Class func is called before setup."""
|
|
|
|
|
2021-06-29 15:57:34 +00:00
|
|
|
def __init__(self) -> None:
|
2021-06-24 15:02:41 +00:00
|
|
|
"""Init custom exception."""
|
|
|
|
super().__init__("Function called before Class setup")
|
|
|
|
|
|
|
|
|
2021-04-25 10:10:33 +00:00
|
|
|
@dataclass
|
|
|
|
class Device:
|
|
|
|
"""FRITZ!Box device class."""
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
connected: bool
|
|
|
|
connected_to: str
|
|
|
|
connection_type: str
|
2021-04-25 10:10:33 +00:00
|
|
|
ip_address: str
|
|
|
|
name: str
|
2021-12-30 22:23:55 +00:00
|
|
|
ssid: str | None
|
2022-01-30 21:22:32 +00:00
|
|
|
wan_access: bool | None = None
|
2021-04-25 10:10:33 +00:00
|
|
|
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
class Interface(TypedDict):
|
|
|
|
"""Interface details."""
|
|
|
|
|
|
|
|
device: str
|
|
|
|
mac: str
|
2022-01-05 11:47:47 +00:00
|
|
|
op_mode: str
|
2021-12-30 22:23:55 +00:00
|
|
|
ssid: str | None
|
|
|
|
type: str
|
|
|
|
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
class HostInfo(TypedDict):
|
|
|
|
"""FRITZ!Box host info class."""
|
|
|
|
|
|
|
|
mac: str
|
|
|
|
name: str
|
|
|
|
ip: str
|
|
|
|
status: bool
|
|
|
|
|
|
|
|
|
2021-12-16 12:24:32 +00:00
|
|
|
class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
2022-01-17 10:09:58 +00:00
|
|
|
"""FritzBoxTools class."""
|
2021-04-25 10:10:33 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2021-06-24 15:02:41 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
password: str,
|
|
|
|
username: str = DEFAULT_USERNAME,
|
|
|
|
host: str = DEFAULT_HOST,
|
|
|
|
port: int = DEFAULT_PORT,
|
|
|
|
) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Initialize FritzboxTools class."""
|
2021-12-16 12:24:32 +00:00
|
|
|
super().__init__(
|
|
|
|
hass=hass,
|
|
|
|
logger=_LOGGER,
|
|
|
|
name=f"{DOMAIN}-{host}-coordinator",
|
|
|
|
update_interval=timedelta(seconds=30),
|
|
|
|
)
|
|
|
|
|
2021-07-16 11:38:37 +00:00
|
|
|
self._devices: dict[str, FritzDevice] = {}
|
2021-06-24 15:02:41 +00:00
|
|
|
self._options: MappingProxyType[str, Any] | None = None
|
|
|
|
self._unique_id: str | None = None
|
|
|
|
self.connection: FritzConnection = None
|
2022-02-04 07:57:14 +00:00
|
|
|
self.fritz_guest_wifi: FritzGuestWLAN = None
|
2021-06-24 15:02:41 +00:00
|
|
|
self.fritz_hosts: FritzHosts = None
|
|
|
|
self.fritz_status: FritzStatus = None
|
2021-04-25 10:10:33 +00:00
|
|
|
self.hass = hass
|
|
|
|
self.host = host
|
2021-12-30 22:23:55 +00:00
|
|
|
self.mesh_role = MeshRoles.NONE
|
2022-02-09 08:44:04 +00:00
|
|
|
self.device_conn_type: str | None = None
|
|
|
|
self.device_is_router: bool = False
|
2021-04-25 10:10:33 +00:00
|
|
|
self.password = password
|
|
|
|
self.port = port
|
|
|
|
self.username = username
|
2021-06-24 15:02:41 +00:00
|
|
|
self._model: str | None = None
|
2021-09-30 09:18:04 +00:00
|
|
|
self._current_firmware: str | None = None
|
|
|
|
self._latest_firmware: str | None = None
|
|
|
|
self._update_available: bool = False
|
2022-05-09 20:07:47 +00:00
|
|
|
self._release_url: str | None = None
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-16 12:24:32 +00:00
|
|
|
async def async_setup(
|
|
|
|
self, options: MappingProxyType[str, Any] | None = None
|
|
|
|
) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Wrap up FritzboxTools class setup."""
|
2021-12-16 12:24:32 +00:00
|
|
|
self._options = options
|
2021-06-24 15:02:41 +00:00
|
|
|
await self.hass.async_add_executor_job(self.setup)
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
def setup(self) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Set up FritzboxTools class."""
|
|
|
|
self.connection = FritzConnection(
|
|
|
|
address=self.host,
|
|
|
|
port=self.port,
|
|
|
|
user=self.username,
|
|
|
|
password=self.password,
|
|
|
|
timeout=60.0,
|
2021-07-26 14:44:58 +00:00
|
|
|
pool_maxsize=30,
|
2021-04-25 10:10:33 +00:00
|
|
|
)
|
|
|
|
|
2021-07-06 13:06:32 +00:00
|
|
|
if not self.connection:
|
|
|
|
_LOGGER.error("Unable to establish a connection with %s", self.host)
|
|
|
|
return
|
|
|
|
|
2022-01-31 23:28:11 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"detected services on %s %s",
|
|
|
|
self.host,
|
|
|
|
list(self.connection.services.keys()),
|
|
|
|
)
|
|
|
|
|
2022-01-08 22:39:51 +00:00
|
|
|
self.fritz_hosts = FritzHosts(fc=self.connection)
|
2022-02-04 07:57:14 +00:00
|
|
|
self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection)
|
2021-05-14 16:46:37 +00:00
|
|
|
self.fritz_status = FritzStatus(fc=self.connection)
|
2022-09-15 14:05:58 +00:00
|
|
|
info = self.fritz_status.get_device_info()
|
2022-01-31 23:28:11 +00:00
|
|
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
"gathered device info of %s %s",
|
|
|
|
self.host,
|
|
|
|
{
|
2022-09-15 14:05:58 +00:00
|
|
|
**vars(info),
|
2022-01-31 23:28:11 +00:00
|
|
|
"NewDeviceLog": "***omitted***",
|
|
|
|
"NewSerialNumber": "***omitted***",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
if not self._unique_id:
|
2022-09-15 14:05:58 +00:00
|
|
|
self._unique_id = info.serial_number
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2022-09-15 14:05:58 +00:00
|
|
|
self._model = info.model_name
|
|
|
|
self._current_firmware = info.software_version
|
2021-09-30 09:18:04 +00:00
|
|
|
|
2022-05-09 20:07:47 +00:00
|
|
|
(
|
|
|
|
self._update_available,
|
|
|
|
self._latest_firmware,
|
|
|
|
self._release_url,
|
|
|
|
) = self._update_device_info()
|
2022-09-15 14:05:58 +00:00
|
|
|
|
|
|
|
if self.fritz_status.has_wan_support:
|
|
|
|
self.device_conn_type = (
|
|
|
|
self.fritz_status.get_default_connection_service().connection_service
|
|
|
|
)
|
|
|
|
self.device_is_router = self.fritz_status.has_wan_enabled
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-16 12:24:32 +00:00
|
|
|
async def _async_update_data(self) -> None:
|
|
|
|
"""Update FritzboxTools data."""
|
|
|
|
try:
|
|
|
|
await self.async_scan_devices()
|
2022-02-03 20:32:36 +00:00
|
|
|
except FRITZ_EXCEPTIONS as ex:
|
|
|
|
raise update_coordinator.UpdateFailed(ex) from ex
|
2021-04-25 10:10:33 +00:00
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def unique_id(self) -> str:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Return unique id."""
|
2021-06-24 15:02:41 +00:00
|
|
|
if not self._unique_id:
|
|
|
|
raise ClassSetupMissing()
|
|
|
|
return self._unique_id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def model(self) -> str:
|
|
|
|
"""Return device model."""
|
|
|
|
if not self._model:
|
|
|
|
raise ClassSetupMissing()
|
|
|
|
return self._model
|
|
|
|
|
|
|
|
@property
|
2021-09-30 09:18:04 +00:00
|
|
|
def current_firmware(self) -> str:
|
|
|
|
"""Return current SW version."""
|
|
|
|
if not self._current_firmware:
|
2021-06-24 15:02:41 +00:00
|
|
|
raise ClassSetupMissing()
|
2021-09-30 09:18:04 +00:00
|
|
|
return self._current_firmware
|
|
|
|
|
|
|
|
@property
|
|
|
|
def latest_firmware(self) -> str | None:
|
|
|
|
"""Return latest SW version."""
|
|
|
|
return self._latest_firmware
|
|
|
|
|
|
|
|
@property
|
|
|
|
def update_available(self) -> bool:
|
|
|
|
"""Return if new SW version is available."""
|
|
|
|
return self._update_available
|
2021-06-24 15:02:41 +00:00
|
|
|
|
2022-05-09 20:07:47 +00:00
|
|
|
@property
|
|
|
|
def release_url(self) -> str | None:
|
|
|
|
"""Return the info URL for latest firmware."""
|
|
|
|
return self._release_url
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
@property
|
|
|
|
def mac(self) -> str:
|
|
|
|
"""Return device Mac address."""
|
|
|
|
if not self._unique_id:
|
|
|
|
raise ClassSetupMissing()
|
2022-01-08 18:23:12 +00:00
|
|
|
return dr.format_mac(self._unique_id)
|
2021-04-25 10:10:33 +00:00
|
|
|
|
|
|
|
@property
|
2021-09-29 03:59:03 +00:00
|
|
|
def devices(self) -> dict[str, FritzDevice]:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Return devices."""
|
|
|
|
return self._devices
|
|
|
|
|
|
|
|
@property
|
|
|
|
def signal_device_new(self) -> str:
|
|
|
|
"""Event specific per FRITZ!Box entry to signal new device."""
|
|
|
|
return f"{DOMAIN}-device-new-{self._unique_id}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def signal_device_update(self) -> str:
|
|
|
|
"""Event specific per FRITZ!Box entry to signal updates in devices."""
|
|
|
|
return f"{DOMAIN}-device-update-{self._unique_id}"
|
|
|
|
|
2021-09-30 09:18:04 +00:00
|
|
|
def _update_hosts_info(self) -> list[HostInfo]:
|
|
|
|
"""Retrieve latest hosts information from the FRITZ!Box."""
|
2021-10-06 03:26:18 +00:00
|
|
|
try:
|
|
|
|
return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return]
|
|
|
|
except Exception as ex: # pylint: disable=[broad-except]
|
|
|
|
if not self.hass.is_stopping:
|
|
|
|
raise HomeAssistantError("Error refreshing hosts info") from ex
|
|
|
|
return []
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2022-05-09 20:07:47 +00:00
|
|
|
def _update_device_info(self) -> tuple[bool, str | None, str | None]:
|
2021-09-30 09:18:04 +00:00
|
|
|
"""Retrieve latest device information from the FRITZ!Box."""
|
2022-05-09 20:07:47 +00:00
|
|
|
info = self.connection.call_action("UserInterface1", "GetInfo")
|
|
|
|
version = info.get("NewX_AVM-DE_Version")
|
|
|
|
release_url = info.get("NewX_AVM-DE_InfoURL")
|
|
|
|
return bool(version), version, release_url
|
2021-09-30 09:18:04 +00:00
|
|
|
|
2022-01-30 21:22:32 +00:00
|
|
|
def _get_wan_access(self, ip_address: str) -> bool | None:
|
|
|
|
"""Get WAN access rule for given IP address."""
|
2022-02-03 20:32:36 +00:00
|
|
|
try:
|
|
|
|
return not self.connection.call_action(
|
|
|
|
"X_AVM-DE_HostFilter:1",
|
|
|
|
"GetWANAccessByIP",
|
|
|
|
NewIPv4Address=ip_address,
|
|
|
|
).get("NewDisallow")
|
|
|
|
except FRITZ_EXCEPTIONS as ex:
|
|
|
|
_LOGGER.debug(
|
|
|
|
"could not get WAN access rule for client device with IP '%s', error: %s",
|
|
|
|
ip_address,
|
|
|
|
ex,
|
|
|
|
)
|
|
|
|
return None
|
2022-01-30 21:22:32 +00:00
|
|
|
|
2021-12-16 12:24:32 +00:00
|
|
|
async def async_scan_devices(self, now: datetime | None = None) -> None:
|
|
|
|
"""Wrap up FritzboxTools class scan."""
|
|
|
|
await self.hass.async_add_executor_job(self.scan_devices, now)
|
|
|
|
|
2022-02-23 00:35:48 +00:00
|
|
|
def manage_device_info(
|
|
|
|
self, dev_info: Device, dev_mac: str, consider_home: bool
|
|
|
|
) -> bool:
|
|
|
|
"""Update device lists."""
|
|
|
|
_LOGGER.debug("Client dev_info: %s", dev_info)
|
|
|
|
|
|
|
|
if dev_mac in self._devices:
|
|
|
|
self._devices[dev_mac].update(dev_info, consider_home)
|
|
|
|
return False
|
|
|
|
|
|
|
|
device = FritzDevice(dev_mac, dev_info.name)
|
|
|
|
device.update(dev_info, consider_home)
|
|
|
|
self._devices[dev_mac] = device
|
|
|
|
return True
|
|
|
|
|
|
|
|
def send_signal_device_update(self, new_device: bool) -> None:
|
|
|
|
"""Signal device data updated."""
|
|
|
|
dispatcher_send(self.hass, self.signal_device_update)
|
|
|
|
if new_device:
|
|
|
|
dispatcher_send(self.hass, self.signal_device_new)
|
|
|
|
|
2021-04-25 18:28:40 +00:00
|
|
|
def scan_devices(self, now: datetime | None = None) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Scan for new devices and return a list of found device ids."""
|
|
|
|
|
2022-04-04 09:04:54 +00:00
|
|
|
if self.hass.is_stopping:
|
|
|
|
_ha_is_stopping("scan devices")
|
|
|
|
return
|
|
|
|
|
2022-01-06 11:15:40 +00:00
|
|
|
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
2022-05-09 20:07:47 +00:00
|
|
|
(
|
|
|
|
self._update_available,
|
|
|
|
self._latest_firmware,
|
|
|
|
self._release_url,
|
|
|
|
) = self._update_device_info()
|
2021-12-30 22:23:55 +00:00
|
|
|
|
2022-01-06 11:15:40 +00:00
|
|
|
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
|
2021-07-07 18:19:31 +00:00
|
|
|
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
|
2021-06-24 15:02:41 +00:00
|
|
|
if self._options:
|
|
|
|
consider_home = self._options.get(
|
2021-07-07 18:19:31 +00:00
|
|
|
CONF_CONSIDER_HOME, _default_consider_home
|
2021-06-24 15:02:41 +00:00
|
|
|
)
|
|
|
|
else:
|
2021-07-07 18:19:31 +00:00
|
|
|
consider_home = _default_consider_home
|
2021-05-24 14:54:57 +00:00
|
|
|
|
2021-04-25 10:10:33 +00:00
|
|
|
new_device = False
|
2021-12-30 22:23:55 +00:00
|
|
|
hosts = {}
|
|
|
|
for host in self._update_hosts_info():
|
|
|
|
if not host.get("mac"):
|
2021-04-25 10:10:33 +00:00
|
|
|
continue
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
hosts[host["mac"]] = Device(
|
|
|
|
name=host["name"],
|
|
|
|
connected=host["status"],
|
|
|
|
connected_to="",
|
|
|
|
connection_type="",
|
|
|
|
ip_address=host["ip"],
|
|
|
|
ssid=None,
|
2022-01-30 21:22:32 +00:00
|
|
|
wan_access=None,
|
2021-12-30 22:23:55 +00:00
|
|
|
)
|
|
|
|
|
2022-09-15 14:05:58 +00:00
|
|
|
if not self.fritz_status.device_has_mesh_support or (
|
2022-02-23 00:35:48 +00:00
|
|
|
self._options
|
|
|
|
and self._options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY)
|
|
|
|
):
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Using old hosts discovery method. (Mesh not supported or user option)"
|
|
|
|
)
|
|
|
|
self.mesh_role = MeshRoles.NONE
|
|
|
|
for mac, info in hosts.items():
|
2022-03-07 16:23:08 +00:00
|
|
|
if info.ip_address:
|
|
|
|
info.wan_access = self._get_wan_access(info.ip_address)
|
2022-02-23 00:35:48 +00:00
|
|
|
if self.manage_device_info(info, mac, consider_home):
|
|
|
|
new_device = True
|
|
|
|
self.send_signal_device_update(new_device)
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
if not (topology := self.fritz_hosts.get_mesh_topology()):
|
|
|
|
raise Exception("Mesh supported but empty topology reported")
|
|
|
|
except FritzActionError:
|
|
|
|
self.mesh_role = MeshRoles.SLAVE
|
|
|
|
# Avoid duplicating device trackers
|
|
|
|
return
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
mesh_intf = {}
|
|
|
|
# first get all meshed devices
|
2022-02-20 05:07:40 +00:00
|
|
|
for node in topology.get("nodes", []):
|
2021-12-30 22:23:55 +00:00
|
|
|
if not node["is_meshed"]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for interf in node["node_interfaces"]:
|
|
|
|
int_mac = interf["mac_address"]
|
|
|
|
mesh_intf[interf["uid"]] = Interface(
|
|
|
|
device=node["device_name"],
|
|
|
|
mac=int_mac,
|
2022-01-05 11:47:47 +00:00
|
|
|
op_mode=interf.get("op_mode", ""),
|
2021-12-30 22:23:55 +00:00
|
|
|
ssid=interf.get("ssid", ""),
|
|
|
|
type=interf["type"],
|
2021-12-16 12:24:32 +00:00
|
|
|
)
|
2022-01-08 18:23:12 +00:00
|
|
|
if dr.format_mac(int_mac) == self.mac:
|
2021-12-30 22:23:55 +00:00
|
|
|
self.mesh_role = MeshRoles(node["mesh_role"])
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
# second get all client devices
|
2022-02-20 05:07:40 +00:00
|
|
|
for node in topology.get("nodes", []):
|
2021-12-30 22:23:55 +00:00
|
|
|
if node["is_meshed"]:
|
|
|
|
continue
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
for interf in node["node_interfaces"]:
|
|
|
|
dev_mac = interf["mac_address"]
|
2022-01-29 05:14:51 +00:00
|
|
|
|
|
|
|
if dev_mac not in hosts:
|
|
|
|
continue
|
|
|
|
|
|
|
|
dev_info: Device = hosts[dev_mac]
|
|
|
|
|
2022-02-01 17:57:34 +00:00
|
|
|
if dev_info.ip_address:
|
|
|
|
dev_info.wan_access = self._get_wan_access(dev_info.ip_address)
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
for link in interf["node_links"]:
|
|
|
|
intf = mesh_intf.get(link["node_interface_1_uid"])
|
2022-01-29 05:14:51 +00:00
|
|
|
if intf is not None:
|
2022-02-01 17:57:34 +00:00
|
|
|
if intf["op_mode"] == "AP_GUEST":
|
|
|
|
dev_info.wan_access = None
|
2021-12-30 22:23:55 +00:00
|
|
|
|
|
|
|
dev_info.connected_to = intf["device"]
|
|
|
|
dev_info.connection_type = intf["type"]
|
|
|
|
dev_info.ssid = intf.get("ssid")
|
2022-02-23 00:35:48 +00:00
|
|
|
|
|
|
|
if self.manage_device_info(dev_info, dev_mac, consider_home):
|
2022-01-29 05:14:51 +00:00
|
|
|
new_device = True
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2022-02-23 00:35:48 +00:00
|
|
|
self.send_signal_device_update(new_device)
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-16 12:25:06 +00:00
|
|
|
async def async_trigger_firmware_update(self) -> bool:
|
|
|
|
"""Trigger firmware update."""
|
|
|
|
results = await self.hass.async_add_executor_job(
|
|
|
|
self.connection.call_action, "UserInterface:1", "X_AVM-DE_DoUpdate"
|
|
|
|
)
|
|
|
|
return cast(bool, results["NewX_AVM-DE_UpdateState"])
|
|
|
|
|
|
|
|
async def async_trigger_reboot(self) -> None:
|
|
|
|
"""Trigger device reboot."""
|
2022-01-08 18:23:12 +00:00
|
|
|
await self.hass.async_add_executor_job(self.connection.reboot)
|
2021-12-16 12:25:06 +00:00
|
|
|
|
|
|
|
async def async_trigger_reconnect(self) -> None:
|
|
|
|
"""Trigger device reconnect."""
|
2022-01-08 18:23:12 +00:00
|
|
|
await self.hass.async_add_executor_job(self.connection.reconnect)
|
2021-10-27 10:01:06 +00:00
|
|
|
|
2022-02-04 07:57:14 +00:00
|
|
|
async def async_trigger_set_guest_password(
|
|
|
|
self, password: str | None, length: int
|
|
|
|
) -> None:
|
|
|
|
"""Trigger service to set a new guest wifi password."""
|
|
|
|
await self.hass.async_add_executor_job(
|
|
|
|
self.fritz_guest_wifi.set_password, password, length
|
|
|
|
)
|
|
|
|
|
2022-01-08 22:38:28 +00:00
|
|
|
async def async_trigger_cleanup(
|
|
|
|
self, config_entry: ConfigEntry | None = None
|
|
|
|
) -> None:
|
2022-01-08 18:23:12 +00:00
|
|
|
"""Trigger device trackers cleanup."""
|
|
|
|
device_hosts_list = await self.hass.async_add_executor_job(
|
|
|
|
self.fritz_hosts.get_hosts_info
|
2021-10-27 10:01:06 +00:00
|
|
|
)
|
2022-01-08 18:23:12 +00:00
|
|
|
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
2021-10-27 10:01:06 +00:00
|
|
|
|
2022-01-08 22:38:28 +00:00
|
|
|
if config_entry is None:
|
|
|
|
if self.config_entry is None:
|
|
|
|
return
|
|
|
|
config_entry = self.config_entry
|
|
|
|
|
2022-01-08 18:23:12 +00:00
|
|
|
ha_entity_reg_list: list[er.RegistryEntry] = er.async_entries_for_config_entry(
|
2021-10-27 10:01:06 +00:00
|
|
|
entity_reg, config_entry.entry_id
|
|
|
|
)
|
|
|
|
entities_removed: bool = False
|
|
|
|
|
2022-01-05 19:21:15 +00:00
|
|
|
device_hosts_macs = set()
|
|
|
|
device_hosts_names = set()
|
|
|
|
for device in device_hosts_list:
|
|
|
|
device_hosts_macs.add(device["mac"])
|
|
|
|
device_hosts_names.add(device["name"])
|
2021-10-27 10:01:06 +00:00
|
|
|
|
|
|
|
for entry in ha_entity_reg_list:
|
2022-01-05 19:21:15 +00:00
|
|
|
if entry.original_name is None:
|
|
|
|
continue
|
|
|
|
entry_name = entry.name or entry.original_name
|
|
|
|
entry_host = entry_name.split(" ")[0]
|
|
|
|
entry_mac = entry.unique_id.split("_")[0]
|
|
|
|
|
|
|
|
if not _cleanup_entity_filter(entry) or (
|
|
|
|
entry_mac in device_hosts_macs and entry_host in device_hosts_names
|
2021-10-27 10:01:06 +00:00
|
|
|
):
|
2022-01-05 19:21:15 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Skipping entity %s [mac=%s, host=%s]",
|
|
|
|
entry_name,
|
|
|
|
entry_mac,
|
|
|
|
entry_host,
|
|
|
|
)
|
2021-10-27 10:01:06 +00:00
|
|
|
continue
|
2022-01-05 19:21:15 +00:00
|
|
|
_LOGGER.info("Removing entity: %s", entry_name)
|
2021-10-27 10:01:06 +00:00
|
|
|
entity_reg.async_remove(entry.entity_id)
|
|
|
|
entities_removed = True
|
|
|
|
|
|
|
|
if entities_removed:
|
|
|
|
self._async_remove_empty_devices(entity_reg, config_entry)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_remove_empty_devices(
|
2022-01-08 18:23:12 +00:00
|
|
|
self, entity_reg: er.EntityRegistry, config_entry: ConfigEntry
|
2021-10-27 10:01:06 +00:00
|
|
|
) -> None:
|
|
|
|
"""Remove devices with no entities."""
|
|
|
|
|
2022-01-08 18:23:12 +00:00
|
|
|
device_reg = dr.async_get(self.hass)
|
|
|
|
device_list = dr.async_entries_for_config_entry(
|
|
|
|
device_reg, config_entry.entry_id
|
|
|
|
)
|
2021-10-27 10:01:06 +00:00
|
|
|
for device_entry in device_list:
|
2022-01-08 18:23:12 +00:00
|
|
|
if not er.async_entries_for_device(
|
2021-10-27 10:01:06 +00:00
|
|
|
entity_reg,
|
|
|
|
device_entry.id,
|
|
|
|
include_disabled_entities=True,
|
|
|
|
):
|
|
|
|
_LOGGER.info("Removing device: %s", device_entry.name)
|
|
|
|
device_reg.async_remove_device(device_entry.id)
|
|
|
|
|
2022-01-08 18:23:12 +00:00
|
|
|
async def service_fritzbox(
|
|
|
|
self, service_call: ServiceCall, config_entry: ConfigEntry
|
|
|
|
) -> None:
|
|
|
|
"""Define FRITZ!Box services."""
|
|
|
|
_LOGGER.debug("FRITZ!Box service: %s", service_call.service)
|
|
|
|
|
|
|
|
if not self.connection:
|
|
|
|
raise HomeAssistantError("Unable to establish a connection")
|
|
|
|
|
|
|
|
try:
|
|
|
|
if service_call.service == SERVICE_REBOOT:
|
|
|
|
_LOGGER.warning(
|
|
|
|
'Service "fritz.reboot" is deprecated, please use the corresponding button entity instead'
|
|
|
|
)
|
|
|
|
await self.async_trigger_reboot()
|
|
|
|
return
|
|
|
|
|
|
|
|
if service_call.service == SERVICE_RECONNECT:
|
|
|
|
_LOGGER.warning(
|
|
|
|
'Service "fritz.reconnect" is deprecated, please use the corresponding button entity instead'
|
|
|
|
)
|
|
|
|
await self.async_trigger_reconnect()
|
|
|
|
return
|
|
|
|
|
|
|
|
if service_call.service == SERVICE_CLEANUP:
|
2022-01-08 22:38:28 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
'Service "fritz.cleanup" is deprecated, please use the corresponding button entity instead'
|
|
|
|
)
|
2022-01-08 18:23:12 +00:00
|
|
|
await self.async_trigger_cleanup(config_entry)
|
|
|
|
return
|
|
|
|
|
2022-02-04 07:57:14 +00:00
|
|
|
if service_call.service == SERVICE_SET_GUEST_WIFI_PW:
|
|
|
|
await self.async_trigger_set_guest_password(
|
|
|
|
service_call.data.get("password"),
|
|
|
|
service_call.data.get("length", DEFAULT_PASSWORD_LENGTH),
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2022-01-08 18:23:12 +00:00
|
|
|
except (FritzServiceError, FritzActionError) as ex:
|
|
|
|
raise HomeAssistantError("Service or parameter unknown") from ex
|
|
|
|
except FritzConnectionException as ex:
|
|
|
|
raise HomeAssistantError("Service not supported") from ex
|
|
|
|
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2022-01-20 11:43:32 +00:00
|
|
|
class AvmWrapper(FritzBoxTools):
|
|
|
|
"""Setup AVM wrapper for API calls."""
|
|
|
|
|
|
|
|
def _service_call_action(
|
|
|
|
self,
|
|
|
|
service_name: str,
|
|
|
|
service_suffix: str,
|
|
|
|
action_name: str,
|
|
|
|
**kwargs: Any,
|
2022-01-23 12:04:19 +00:00
|
|
|
) -> dict:
|
2022-01-20 11:43:32 +00:00
|
|
|
"""Return service details."""
|
|
|
|
|
2022-04-04 09:04:54 +00:00
|
|
|
if self.hass.is_stopping:
|
|
|
|
_ha_is_stopping(f"{service_name}/{action_name}")
|
|
|
|
return {}
|
|
|
|
|
2022-01-20 11:43:32 +00:00
|
|
|
if f"{service_name}{service_suffix}" not in self.connection.services:
|
2022-01-23 12:04:19 +00:00
|
|
|
return {}
|
2022-01-20 11:43:32 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
result: dict = self.connection.call_action(
|
|
|
|
f"{service_name}:{service_suffix}",
|
|
|
|
action_name,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
return result
|
|
|
|
except FritzSecurityError:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Authorization Error: Please check the provided credentials and verify that you can log into the web interface",
|
|
|
|
exc_info=True,
|
|
|
|
)
|
2022-01-31 23:28:11 +00:00
|
|
|
except FRITZ_EXCEPTIONS:
|
2022-01-20 11:43:32 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Service/Action Error: cannot execute service %s with action %s",
|
|
|
|
service_name,
|
|
|
|
action_name,
|
|
|
|
exc_info=True,
|
|
|
|
)
|
|
|
|
except FritzConnectionException:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Connection Error: Please check the device is properly configured for remote login",
|
|
|
|
exc_info=True,
|
|
|
|
)
|
2022-01-23 12:04:19 +00:00
|
|
|
return {}
|
2022-01-20 11:43:32 +00:00
|
|
|
|
2022-03-03 23:41:50 +00:00
|
|
|
async def async_get_upnp_configuration(self) -> dict[str, Any]:
|
|
|
|
"""Call X_AVM-DE_UPnP service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(self.get_upnp_configuration)
|
|
|
|
|
2022-02-06 22:17:10 +00:00
|
|
|
async def async_get_wan_link_properties(self) -> dict[str, Any]:
|
|
|
|
"""Call WANCommonInterfaceConfig service."""
|
2022-01-23 12:04:19 +00:00
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
2022-02-06 22:17:10 +00:00
|
|
|
partial(self.get_wan_link_properties)
|
2022-01-23 12:04:19 +00:00
|
|
|
)
|
|
|
|
|
2022-03-04 22:38:28 +00:00
|
|
|
async def async_get_connection_info(self) -> ConnectionInfo:
|
|
|
|
"""Return ConnectionInfo data."""
|
|
|
|
|
|
|
|
link_properties = await self.async_get_wan_link_properties()
|
|
|
|
connection_info = ConnectionInfo(
|
|
|
|
connection=link_properties.get("NewWANAccessType", "").lower(),
|
|
|
|
mesh_role=self.mesh_role,
|
|
|
|
wan_enabled=self.device_is_router,
|
|
|
|
)
|
|
|
|
_LOGGER.debug(
|
|
|
|
"ConnectionInfo for FritzBox %s: %s",
|
|
|
|
self.host,
|
|
|
|
connection_info,
|
|
|
|
)
|
|
|
|
return connection_info
|
|
|
|
|
2022-01-23 12:04:19 +00:00
|
|
|
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
|
|
|
|
"""Call GetGenericPortMappingEntry action."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.get_port_mapping, con_type, index)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_get_wlan_configuration(self, index: int) -> dict[str, Any]:
|
|
|
|
"""Call WLANConfiguration service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.get_wlan_configuration, index)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_get_ontel_deflections(self) -> dict[str, Any]:
|
|
|
|
"""Call GetDeflections action from X_AVM-DE_OnTel service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.get_ontel_deflections)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_set_wlan_configuration(
|
|
|
|
self, index: int, turn_on: bool
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Call SetEnable action from WLANConfiguration service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.set_wlan_configuration, index, turn_on)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_set_deflection_enable(
|
|
|
|
self, index: int, turn_on: bool
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Call SetDeflectionEnable service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.set_deflection_enable, index, turn_on)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_add_port_mapping(
|
|
|
|
self, con_type: str, port_mapping: Any
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Call AddPortMapping service."""
|
2022-01-20 11:43:32 +00:00
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(
|
2022-01-23 12:04:19 +00:00
|
|
|
self.add_port_mapping,
|
|
|
|
con_type,
|
|
|
|
port_mapping,
|
2022-01-20 11:43:32 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-01-23 12:04:19 +00:00
|
|
|
async def async_set_allow_wan_access(
|
|
|
|
self, ip_address: str, turn_on: bool
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Call X_AVM-DE_HostFilter service."""
|
|
|
|
|
|
|
|
return await self.hass.async_add_executor_job(
|
|
|
|
partial(self.set_allow_wan_access, ip_address, turn_on)
|
|
|
|
)
|
|
|
|
|
2022-03-03 23:41:50 +00:00
|
|
|
def get_upnp_configuration(self) -> dict[str, Any]:
|
|
|
|
"""Call X_AVM-DE_UPnP service."""
|
|
|
|
|
|
|
|
return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo")
|
|
|
|
|
2022-01-23 12:04:19 +00:00
|
|
|
def get_ontel_num_deflections(self) -> dict[str, Any]:
|
|
|
|
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
"X_AVM-DE_OnTel", "1", "GetNumberOfDeflections"
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_ontel_deflections(self) -> dict[str, Any]:
|
|
|
|
"""Call GetDeflections action from X_AVM-DE_OnTel service."""
|
|
|
|
|
|
|
|
return self._service_call_action("X_AVM-DE_OnTel", "1", "GetDeflections")
|
|
|
|
|
|
|
|
def get_default_connection(self) -> dict[str, Any]:
|
|
|
|
"""Call Layer3Forwarding service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
"Layer3Forwarding", "1", "GetDefaultConnectionService"
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_num_port_mapping(self, con_type: str) -> dict[str, Any]:
|
|
|
|
"""Call GetPortMappingNumberOfEntries action."""
|
|
|
|
|
|
|
|
return self._service_call_action(con_type, "1", "GetPortMappingNumberOfEntries")
|
|
|
|
|
|
|
|
def get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
|
|
|
|
"""Call GetGenericPortMappingEntry action."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_wlan_configuration(self, index: int) -> dict[str, Any]:
|
|
|
|
"""Call WLANConfiguration service."""
|
|
|
|
|
|
|
|
return self._service_call_action("WLANConfiguration", str(index), "GetInfo")
|
|
|
|
|
2022-02-06 22:17:10 +00:00
|
|
|
def get_wan_link_properties(self) -> dict[str, Any]:
|
|
|
|
"""Call WANCommonInterfaceConfig service."""
|
2022-01-20 11:43:32 +00:00
|
|
|
|
2022-02-06 22:17:10 +00:00
|
|
|
return self._service_call_action(
|
|
|
|
"WANCommonInterfaceConfig", "1", "GetCommonLinkProperties"
|
|
|
|
)
|
2022-01-23 12:04:19 +00:00
|
|
|
|
|
|
|
def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]:
|
|
|
|
"""Call SetEnable action from WLANConfiguration service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
"WLANConfiguration",
|
|
|
|
str(index),
|
|
|
|
"SetEnable",
|
|
|
|
NewEnable="1" if turn_on else "0",
|
|
|
|
)
|
|
|
|
|
|
|
|
def set_deflection_enable(self, index: int, turn_on: bool) -> dict[str, Any]:
|
|
|
|
"""Call SetDeflectionEnable service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
"X_AVM-DE_OnTel",
|
|
|
|
"1",
|
|
|
|
"SetDeflectionEnable",
|
|
|
|
NewDeflectionId=index,
|
|
|
|
NewEnable="1" if turn_on else "0",
|
|
|
|
)
|
|
|
|
|
|
|
|
def add_port_mapping(self, con_type: str, port_mapping: Any) -> dict[str, Any]:
|
|
|
|
"""Call AddPortMapping service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
con_type, "1", "AddPortMapping", **port_mapping
|
|
|
|
)
|
|
|
|
|
|
|
|
def set_allow_wan_access(self, ip_address: str, turn_on: bool) -> dict[str, Any]:
|
|
|
|
"""Call X_AVM-DE_HostFilter service."""
|
|
|
|
|
|
|
|
return self._service_call_action(
|
|
|
|
"X_AVM-DE_HostFilter",
|
2022-01-20 11:43:32 +00:00
|
|
|
"1",
|
2022-01-23 12:04:19 +00:00
|
|
|
"DisallowWANAccessByIP",
|
|
|
|
NewIPv4Address=ip_address,
|
|
|
|
NewDisallow="0" if turn_on else "1",
|
2022-01-20 11:43:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
@dataclass
|
2021-04-29 18:10:36 +00:00
|
|
|
class FritzData:
|
|
|
|
"""Storage class for platform global data."""
|
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
tracked: dict = field(default_factory=dict)
|
2021-07-16 11:38:37 +00:00
|
|
|
profile_switches: dict = field(default_factory=dict)
|
|
|
|
|
|
|
|
|
2022-03-21 13:13:16 +00:00
|
|
|
class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]):
|
2022-01-06 11:15:40 +00:00
|
|
|
"""Entity base class for a device connected to a FRITZ!Box device."""
|
2021-07-16 11:38:37 +00:00
|
|
|
|
2022-01-20 11:43:32 +00:00
|
|
|
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
2021-07-16 11:38:37 +00:00
|
|
|
"""Initialize a FRITZ!Box device."""
|
2022-01-20 11:43:32 +00:00
|
|
|
super().__init__(avm_wrapper)
|
|
|
|
self._avm_wrapper = avm_wrapper
|
2021-07-16 11:38:37 +00:00
|
|
|
self._mac: str = device.mac_address
|
|
|
|
self._name: str = device.hostname or DEFAULT_DEVICE_NAME
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Return device name."""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ip_address(self) -> str | None:
|
|
|
|
"""Return the primary ip address of the device."""
|
|
|
|
if self._mac:
|
2022-01-20 11:43:32 +00:00
|
|
|
return self._avm_wrapper.devices[self._mac].ip_address
|
2021-07-16 11:38:37 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mac_address(self) -> str:
|
|
|
|
"""Return the mac address of the device."""
|
|
|
|
return self._mac
|
|
|
|
|
|
|
|
@property
|
|
|
|
def hostname(self) -> str | None:
|
|
|
|
"""Return hostname of the device."""
|
|
|
|
if self._mac:
|
2022-01-20 11:43:32 +00:00
|
|
|
return self._avm_wrapper.devices[self._mac].hostname
|
2021-07-16 11:38:37 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
async def async_process_update(self) -> None:
|
|
|
|
"""Update device."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
async def async_on_demand_update(self) -> None:
|
|
|
|
"""Update state."""
|
|
|
|
await self.async_process_update()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2021-04-29 18:10:36 +00:00
|
|
|
|
2021-04-25 10:10:33 +00:00
|
|
|
class FritzDevice:
|
2021-07-16 11:38:37 +00:00
|
|
|
"""Representation of a device connected to the FRITZ!Box."""
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-06-24 15:02:41 +00:00
|
|
|
def __init__(self, mac: str, name: str) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Initialize device info."""
|
2021-12-30 22:23:55 +00:00
|
|
|
self._connected = False
|
|
|
|
self._connected_to: str | None = None
|
|
|
|
self._connection_type: str | None = None
|
2021-06-24 15:02:41 +00:00
|
|
|
self._ip_address: str | None = None
|
|
|
|
self._last_activity: datetime | None = None
|
2021-12-30 22:23:55 +00:00
|
|
|
self._mac = mac
|
|
|
|
self._name = name
|
|
|
|
self._ssid: str | None = None
|
2022-01-30 21:22:32 +00:00
|
|
|
self._wan_access: bool | None = False
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
def update(self, dev_info: Device, consider_home: float) -> None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Update device info."""
|
|
|
|
utc_point_in_time = dt_util.utcnow()
|
2021-05-24 14:54:57 +00:00
|
|
|
|
2021-06-03 22:42:59 +00:00
|
|
|
if self._last_activity:
|
|
|
|
consider_home_evaluated = (
|
2021-05-24 14:54:57 +00:00
|
|
|
utc_point_in_time - self._last_activity
|
|
|
|
).total_seconds() < consider_home
|
|
|
|
else:
|
2021-12-30 22:23:55 +00:00
|
|
|
consider_home_evaluated = dev_info.connected
|
2021-05-24 14:54:57 +00:00
|
|
|
|
2021-06-03 22:42:59 +00:00
|
|
|
if not self._name:
|
|
|
|
self._name = dev_info.name or self._mac.replace(":", "_")
|
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
self._connected = dev_info.connected or consider_home_evaluated
|
2021-06-03 22:42:59 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
if dev_info.connected:
|
2021-05-24 14:54:57 +00:00
|
|
|
self._last_activity = utc_point_in_time
|
2021-06-03 22:42:59 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
self._connected_to = dev_info.connected_to
|
|
|
|
self._connection_type = dev_info.connection_type
|
2021-07-16 11:38:37 +00:00
|
|
|
self._ip_address = dev_info.ip_address
|
2021-12-30 22:23:55 +00:00
|
|
|
self._ssid = dev_info.ssid
|
2021-12-16 12:24:32 +00:00
|
|
|
self._wan_access = dev_info.wan_access
|
2021-04-25 10:10:33 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
@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
|
|
|
|
|
2021-04-25 10:10:33 +00:00
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def is_connected(self) -> bool:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Return connected status."""
|
|
|
|
return self._connected
|
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def mac_address(self) -> str:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Get MAC address."""
|
|
|
|
return self._mac
|
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def hostname(self) -> str:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Get Name."""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def ip_address(self) -> str | None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Get IP address."""
|
|
|
|
return self._ip_address
|
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def last_activity(self) -> datetime | None:
|
2021-04-25 10:10:33 +00:00
|
|
|
"""Return device last activity."""
|
|
|
|
return self._last_activity
|
2021-05-04 03:11:21 +00:00
|
|
|
|
2021-12-30 22:23:55 +00:00
|
|
|
@property
|
|
|
|
def ssid(self) -> str | None:
|
|
|
|
"""Return device connected SSID."""
|
|
|
|
return self._ssid
|
|
|
|
|
2021-12-16 12:24:32 +00:00
|
|
|
@property
|
2022-01-30 21:22:32 +00:00
|
|
|
def wan_access(self) -> bool | None:
|
2021-12-16 12:24:32 +00:00
|
|
|
"""Return device wan access."""
|
|
|
|
return self._wan_access
|
|
|
|
|
2021-05-04 03:11:21 +00:00
|
|
|
|
2021-06-29 15:57:34 +00:00
|
|
|
class SwitchInfo(TypedDict):
|
|
|
|
"""FRITZ!Box switch info class."""
|
|
|
|
|
|
|
|
description: str
|
|
|
|
friendly_name: str
|
|
|
|
icon: str
|
|
|
|
type: str
|
|
|
|
callback_update: Callable
|
|
|
|
callback_switch: Callable
|
|
|
|
|
|
|
|
|
2021-05-04 03:11:21 +00:00
|
|
|
class FritzBoxBaseEntity:
|
|
|
|
"""Fritz host entity base class."""
|
|
|
|
|
2022-01-20 11:43:32 +00:00
|
|
|
def __init__(self, avm_wrapper: AvmWrapper, device_name: str) -> None:
|
2021-05-04 03:11:21 +00:00
|
|
|
"""Init device info class."""
|
2022-01-20 11:43:32 +00:00
|
|
|
self._avm_wrapper = avm_wrapper
|
2021-05-04 03:11:21 +00:00
|
|
|
self._device_name = device_name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mac_address(self) -> str:
|
|
|
|
"""Return the mac address of the main device."""
|
2022-01-20 11:43:32 +00:00
|
|
|
return self._avm_wrapper.mac
|
2021-05-04 03:11:21 +00:00
|
|
|
|
|
|
|
@property
|
2021-06-24 15:02:41 +00:00
|
|
|
def device_info(self) -> DeviceInfo:
|
2021-05-04 03:11:21 +00:00
|
|
|
"""Return the device information."""
|
2021-10-22 15:40:13 +00:00
|
|
|
return DeviceInfo(
|
2022-01-20 11:43:32 +00:00
|
|
|
configuration_url=f"http://{self._avm_wrapper.host}",
|
2022-01-08 18:23:12 +00:00
|
|
|
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
|
2022-01-20 11:43:32 +00:00
|
|
|
identifiers={(DOMAIN, self._avm_wrapper.unique_id)},
|
2021-10-22 15:40:13 +00:00
|
|
|
manufacturer="AVM",
|
2022-01-20 11:43:32 +00:00
|
|
|
model=self._avm_wrapper.model,
|
2021-10-23 10:01:21 +00:00
|
|
|
name=self._device_name,
|
2022-01-20 11:43:32 +00:00
|
|
|
sw_version=self._avm_wrapper.current_firmware,
|
2021-10-22 15:40:13 +00:00
|
|
|
)
|
2022-03-04 22:38:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class ConnectionInfo:
|
|
|
|
"""Fritz sensor connection information class."""
|
|
|
|
|
|
|
|
connection: str
|
|
|
|
mesh_role: MeshRoles
|
|
|
|
wan_enabled: bool
|