1+
12"""API for myUplink bound to Home Assistant OAuth."""
23
34from __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