diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 46acad68545..c2656b7f2f3 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -36,6 +36,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import slugify from homeassistant.util.hass_dict import HassKey from .const import ( @@ -90,10 +91,56 @@ class UpdateCoordinatorDataType(TypedDict): entity_states: dict[str, StateType | bool] +class FritzConnectionCached(FritzConnection): # type: ignore[misc] + """FritzConnection with cached call action.""" + + _call_cache: dict[str, dict[str, Any]] + + def clear_cache(self) -> None: + """Clear cached calls.""" + self._call_cache = {} + _LOGGER.debug("Cleared FritzConnection call action cache") + + def call_action( + self, + service_name: str, + action_name: str, + *, + arguments: dict | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Call action with cached services. Only get actions are cached.""" + if not action_name.lower().startswith("get"): + return super().call_action( # type: ignore[no-any-return] + service_name, action_name, arguments=arguments, **kwargs + ) + + if not hasattr(self, "_call_cache"): + self._call_cache = {} + + kwargs_key = ",".join(f"{k}={v!r}" for k, v in sorted(kwargs.items())) + + cache_key = slugify(f"{service_name}:{action_name}:{arguments}:{kwargs_key}") + if (result := self._call_cache.get(cache_key)) is not None: + _LOGGER.debug("Using cached result for %s %s", service_name, action_name) + return result + + result = super().call_action( + service_name, action_name, arguments=arguments, **kwargs + ) + self._call_cache[cache_key] = result + return result # type: ignore[no-any-return] + + class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): """FritzBoxTools class.""" config_entry: FritzConfigEntry + connection: FritzConnectionCached + fritz_guest_wifi: FritzGuestWLAN + fritz_hosts: FritzHosts + fritz_status: FritzStatus + fritz_call: FritzCall def __init__( self, @@ -118,11 +165,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): self._devices: dict[str, FritzDevice] = {} self._options: Mapping[str, Any] | None = None self._unique_id: str | None = None - self.connection: FritzConnection = None - self.fritz_guest_wifi: FritzGuestWLAN = None - self.fritz_hosts: FritzHosts = None - self.fritz_status: FritzStatus = None - self.fritz_call: FritzCall = None self.host = host self.mesh_role = MeshRoles.NONE self.mesh_wifi_uplink = False @@ -159,11 +201,12 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): name=self.config_entry.title, sw_version=self.current_firmware, ) + self.connection.clear_cache() def setup(self) -> None: """Set up FritzboxTools class.""" - self.connection = FritzConnection( + self.connection = FritzConnectionCached( address=self.host, port=self.port, user=self.username, @@ -263,6 +306,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): "call_deflections": {}, "entity_states": {}, } + self.connection.clear_cache() try: await self.async_update_device_info() diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 98fa3c848f2..05eea3e4e87 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -58,6 +58,10 @@ class FritzConnectionMock: """Overrire services data.""" self._services = services + def clear_cache(self) -> None: + """Mock clear_cache method.""" + return + def _call_action(self, service: str, action: str, **kwargs): LOGGER.debug( "_call_action service: %s, action: %s, **kwargs: %s", @@ -89,7 +93,8 @@ def fc_data_mock(): def fc_class_mock(fc_data): """Fixture that sets up a mocked FritzConnection class.""" with patch( - "homeassistant.components.fritz.coordinator.FritzConnection", autospec=True + "homeassistant.components.fritz.coordinator.FritzConnectionCached", + autospec=True, ) as result: result.return_value = FritzConnectionMock(fc_data) yield result diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 956e4f6037c..09550c40485 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -75,7 +75,7 @@ async def test_setup_auth_fail(hass: HomeAssistant, error) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.fritz.coordinator.FritzConnection", + "homeassistant.components.fritz.coordinator.FritzConnectionCached", side_effect=error, ): await hass.config_entries.async_setup(entry.entry_id) @@ -95,7 +95,7 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.fritz.coordinator.FritzConnection", + "homeassistant.components.fritz.coordinator.FritzConnectionCached", side_effect=error, ): await hass.config_entries.async_setup(entry.entry_id)