Make central AvmWrapper class fully async in Fritz!Tools (#83768)

This commit is contained in:
Michael 2023-01-16 20:54:32 +01:00 committed by GitHub
parent 156c815499
commit 5fbc005224
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 108 additions and 155 deletions

View File

@ -34,7 +34,7 @@ from homeassistant.helpers import (
entity_registry as er, entity_registry as er,
update_coordinator, 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.helpers.entity import DeviceInfo
from homeassistant.util import dt as dt_util 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.""" """Event specific per FRITZ!Box entry to signal updates in devices."""
return f"{DOMAIN}-device-update-{self._unique_id}" 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.""" """Retrieve latest hosts information from the FRITZ!Box."""
try: 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] except Exception as ex: # pylint: disable=[broad-except]
if not self.hass.is_stopping: if not self.hass.is_stopping:
raise HomeAssistantError("Error refreshing hosts info") from ex 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") release_url = info.get("NewX_AVM-DE_InfoURL")
return bool(version), version, release_url 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.""" """Get WAN access rule for given IP address."""
try: try:
return not self.connection.call_action( wan_access = await self.hass.async_add_executor_job(
"X_AVM-DE_HostFilter:1", partial(
"GetWANAccessByIP", self.connection.call_action,
NewIPv4Address=ip_address, "X_AVM-DE_HostFilter:1",
).get("NewDisallow") "GetWANAccessByIP",
NewIPv4Address=ip_address,
)
)
return not wan_access.get("NewDisallow")
except FRITZ_EXCEPTIONS as ex: except FRITZ_EXCEPTIONS as ex:
_LOGGER.debug( _LOGGER.debug(
( (
@ -337,10 +347,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
) )
return 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( def manage_device_info(
self, dev_info: Device, dev_mac: str, consider_home: bool self, dev_info: Device, dev_mac: str, consider_home: bool
) -> bool: ) -> bool:
@ -356,13 +362,13 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
self._devices[dev_mac] = device self._devices[dev_mac] = device
return True 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.""" """Signal device data updated."""
dispatcher_send(self.hass, self.signal_device_update) async_dispatcher_send(self.hass, self.signal_device_update)
if new_device: 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.""" """Scan for new devices and return a list of found device ids."""
if self.hass.is_stopping: if self.hass.is_stopping:
@ -374,7 +380,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
self._update_available, self._update_available,
self._latest_firmware, self._latest_firmware,
self._release_url, self._release_url,
) = self._update_device_info() ) = await self._async_update_device_info()
_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()
@ -387,7 +393,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
new_device = False new_device = False
hosts = {} hosts = {}
for host in self._update_hosts_info(): for host in await self._async_update_hosts_info():
if not host.get("mac"): if not host.get("mac"):
continue continue
@ -411,14 +417,18 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
self.mesh_role = MeshRoles.NONE self.mesh_role = MeshRoles.NONE
for mac, info in hosts.items(): for mac, info in hosts.items():
if info.ip_address: 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): if self.manage_device_info(info, mac, consider_home):
new_device = True new_device = True
self.send_signal_device_update(new_device) await self.async_send_signal_device_update(new_device)
return return
try: 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") raise Exception("Mesh supported but empty topology reported")
except FritzActionError: except FritzActionError:
self.mesh_role = MeshRoles.SLAVE self.mesh_role = MeshRoles.SLAVE
@ -457,7 +467,9 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
dev_info: Device = hosts[dev_mac] dev_info: Device = hosts[dev_mac]
if dev_info.ip_address: 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"]: for link in interf["node_links"]:
intf = mesh_intf.get(link["node_interface_1_uid"]) 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): if self.manage_device_info(dev_info, dev_mac, consider_home):
new_device = True 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: async def async_trigger_firmware_update(self) -> bool:
"""Trigger firmware update.""" """Trigger firmware update."""
@ -615,7 +627,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]):
class AvmWrapper(FritzBoxTools): class AvmWrapper(FritzBoxTools):
"""Setup AVM wrapper for API calls.""" """Setup AVM wrapper for API calls."""
def _service_call_action( async def _async_service_call(
self, self,
service_name: str, service_name: str,
service_suffix: str, service_suffix: str,
@ -632,10 +644,13 @@ class AvmWrapper(FritzBoxTools):
return {} return {}
try: try:
result: dict = self.connection.call_action( result: dict = await self.hass.async_add_executor_job(
f"{service_name}:{service_suffix}", partial(
action_name, self.connection.call_action,
**kwargs, f"{service_name}:{service_suffix}",
action_name,
**kwargs,
)
) )
return result return result
except FritzSecurityError: except FritzSecurityError:
@ -666,13 +681,15 @@ class AvmWrapper(FritzBoxTools):
async def async_get_upnp_configuration(self) -> dict[str, Any]: async def async_get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service.""" """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]: async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service.""" """Call WANCommonInterfaceConfig service."""
return await self.hass.async_add_executor_job( return await self._async_service_call(
partial(self.get_wan_link_properties) "WANCommonInterfaceConfig",
"1",
"GetCommonLinkProperties",
) )
async def async_ipv6_active(self) -> bool: async def async_ipv6_active(self) -> bool:
@ -703,34 +720,49 @@ class AvmWrapper(FritzBoxTools):
) )
return connection_info 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]: async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
"""Call GetGenericPortMappingEntry action.""" """Call GetGenericPortMappingEntry action."""
return await self.hass.async_add_executor_job( return await self._async_service_call(
partial(self.get_port_mapping, con_type, index) con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index
) )
async def async_get_wlan_configuration(self, index: int) -> dict[str, Any]: async def async_get_wlan_configuration(self, index: int) -> dict[str, Any]:
"""Call WLANConfiguration service.""" """Call WLANConfiguration service."""
return await self.hass.async_add_executor_job( return await self._async_service_call(
partial(self.get_wlan_configuration, index) "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]: async def async_get_ontel_deflections(self) -> dict[str, Any]:
"""Call GetDeflections action from X_AVM-DE_OnTel service.""" """Call GetDeflections action from X_AVM-DE_OnTel service."""
return await self.hass.async_add_executor_job( return await self._async_service_call("X_AVM-DE_OnTel", "1", "GetDeflections")
partial(self.get_ontel_deflections)
)
async def async_set_wlan_configuration( async def async_set_wlan_configuration(
self, index: int, turn_on: bool self, index: int, turn_on: bool
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Call SetEnable action from WLANConfiguration service.""" """Call SetEnable action from WLANConfiguration service."""
return await self.hass.async_add_executor_job( return await self._async_service_call(
partial(self.set_wlan_configuration, index, turn_on) "WLANConfiguration",
str(index),
"SetEnable",
NewEnable="1" if turn_on else "0",
) )
async def async_set_deflection_enable( async def async_set_deflection_enable(
@ -738,94 +770,7 @@ class AvmWrapper(FritzBoxTools):
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Call SetDeflectionEnable service.""" """Call SetDeflectionEnable service."""
return await self.hass.async_add_executor_job( return await self._async_service_call(
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(
"X_AVM-DE_OnTel", "X_AVM-DE_OnTel",
"1", "1",
"SetDeflectionEnable", "SetDeflectionEnable",
@ -833,17 +778,24 @@ class AvmWrapper(FritzBoxTools):
NewEnable="1" if turn_on else "0", 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.""" """Call AddPortMapping service."""
return self._service_call_action( return await self._async_service_call(
con_type, "1", "AddPortMapping", **port_mapping 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.""" """Call X_AVM-DE_HostFilter service."""
return self._service_call_action( return await self._async_service_call(
"X_AVM-DE_HostFilter", "X_AVM-DE_HostFilter",
"1", "1",
"DisallowWANAccessByIP", "DisallowWANAccessByIP",

View File

@ -39,14 +39,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def deflection_entities_list( async def _async_deflection_entities_list(
avm_wrapper: AvmWrapper, device_friendly_name: str avm_wrapper: AvmWrapper, device_friendly_name: str
) -> list[FritzBoxDeflectionSwitch]: ) -> list[FritzBoxDeflectionSwitch]:
"""Get list of deflection entities.""" """Get list of deflection entities."""
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION) _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: if not deflections_response:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
return [] return []
@ -61,7 +61,7 @@ def deflection_entities_list(
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
return [] return []
if not (deflection_list := avm_wrapper.get_ontel_deflections()): if not (deflection_list := await avm_wrapper.async_get_ontel_deflections()):
return [] return []
items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] 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 avm_wrapper: AvmWrapper, device_friendly_name: str, local_ip: str
) -> list[FritzBoxPortSwitch]: ) -> list[FritzBoxPortSwitch]:
"""Get list of port forwarding entities.""" """Get list of port forwarding entities."""
@ -86,7 +86,7 @@ def port_entities_list(
return [] return []
# Query port forwardings and setup a switch for each forward for the current device # 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: if not resp:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
return [] return []
@ -103,7 +103,9 @@ def port_entities_list(
for i in range(port_forwards_count): 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: if not portmap:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
continue continue
@ -136,7 +138,7 @@ def port_entities_list(
return entities_list return entities_list
def wifi_entities_list( async def _async_wifi_entities_list(
avm_wrapper: AvmWrapper, device_friendly_name: str avm_wrapper: AvmWrapper, device_friendly_name: str
) -> list[FritzBoxWifiSwitch]: ) -> list[FritzBoxWifiSwitch]:
"""Get list of wifi entities.""" """Get list of wifi entities."""
@ -155,9 +157,7 @@ def wifi_entities_list(
_LOGGER.debug("WiFi networks count: %s", wifi_count) _LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {} networks: dict = {}
for i in range(1, wifi_count + 1): for i in range(1, wifi_count + 1):
network_info = avm_wrapper.connection.call_action( network_info = await avm_wrapper.async_get_wlan_configuration(i)
f"WLANConfiguration{i}", "GetInfo"
)
# Devices with 4 WLAN services, use the 2nd for internal communications # Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2): if not (wifi_count == 4 and i == 2):
networks[i] = { networks[i] = {
@ -190,7 +190,7 @@ def wifi_entities_list(
] ]
def profile_entities_list( async def _async_profile_entities_list(
avm_wrapper: AvmWrapper, avm_wrapper: AvmWrapper,
data_fritz: FritzData, data_fritz: FritzData,
) -> list[FritzBoxProfileSwitch]: ) -> list[FritzBoxProfileSwitch]:
@ -221,7 +221,7 @@ def profile_entities_list(
return new_profiles return new_profiles
def all_entities_list( async def async_all_entities_list(
avm_wrapper: AvmWrapper, avm_wrapper: AvmWrapper,
device_friendly_name: str, device_friendly_name: str,
data_fritz: FritzData, data_fritz: FritzData,
@ -233,10 +233,10 @@ def all_entities_list(
return [] return []
return [ return [
*deflection_entities_list(avm_wrapper, device_friendly_name), *await _async_deflection_entities_list(avm_wrapper, device_friendly_name),
*port_entities_list(avm_wrapper, device_friendly_name, local_ip), *await _async_port_entities_list(avm_wrapper, device_friendly_name, local_ip),
*wifi_entities_list(avm_wrapper, device_friendly_name), *await _async_wifi_entities_list(avm_wrapper, device_friendly_name),
*profile_entities_list(avm_wrapper, data_fritz), *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) local_ip = await async_get_source_ip(avm_wrapper.hass, target_ip=avm_wrapper.host)
entities_list = await hass.async_add_executor_job( entities_list = await async_all_entities_list(
all_entities_list,
avm_wrapper, avm_wrapper,
entry.title, entry.title,
data_fritz, data_fritz,
@ -263,12 +262,14 @@ async def async_setup_entry(
async_add_entities(entities_list) async_add_entities(entities_list)
@callback @callback
def update_avm_device() -> None: async def async_update_avm_device() -> None:
"""Update the values of the AVM device.""" """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( 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
)
) )