Make the network device tracking feature optional in AVM Fritz!Tools (#144149)

* make the network device tracking feature optional

* fix doc strings

* Apply suggestions from code review

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
Michael 2025-05-03 21:04:59 +02:00 committed by GitHub
parent aea5760424
commit fb94f8ea18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 71 additions and 17 deletions

View File

@ -15,6 +15,8 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_SSL, DEFAULT_SSL,
DOMAIN, DOMAIN,
FRITZ_AUTH_EXCEPTIONS, FRITZ_AUTH_EXCEPTIONS,
@ -38,6 +40,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool:
"""Set up fritzboxtools from config entry.""" """Set up fritzboxtools from config entry."""
_LOGGER.debug("Setting up FRITZ!Box Tools component") _LOGGER.debug("Setting up FRITZ!Box Tools component")
avm_wrapper = AvmWrapper( avm_wrapper = AvmWrapper(
hass=hass, hass=hass,
config_entry=entry, config_entry=entry,
@ -46,6 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
username=entry.data[CONF_USERNAME], username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD], password=entry.data[CONF_PASSWORD],
use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL), use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL),
device_discovery_enabled=entry.options.get(
CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_FEATURE_DEVICE_TRACKING
),
) )
try: try:
@ -62,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
raise ConfigEntryAuthFailed("Missing UPnP configuration") raise ConfigEntryAuthFailed("Missing UPnP configuration")
await avm_wrapper.async_config_entry_first_refresh() await avm_wrapper.async_config_entry_first_refresh()
await avm_wrapper.async_trigger_cleanup()
entry.runtime_data = avm_wrapper entry.runtime_data = avm_wrapper

View File

@ -35,7 +35,9 @@ from homeassistant.helpers.service_info.ssdp import (
from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.typing import VolDictType
from .const import ( from .const import (
CONF_FEATURE_DEVICE_TRACKING,
CONF_OLD_DISCOVERY, CONF_OLD_DISCOVERY,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY,
DEFAULT_HOST, DEFAULT_HOST,
DEFAULT_HTTP_PORT, DEFAULT_HTTP_PORT,
@ -72,7 +74,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Initialize FRITZ!Box Tools flow.""" """Initialize FRITZ!Box Tools flow."""
self._name: str = "" self._name: str = ""
self._password: str = "" self._password: str = ""
self._use_tls: bool = False self._use_tls: bool = DEFAULT_SSL
self._feature_device_discovery: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING
self._port: int | None = None self._port: int | None = None
self._username: str = "" self._username: str = ""
self._model: str = "" self._model: str = ""
@ -141,6 +144,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
options={ options={
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY, CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
CONF_FEATURE_DEVICE_TRACKING: self._feature_device_discovery,
}, },
) )
@ -204,6 +208,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = user_input[CONF_USERNAME] self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]
self._use_tls = user_input[CONF_SSL] self._use_tls = user_input[CONF_SSL]
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
self._port = self._determine_port(user_input) self._port = self._determine_port(user_input)
error = await self.async_fritz_tools_init() error = await self.async_fritz_tools_init()
@ -234,6 +239,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(
CONF_FEATURE_DEVICE_TRACKING,
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
): bool,
} }
), ),
errors=errors or {}, errors=errors or {},
@ -250,6 +259,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(
CONF_FEATURE_DEVICE_TRACKING,
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
): bool,
} }
), ),
description_placeholders={"name": self._name}, description_placeholders={"name": self._name},
@ -405,7 +418,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
"""Handle options flow.""" """Handle options flow."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(data=user_input)
options = self.config_entry.options options = self.config_entry.options
data_schema = vol.Schema( data_schema = vol.Schema(
@ -420,6 +433,13 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
CONF_OLD_DISCOVERY, CONF_OLD_DISCOVERY,
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY), default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
): bool, ): bool,
vol.Optional(
CONF_FEATURE_DEVICE_TRACKING,
default=options.get(
CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
),
): bool,
} }
) )
return self.async_show_form(step_id="init", data_schema=data_schema) return self.async_show_form(step_id="init", data_schema=data_schema)

View File

