mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 14:58:11 -04:00
Handle Zonneplan rate limit and API errors (#183594)
This commit is contained in:
@@ -10,6 +10,7 @@ from pyzonneplan import (
|
||||
Zonneplan,
|
||||
ZonneplanConnectionError,
|
||||
ZonneplanInvalidOtpError,
|
||||
ZonneplanRateLimitError,
|
||||
ZonneplanTimeoutError,
|
||||
)
|
||||
|
||||
@@ -67,6 +68,8 @@ class ZonneplanConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return {"base": "cannot_connect"}
|
||||
except ZonneplanTimeoutError:
|
||||
return {"base": "timeout_connect"}
|
||||
except ZonneplanRateLimitError:
|
||||
return {"base": "rate_limited"}
|
||||
except Exception:
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return {"base": "unknown"}
|
||||
@@ -104,6 +107,8 @@ class ZonneplanConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except ZonneplanTimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except ZonneplanRateLimitError:
|
||||
errors["base"] = "rate_limited"
|
||||
except Exception:
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -13,6 +13,8 @@ from pyzonneplan import (
|
||||
Zonneplan,
|
||||
ZonneplanAuthenticationError,
|
||||
ZonneplanConnectionError,
|
||||
ZonneplanError,
|
||||
ZonneplanRateLimitError,
|
||||
ZonneplanTimeoutError,
|
||||
)
|
||||
from pyzonneplan.const import PriceChart
|
||||
@@ -107,6 +109,17 @@ class ZonneplanCoordinator(DataUpdateCoordinator[ZonneplanData]):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except ZonneplanRateLimitError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="rate_limited",
|
||||
retry_after=err.retry_after,
|
||||
) from err
|
||||
except ZonneplanError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
) from err
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.zonneplan.token is not None
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"rate_limited": "Too many requests to the Zonneplan API. Wait a minute and try again.",
|
||||
"timeout_connect": "Timeout while communicating with the Zonneplan API",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
@@ -111,12 +112,18 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "Unexpected error from the Zonneplan API"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Error communicating with the Zonneplan API"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "An error occurred while trying to authenticate"
|
||||
},
|
||||
"rate_limited": {
|
||||
"message": "The Zonneplan API rate limit was reached"
|
||||
},
|
||||
"timeout_connect": {
|
||||
"message": "Timeout while communicating with the Zonneplan API"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from pyzonneplan import (
|
||||
Token,
|
||||
ZonneplanConnectionError,
|
||||
ZonneplanInvalidOtpError,
|
||||
ZonneplanRateLimitError,
|
||||
ZonneplanTimeoutError,
|
||||
)
|
||||
|
||||
@@ -64,6 +65,9 @@ async def test_full_flow(hass: HomeAssistant, mock_zonneplan_client: AsyncMock)
|
||||
[
|
||||
pytest.param(ZonneplanConnectionError("offline"), "cannot_connect"),
|
||||
pytest.param(ZonneplanTimeoutError("timed out"), "timeout_connect"),
|
||||
pytest.param(
|
||||
ZonneplanRateLimitError("slow down", retry_after=30), "rate_limited"
|
||||
),
|
||||
pytest.param(Exception("unexpected"), "unknown"),
|
||||
],
|
||||
)
|
||||
@@ -114,6 +118,9 @@ async def test_step_user_exceptions(
|
||||
[
|
||||
pytest.param(ZonneplanConnectionError("offline"), "cannot_connect"),
|
||||
pytest.param(ZonneplanTimeoutError("timed out"), "timeout_connect"),
|
||||
pytest.param(
|
||||
ZonneplanRateLimitError("slow down", retry_after=30), "rate_limited"
|
||||
),
|
||||
pytest.param(ZonneplanInvalidOtpError("bad otp"), "invalid_auth"),
|
||||
pytest.param(Exception("unexpected"), "unknown"),
|
||||
],
|
||||
@@ -225,6 +232,9 @@ async def test_reauth_flow(
|
||||
[
|
||||
pytest.param(ZonneplanConnectionError("offline"), "cannot_connect"),
|
||||
pytest.param(ZonneplanTimeoutError("timed out"), "timeout_connect"),
|
||||
pytest.param(
|
||||
ZonneplanRateLimitError("slow down", retry_after=30), "rate_limited"
|
||||
),
|
||||
pytest.param(Exception("unexpected"), "unknown"),
|
||||
],
|
||||
)
|
||||
@@ -335,6 +345,9 @@ async def test_reconfigure_flow(
|
||||
[
|
||||
pytest.param(ZonneplanConnectionError("offline"), "cannot_connect"),
|
||||
pytest.param(ZonneplanTimeoutError("timed out"), "timeout_connect"),
|
||||
pytest.param(
|
||||
ZonneplanRateLimitError("slow down", retry_after=30), "rate_limited"
|
||||
),
|
||||
pytest.param(Exception("unexpected"), "unknown"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3,20 +3,24 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from pyzonneplan import (
|
||||
Token,
|
||||
ZonneplanAuthenticationError,
|
||||
ZonneplanConnectionError,
|
||||
ZonneplanRateLimitError,
|
||||
ZonneplanRequestError,
|
||||
ZonneplanTimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant.components.zonneplan.coordinator import UPDATE_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -37,6 +41,16 @@ from tests.common import MockConfigEntry
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
id="connection_error",
|
||||
),
|
||||
pytest.param(
|
||||
ZonneplanRateLimitError("slow down", retry_after=30),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
id="rate_limit_error",
|
||||
),
|
||||
pytest.param(
|
||||
ZonneplanRequestError("rejected"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
id="request_error",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_update_failed(
|
||||
@@ -56,6 +70,34 @@ async def test_setup_entry_update_failed(
|
||||
assert mock_config_entry.state is expected_state
|
||||
|
||||
|
||||
async def test_rate_limit_postpones_update(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_zonneplan_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test a rate limit schedules the next update after its Retry-After."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_zonneplan_client.async_get_account.call_count == 1
|
||||
|
||||
mock_zonneplan_client.async_get_account.side_effect = ZonneplanRateLimitError(
|
||||
"slow down", retry_after=120
|
||||
)
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_zonneplan_client.async_get_account.call_count == 2
|
||||
|
||||
# The next update follows Retry-After instead of the 15-minute interval.
|
||||
mock_zonneplan_client.async_get_account.side_effect = None
|
||||
freezer.tick(timedelta(seconds=120))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_zonneplan_client.async_get_account.call_count == 3
|
||||
|
||||
|
||||
async def test_setup_entry_persists_rotated_token(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
Reference in New Issue
Block a user