From f8af44cac2ed5483c95917f0ac8224a7e11d636f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 27 Oct 2021 12:01:06 +0200 Subject: [PATCH] Fritz clean device_tracker service (#56535) --- homeassistant/components/fritz/common.py | 86 ++++++++++++++++++-- homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/services.py | 32 ++++++-- homeassistant/components/fritz/services.yaml | 13 +++ 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 58c3357b33d8..78b3f2073a72 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -17,15 +17,27 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_entries_for_config_entry, + async_get, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + RegistryEntry, + async_entries_for_device, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -35,6 +47,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, + SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, TRACKER_SCAN_INTERVAL, @@ -70,6 +83,13 @@ def device_filter_out_from_trackers( return bool(reason) +def _cleanup_entity_filter(device: RegistryEntry) -> bool: + """Filter only relevant entities.""" + return device.domain == DEVICE_TRACKER_DOMAIN or ( + device.domain == DEVICE_SWITCH_DOMAIN and "_internet_access" in device.entity_id + ) + + class ClassSetupMissing(Exception): """Raised when a Class func is called before setup.""" @@ -281,29 +301,83 @@ class FritzBoxTools: _LOGGER.debug("Checking host info for FRITZ!Box router %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() - async def service_fritzbox(self, service: str) -> None: + async def service_fritzbox( + self, service_call: ServiceCall, config_entry: ConfigEntry + ) -> None: """Define FRITZ!Box services.""" - _LOGGER.debug("FRITZ!Box router: %s", service) + _LOGGER.debug("FRITZ!Box router: %s", service_call.service) if not self.connection: raise HomeAssistantError("Unable to establish a connection") try: - if service == SERVICE_REBOOT: + if service_call.service == SERVICE_REBOOT: await self.hass.async_add_executor_job( self.connection.call_action, "DeviceConfig1", "Reboot" ) - elif service == SERVICE_RECONNECT: + return + + if service_call.service == SERVICE_RECONNECT: await self.hass.async_add_executor_job( self.connection.call_action, "WANIPConn1", "ForceTermination", ) + return + + if service_call.service == SERVICE_CLEANUP: + device_hosts_list: list = await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) + except (FritzServiceError, FritzActionError) as ex: raise HomeAssistantError("Service or parameter unknown") from ex except FritzConnectionException as ex: raise HomeAssistantError("Service not supported") from ex + entity_reg: EntityRegistry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + + ha_entity_reg_list: list[ + RegistryEntry + ] = self.hass.helpers.entity_registry.async_entries_for_config_entry( + entity_reg, config_entry.entry_id + ) + entities_removed: bool = False + + device_hosts_macs = {device["mac"] for device in device_hosts_list} + + for entry in ha_entity_reg_list: + if ( + not _cleanup_entity_filter(entry) + or entry.unique_id.split("_")[0] in device_hosts_macs + ): + continue + _LOGGER.info("Removing entity: %s", entry.name or entry.original_name) + 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( + self, entity_reg: EntityRegistry, config_entry: ConfigEntry + ) -> None: + """Remove devices with no entities.""" + + device_reg = async_get(self.hass) + device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id) + for device_entry in device_list: + if async_entries_for_device( + 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) + @dataclass class FritzData: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 3ed4e7057303..d2c26d7bee88 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -22,6 +22,7 @@ ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" SERVICE_REBOOT = "reboot" SERVICE_RECONNECT = "reconnect" +SERVICE_CLEANUP = "cleanup" SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 359e7ced239e..2d8e16f15f0a 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -5,15 +5,25 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_extract_config_entry_ids -from .const import DOMAIN, FRITZ_SERVICES, SERVICE_REBOOT, SERVICE_RECONNECT +from .common import FritzBoxTools +from .const import ( + DOMAIN, + FRITZ_SERVICES, + SERVICE_CLEANUP, + SERVICE_REBOOT, + SERVICE_RECONNECT, +) _LOGGER = logging.getLogger(__name__) +SERVICE_LIST = [SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT] + + async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" - for service in (SERVICE_REBOOT, SERVICE_RECONNECT): + for service in SERVICE_LIST: if hass.services.has_service(DOMAIN, service): return @@ -29,12 +39,18 @@ async def async_setup_services(hass: HomeAssistant) -> None: f"Failed to call service '{service_call.service}'. Config entry for target not found" ) - for entry in fritzbox_entry_ids: + for entry_id in fritzbox_entry_ids: _LOGGER.debug("Executing service %s", service_call.service) - fritz_tools = hass.data[DOMAIN][entry] - await fritz_tools.service_fritzbox(service_call.service) + fritz_tools: FritzBoxTools = hass.data[DOMAIN][entry_id] + if config_entry := hass.config_entries.async_get_entry(entry_id): + await fritz_tools.service_fritzbox(service_call, config_entry) + else: + _LOGGER.error( + "Executing service %s failed, no config entry found", + service_call.service, + ) - for service in (SERVICE_REBOOT, SERVICE_RECONNECT): + for service in SERVICE_LIST: hass.services.async_register(DOMAIN, service, async_call_fritz_service) @@ -59,5 +75,5 @@ async def async_unload_services(hass: HomeAssistant) -> None: hass.data[FRITZ_SERVICES] = False - hass.services.async_remove(DOMAIN, SERVICE_REBOOT) - hass.services.async_remove(DOMAIN, SERVICE_RECONNECT) + for service in SERVICE_LIST: + hass.services.async_remove(DOMAIN, service) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 16e541762481..2375aa71f575 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -22,3 +22,16 @@ reboot: integration: fritz entity: device_class: connectivity + +cleanup: + description: Remove FRITZ!Box stale device_tracker entities + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to check + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity