Skip to content

Commit

Permalink
Silently retry Fronius inverter endpoint 2 times (#61826)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmio committed Dec 19, 2021
1 parent 38cb477 commit 37bed64
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 15 deletions.
24 changes: 19 additions & 5 deletions homeassistant/components/fronius/coordinator.py
Expand Up @@ -5,7 +5,7 @@
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Dict, TypeVar

from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError

from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.core import callback
Expand Down Expand Up @@ -43,6 +43,8 @@ class FroniusCoordinatorBase(
error_interval: timedelta
valid_descriptions: list[SensorEntityDescription]

MAX_FAILED_UPDATES = 3

def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None:
"""Set up the FroniusCoordinatorBase class."""
self._failed_update_count = 0
Expand All @@ -62,7 +64,7 @@ async def _async_update_data(self) -> dict[SolarNetId, Any]:
data = await self._update_method()
except FroniusError as err:
self._failed_update_count += 1
if self._failed_update_count == 3:
if self._failed_update_count == self.MAX_FAILED_UPDATES:
self.update_interval = self.error_interval
raise UpdateFailed(err) from err

Expand Down Expand Up @@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
error_interval = timedelta(minutes=10)
valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS

SILENT_RETRIES = 3

def __init__(
self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any
) -> None:
Expand All @@ -125,9 +129,19 @@ def __init__(

async def _update_method(self) -> dict[SolarNetId, Any]:
"""Return data per solar net id from pyfronius."""
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
# almost 1% of `current_inverter_data` requests on Symo devices result in
# `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal
# communication between the logger and the inverter.
for silent_retry in range(self.SILENT_RETRIES):
try:
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
except BadStatusError as err:
if silent_retry == (self.SILENT_RETRIES - 1):
raise err
continue
break
# wrap a single devices data in a dict with solar_net_id key for
# FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys
return {self.inverter_info.solar_net_id: data}
Expand Down
38 changes: 28 additions & 10 deletions tests/components/fronius/test_coordinator.py
@@ -1,7 +1,7 @@
"""Test the Fronius update coordinators."""
from unittest.mock import patch

from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError

from homeassistant.components.fronius.coordinator import (
FroniusInverterUpdateCoordinator,
Expand All @@ -18,38 +18,56 @@ async def test_adaptive_update_interval(hass, aioclient_mock):
with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data:
mock_responses(aioclient_mock)
await setup_fronius_integration(hass)
assert mock_inverter_data.call_count == 1
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()

async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 2
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()

mock_inverter_data.side_effect = FroniusError
# first 3 requests at default interval - 4th has different interval
for _ in range(4):
mock_inverter_data.side_effect = FroniusError()
# first 3 bad requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 5
assert mock_inverter_data.call_count == 3
mock_inverter_data.reset_mock()

async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 6
assert mock_inverter_data.call_count == 1
mock_inverter_data.reset_mock()

mock_inverter_data.side_effect = None
# next successful request resets to default interval
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 7
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()

async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 8
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()

# BadStatusError on inverter endpoints have special handling
mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8)
# first 3 requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
# BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9
assert mock_inverter_data.call_count == 9

0 comments on commit 37bed64

Please sign in to comment.