Skip to content

Commit 76efd86

Browse files
authored
Merge pull request #248 from lbjordan/lbjordan-patch-2
1.8.1 fails setup in a retry loop
2 parents 1b2e126 + a425d88 commit 76efd86

1 file changed

Lines changed: 40 additions & 15 deletions

File tree

custom_components/myuplink/api.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
"""API for myUplink bound to Home Assistant OAuth."""
23

34
from __future__ import annotations
@@ -69,20 +70,34 @@ def _update_rate_limit_headers(self, response: ClientResponse) -> None:
6970
RateLimit-Remaining: requests still available in the current window
7071
RateLimit-Reset: seconds until the current window expires
7172
"""
72-
if "RateLimit-Limit" in response.headers:
73-
self.rate_limit_limit = int(response.headers["RateLimit-Limit"])
74-
if "RateLimit-Remaining" in response.headers:
75-
self.rate_limit_remaining = int(response.headers["RateLimit-Remaining"])
76-
if "RateLimit-Reset" in response.headers:
77-
reset_seconds = int(response.headers["RateLimit-Reset"])
73+
limit = self._header_int(response, "RateLimit-Limit")
74+
if limit is not None:
75+
self.rate_limit_limit = limit
76+
77+
remaining = self._header_int(response, "RateLimit-Remaining")
78+
if remaining is not None:
79+
self.rate_limit_remaining = remaining
80+
81+
reset_seconds = self._header_int(response, "RateLimit-Reset")
82+
if reset_seconds is not None:
7883
self.rate_limit_reset_at = datetime.now() + timedelta(seconds=reset_seconds)
7984
_LOGGER.debug(
80-
"Rate limit window: %d/%d remaining, resets in %d seconds",
85+
"Rate limit window: %s/%s remaining, resets in %d seconds",
8186
self.rate_limit_remaining,
8287
self.rate_limit_limit,
8388
reset_seconds,
8489
)
8590

91+
def _header_int(self, response: ClientResponse, name: str) -> int | None:
92+
value = response.headers.get(name)
93+
if value is None:
94+
return None
95+
try:
96+
return int(value)
97+
except (TypeError, ValueError):
98+
_LOGGER.debug("Could not parse %s header value: %r", name, value)
99+
return None
100+
86101
async def request(self, method, path, **kwargs) -> ClientResponse:
87102
"""Make an authorized request with rate limit window awareness."""
88103
headers = kwargs.pop("headers", None)
@@ -651,6 +666,9 @@ class Throttle:
651666
"""
652667

653668
MIN_DELAY_SECONDS = 60 / 25
669+
# Start pacing requests only once the window has this many or fewer
670+
# requests left; above this, fire requests back-to-back.
671+
LOW_REMAINING_THRESHOLD = 5
654672

655673
def __init__(self, auth: AsyncConfigEntryAuth) -> None:
656674
"""Initialize throttle."""
@@ -675,14 +693,21 @@ async def __aenter__(self):
675693
await asyncio.sleep(wait_seconds + 0.1)
676694
return self
677695

678-
time_since_last_request = (now - self._last_request_time).total_seconds()
679-
if time_since_last_request < self.MIN_DELAY_SECONDS:
680-
delay = self.MIN_DELAY_SECONDS - time_since_last_request
681-
_LOGGER.debug(
682-
"Throttling request: waiting %.2f seconds to maintain rate limit (25 req/min)",
683-
delay,
684-
)
685-
await asyncio.sleep(delay)
696+
# Only pace requests when the rate-limit window is running low.
697+
# With plenty of headroom there is no need to insert a fixed delay
698+
# before every call; doing so serializes startup and can overrun
699+
# Home Assistant's setup timeout on accounts with several devices.
700+
remaining = self._auth.rate_limit_remaining
701+
if remaining is not None and remaining <= self.LOW_REMAINING_THRESHOLD:
702+
time_since_last_request = (now - self._last_request_time).total_seconds()
703+
if time_since_last_request < self.MIN_DELAY_SECONDS:
704+
delay = self.MIN_DELAY_SECONDS - time_since_last_request
705+
_LOGGER.debug(
706+
"Rate limit low (%d remaining): waiting %.2f seconds",
707+
remaining,
708+
delay,
709+
)
710+
await asyncio.sleep(delay)
686711

687712
return self
688713

0 commit comments

Comments
 (0)