@ -40,6 +40,9 @@ PLATFORMS = [
CONF_OLD_DISCOVERY = "old_discovery" CONF_OLD_DISCOVERY = "old_discovery"
DEFAULT_CONF_OLD_DISCOVERY = False DEFAULT_CONF_OLD_DISCOVERY = False
CONF_FEATURE_DEVICE_TRACKING = "feature_device_tracking"
DEFAULT_CONF_FEATURE_DEVICE_TRACKING = True
DSL_CONNECTION: Literal["dsl"] = "dsl" DSL_CONNECTION: Literal["dsl"] = "dsl"
DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_DEVICE_NAME = "Unknown device"

View File

@ -39,6 +39,7 @@ from homeassistant.util.hass_dict import HassKey
from .const import ( from .const import (
CONF_OLD_DISCOVERY, CONF_OLD_DISCOVERY,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY,
DEFAULT_HOST, DEFAULT_HOST,
DEFAULT_SSL, DEFAULT_SSL,
@ -175,6 +176,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
username: str = DEFAULT_USERNAME, username: str = DEFAULT_USERNAME,
host: str = DEFAULT_HOST, host: str = DEFAULT_HOST,
use_tls: bool = DEFAULT_SSL, use_tls: bool = DEFAULT_SSL,
device_discovery_enabled: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
) -> None: ) -> None:
"""Initialize FritzboxTools class.""" """Initialize FritzboxTools class."""
super().__init__( super().__init__(
@ -202,6 +204,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.port = port self.port = port
self.username = username self.username = username
self.use_tls = use_tls self.use_tls = use_tls
self.device_discovery_enabled = device_discovery_enabled
self.has_call_deflections: bool = False self.has_call_deflections: bool = False
self._model: str | None = None self._model: str | None = None
self._current_firmware: str | None = None self._current_firmware: str | None = None
@ -332,10 +335,15 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
"entity_states": {}, "entity_states": {},
} }
try: try:
await self.async_update_device_info()
if self.device_discovery_enabled:
await self.async_scan_devices() await self.async_scan_devices()
entity_data["entity_states"] = await self.hass.async_add_executor_job( entity_data["entity_states"] = await self.hass.async_add_executor_job(
self._entity_states_update self._entity_states_update
) )
if self.has_call_deflections: if self.has_call_deflections:
entity_data[ entity_data[
"call_deflections" "call_deflections"
@ -551,12 +559,8 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
if new_device: if new_device:
async_dispatcher_send(self.hass, self.signal_device_new) async_dispatcher_send(self.hass, self.signal_device_new)
async def async_scan_devices(self, now: datetime | None = None) -> None: async def async_update_device_info(self, now: datetime | None = None) -> None:
"""Scan for new devices and return a list of found device ids.""" """Update own device information."""
if self.hass.is_stopping:
_ha_is_stopping("scan devices")
return
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
( (
@ -565,6 +569,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self._release_url, self._release_url,
) = await self._async_update_device_info() ) = await self._async_update_device_info()
async def async_scan_devices(self, now: datetime | None = None) -> None:
"""Scan for new network devices."""
if self.hass.is_stopping:
_ha_is_stopping("scan devices")
return
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
if self._options: if self._options:
@ -683,6 +694,9 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
async def async_trigger_cleanup(self) -> None: async def async_trigger_cleanup(self) -> None:
"""Trigger device trackers cleanup.""" """Trigger device trackers cleanup."""
_LOGGER.debug("Device tracker cleanup triggered")
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
if self.device_discovery_enabled:
device_hosts = await self._async_update_hosts_info() device_hosts = await self._async_update_hosts_info()
entity_reg: er.EntityRegistry = er.async_get(self.hass) entity_reg: er.EntityRegistry = er.async_get(self.hass)
config_entry = self.config_entry config_entry = self.config_entry

View File

@ -4,7 +4,9 @@
"data_description_port": "Leave empty to use the default port.", "data_description_port": "Leave empty to use the default port.",
"data_description_username": "Username for the FRITZ!Box.", "data_description_username": "Username for the FRITZ!Box.",
"data_description_password": "Password for the FRITZ!Box.", "data_description_password": "Password for the FRITZ!Box.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box." "data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
"data_description_feature_device_tracking": "Enable or disable the network device tracking feature.",
"data_feature_device_tracking": "Enable network device tracking"
}, },
"config": { "config": {
"flow_title": "{name}", "flow_title": "{name}",
@ -15,12 +17,14 @@
"data": { "data": {
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]" "ssl": "[%key:common::config_flow::data::ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
}, },
"data_description": { "data_description": {
"username": "[%key:component::fritz::common::data_description_username%]", "username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]", "password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]" "ssl": "[%key:component::fritz::common::data_description_ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
} }
}, },
"reauth_confirm": { "reauth_confirm": {
@ -57,14 +61,16 @@
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]" "ssl": "[%key:common::config_flow::data::ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
}, },
"data_description": { "data_description": {
"host": "[%key:component::fritz::common::data_description_host%]", "host": "[%key:component::fritz::common::data_description_host%]",
"port": "[%key:component::fritz::common::data_description_port%]", "port": "[%key:component::fritz::common::data_description_port%]",
"username": "[%key:component::fritz::common::data_description_username%]", "username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]", "password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]" "ssl": "[%key:component::fritz::common::data_description_ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
} }
} }
}, },
@ -89,11 +95,13 @@
"init": { "init": {
"data": { "data": {
"consider_home": "Seconds to consider a device at 'home'", "consider_home": "Seconds to consider a device at 'home'",
"old_discovery": "Enable old discovery method" "old_discovery": "Enable old discovery method",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
}, },
"data_description": { "data_description": {
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.", "consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
"old_discovery": "Enable old discovery method. This is needed for some scenarios." "old_discovery": "Enable old discovery method. This is needed for some scenarios.",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
} }
} }
} }

View File

@ -16,6 +16,7 @@ from homeassistant.components.device_tracker import (
DEFAULT_CONSIDER_HOME, DEFAULT_CONSIDER_HOME,
) )
from homeassistant.components.fritz.const import ( from homeassistant.components.fritz.const import (
CONF_FEATURE_DEVICE_TRACKING,
CONF_OLD_DISCOVERY, CONF_OLD_DISCOVERY,
DOMAIN, DOMAIN,
ERROR_AUTH_INVALID, ERROR_AUTH_INVALID,
@ -744,6 +745,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert result["data"] == { assert result["data"] == {
CONF_OLD_DISCOVERY: False, CONF_OLD_DISCOVERY: False,
CONF_CONSIDER_HOME: 37, CONF_CONSIDER_HOME: 37,
CONF_FEATURE_DEVICE_TRACKING: True,
} }