From 5fbc00522444db50252dbd1e8b96e379ae51226b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:54:32 +0100 Subject: [PATCH] Make central AvmWrapper class fully async in Fritz!Tools (#83768) --- homeassistant/components/fritz/common.py | 220 +++++++++-------------- homeassistant/components/fritz/switch.py | 43 ++--- 2 files changed, 108 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 94053f47284..89c85c77972 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -34,7 +34,7 @@ from homeassistant.helpers import ( entity_registry as er, update_coordinator, ) -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import dt as dt_util @@ -302,10 +302,12 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - def _update_hosts_info(self) -> list[HostInfo]: + async def _async_update_hosts_info(self) -> list[HostInfo]: """Retrieve latest hosts information from the FRITZ!Box.""" try: - return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] + return await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) except Exception as ex: # pylint: disable=[broad-except] if not self.hass.is_stopping: raise HomeAssistantError("Error refreshing hosts info") from ex @@ -318,14 +320,22 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): release_url = info.get("NewX_AVM-DE_InfoURL") return bool(version), version, release_url - def _get_wan_access(self, ip_address: str) -> bool | None: + async def _async_update_device_info(self) -> tuple[bool, str | None, str | None]: + """Retrieve latest device information from the FRITZ!Box.""" + return await self.hass.async_add_executor_job(self._update_device_info) + + async def _async_get_wan_access(self, ip_address: str) -> bool | None: """Get WAN access rule for given IP address.""" try: - return not self.connection.call_action( - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=ip_address, - ).get("NewDisallow") + wan_access = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ) + ) + return not wan_access.get("NewDisallow") except FRITZ_EXCEPTIONS as ex: _LOGGER.debug( ( @@ -337,10 +347,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): ) return None - 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) - def manage_device_info( self, dev_info: Device, dev_mac: str, consider_home: bool ) -> bool: @@ -356,13 +362,13 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._devices[dev_mac] = device return True - def send_signal_device_update(self, new_device: bool) -> None: + async def async_send_signal_device_update(self, new_device: bool) -> None: """Signal device data updated.""" - dispatcher_send(self.hass, self.signal_device_update) + async_dispatcher_send(self.hass, self.signal_device_update) if new_device: - dispatcher_send(self.hass, self.signal_device_new) + async_dispatcher_send(self.hass, self.signal_device_new) - def scan_devices(self, now: datetime | None = None) -> None: + async def async_scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" if self.hass.is_stopping: @@ -374,7 +380,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._update_available, self._latest_firmware, self._release_url, - ) = self._update_device_info() + ) = await self._async_update_device_info() _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() @@ -387,7 +393,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): new_device = False hosts = {} - for host in self._update_hosts_info(): + for host in await self._async_update_hosts_info(): if not host.get("mac"): continue @@ -411,14 +417,18 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): if info.ip_address: - info.wan_access = self._get_wan_access(info.ip_address) + info.wan_access = await self._async_get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) return try: - if not (topology := self.fritz_hosts.get_mesh_topology()): + if not ( + topology := await self.hass.async_add_executor_job( + self.fritz_hosts.get_mesh_topology + ) + ): raise Exception("Mesh supported but empty topology reported") except FritzActionError: self.mesh_role = MeshRoles.SLAVE @@ -457,7 +467,9 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): dev_info: Device = hosts[dev_mac] if dev_info.ip_address: - dev_info.wan_access = self._get_wan_access(dev_info.ip_address) + dev_info.wan_access = await self._async_get_wan_access( + dev_info.ip_address + ) for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) @@ -472,7 +484,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): if self.manage_device_info(dev_info, dev_mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) async def async_trigger_firmware_update(self) -> bool: """Trigger firmware update.""" @@ -615,7 +627,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): class AvmWrapper(FritzBoxTools): """Setup AVM wrapper for API calls.""" - def _service_call_action( + async def _async_service_call( self, service_name: str, service_suffix: str, @@ -632,10 +644,13 @@ class AvmWrapper(FritzBoxTools): return {} try: - result: dict = self.connection.call_action( - f"{service_name}:{service_suffix}", - action_name, - **kwargs, + result: dict = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + f"{service_name}:{service_suffix}", + action_name, + **kwargs, + ) ) return result except FritzSecurityError: @@ -666,13 +681,15 @@ class AvmWrapper(FritzBoxTools): 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) + return await self._async_service_call("X_AVM-DE_UPnP", "1", "GetInfo") async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" - return await self.hass.async_add_executor_job( - partial(self.get_wan_link_properties) + return await self._async_service_call( + "WANCommonInterfaceConfig", + "1", + "GetCommonLinkProperties", ) async def async_ipv6_active(self) -> bool: @@ -703,34 +720,49 @@ class AvmWrapper(FritzBoxTools): ) return connection_info + async def async_get_num_port_mapping(self, con_type: str) -> dict[str, Any]: + """Call GetPortMappingNumberOfEntries action.""" + + return await self._async_service_call( + con_type, "1", "GetPortMappingNumberOfEntries" + ) + 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) + return await self._async_service_call( + con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=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) + return await self._async_service_call( + "WLANConfiguration", str(index), "GetInfo" + ) + + async def async_get_ontel_num_deflections(self) -> dict[str, Any]: + """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" + + return await self._async_service_call( + "X_AVM-DE_OnTel", "1", "GetNumberOfDeflections" ) 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) - ) + return await self._async_service_call("X_AVM-DE_OnTel", "1", "GetDeflections") 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) + return await self._async_service_call( + "WLANConfiguration", + str(index), + "SetEnable", + NewEnable="1" if turn_on else "0", ) async def async_set_deflection_enable( @@ -738,94 +770,7 @@ class AvmWrapper(FritzBoxTools): ) -> 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.""" - - return await self.hass.async_add_executor_job( - partial( - self.add_port_mapping, - con_type, - port_mapping, - ) - ) - - 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) - ) - - 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") - - 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") - - def get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return self._service_call_action( - "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" - ) - - 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( + return await self._async_service_call( "X_AVM-DE_OnTel", "1", "SetDeflectionEnable", @@ -833,17 +778,24 @@ class AvmWrapper(FritzBoxTools): NewEnable="1" if turn_on else "0", ) - def add_port_mapping(self, con_type: str, port_mapping: Any) -> dict[str, Any]: + async def async_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 + return await self._async_service_call( + con_type, + "1", + "AddPortMapping", + **port_mapping, ) - def set_allow_wan_access(self, ip_address: str, turn_on: bool) -> dict[str, Any]: + async def async_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( + return await self._async_service_call( "X_AVM-DE_HostFilter", "1", "DisallowWANAccessByIP", diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a45cd347463..a83b39ebbb1 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -39,14 +39,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def deflection_entities_list( +async def _async_deflection_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxDeflectionSwitch]: """Get list of deflection entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION) - deflections_response = avm_wrapper.get_ontel_num_deflections() + deflections_response = await avm_wrapper.async_get_ontel_num_deflections() if not deflections_response: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -61,7 +61,7 @@ def deflection_entities_list( _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] - if not (deflection_list := avm_wrapper.get_ontel_deflections()): + if not (deflection_list := await avm_wrapper.async_get_ontel_deflections()): return [] items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] @@ -74,7 +74,7 @@ def deflection_entities_list( ] -def port_entities_list( +async def _async_port_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, local_ip: str ) -> list[FritzBoxPortSwitch]: """Get list of port forwarding entities.""" @@ -86,7 +86,7 @@ def port_entities_list( return [] # Query port forwardings and setup a switch for each forward for the current device - resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type) + resp = await avm_wrapper.async_get_num_port_mapping(avm_wrapper.device_conn_type) if not resp: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -103,7 +103,9 @@ def port_entities_list( for i in range(port_forwards_count): - portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i) + portmap = await avm_wrapper.async_get_port_mapping( + avm_wrapper.device_conn_type, i + ) if not portmap: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) continue @@ -136,7 +138,7 @@ def port_entities_list( return entities_list -def wifi_entities_list( +async def _async_wifi_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" @@ -155,9 +157,7 @@ def wifi_entities_list( _LOGGER.debug("WiFi networks count: %s", wifi_count) networks: dict = {} for i in range(1, wifi_count + 1): - network_info = avm_wrapper.connection.call_action( - f"WLANConfiguration{i}", "GetInfo" - ) + network_info = await avm_wrapper.async_get_wlan_configuration(i) # Devices with 4 WLAN services, use the 2nd for internal communications if not (wifi_count == 4 and i == 2): networks[i] = { @@ -190,7 +190,7 @@ def wifi_entities_list( ] -def profile_entities_list( +async def _async_profile_entities_list( avm_wrapper: AvmWrapper, data_fritz: FritzData, ) -> list[FritzBoxProfileSwitch]: @@ -221,7 +221,7 @@ def profile_entities_list( return new_profiles -def all_entities_list( +async def async_all_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, data_fritz: FritzData, @@ -233,10 +233,10 @@ def all_entities_list( return [] return [ - *deflection_entities_list(avm_wrapper, device_friendly_name), - *port_entities_list(avm_wrapper, device_friendly_name, local_ip), - *wifi_entities_list(avm_wrapper, device_friendly_name), - *profile_entities_list(avm_wrapper, data_fritz), + *await _async_deflection_entities_list(avm_wrapper, device_friendly_name), + *await _async_port_entities_list(avm_wrapper, device_friendly_name, local_ip), + *await _async_wifi_entities_list(avm_wrapper, device_friendly_name), + *await _async_profile_entities_list(avm_wrapper, data_fritz), ] @@ -252,8 +252,7 @@ async def async_setup_entry( local_ip = await async_get_source_ip(avm_wrapper.hass, target_ip=avm_wrapper.host) - entities_list = await hass.async_add_executor_job( - all_entities_list, + entities_list = await async_all_entities_list( avm_wrapper, entry.title, data_fritz, @@ -263,12 +262,14 @@ async def async_setup_entry( async_add_entities(entities_list) @callback - def update_avm_device() -> None: + async def async_update_avm_device() -> None: """Update the values of the AVM device.""" - async_add_entities(profile_entities_list(avm_wrapper, data_fritz)) + async_add_entities(await _async_profile_entities_list(avm_wrapper, data_fritz)) entry.async_on_unload( - async_dispatcher_connect(hass, avm_wrapper.signal_device_new, update_avm_device) + async_dispatcher_connect( + hass, avm_wrapper.signal_device_new, async_update_avm_device + ) )