mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-07-27 17:26:48 +00:00
fix(api): accept priceInfoRange-only responses and retry transient empty data
Some markets/accounts (e.g. NL hourly) return an empty priceInfo(QUARTER_HOURLY).today while priceInfoRange still delivers the full set of intervals. _check_price_info_empty required today's data and wrongly classified such complete responses as empty, which blocked setup. It now treats a response as valid when today OR yesterday data is present, since priceInfoRange is the authoritative source the interval pool uses. Additionally, "Empty data received" is now retried a few times with exponential backoff, smoothing brief Tibber outages that transiently return empty data within a single update cycle. Long outages are still absorbed by the IntervalPool cache fallback, so the in-request retry count stays small. Reported in #141. Impact: Setup and updates no longer fail with "Empty data received" for NL hourly accounts (and similar), and short Tibber blips are smoothed over.
This commit is contained in:
parent
a4a1243a36
commit
8da3083fb2
2 changed files with 31 additions and 6 deletions
|
|
@ -51,6 +51,19 @@ class TibberPricesApiClient:
|
|||
self._max_retries = 5
|
||||
self._retry_delay = 2 # Base retry delay in seconds
|
||||
|
||||
# Empty-data responses are usually permanent (no data for the requested
|
||||
# range), but during a Tibber outage the API can return empty data
|
||||
# transiently before recovering. Retry a small number of times with backoff
|
||||
# so a brief blip is smoothed out within a single update cycle.
|
||||
#
|
||||
# NOTE: This is NOT the mechanism that bridges multi-hour outages. Long
|
||||
# outages are absorbed by the IntervalPool cache fallback (see
|
||||
# interval_pool/manager.py): as long as cached intervals cover the current
|
||||
# interval, the integration keeps serving cached prices and retries on the
|
||||
# NEXT update cycle. That bridge is unbounded in time, so this in-request
|
||||
# retry count must stay small to avoid blocking the event loop.
|
||||
self._max_empty_data_retries = 2
|
||||
|
||||
# Timeout configuration - more granular control
|
||||
self._connect_timeout = 10 # Connection timeout in seconds
|
||||
self._request_timeout = 25 # Total request timeout in seconds
|
||||
|
|
@ -834,9 +847,17 @@ class TibberPricesApiClient:
|
|||
"""Handle retry logic for API-specific errors."""
|
||||
error_msg = str(error)
|
||||
|
||||
# Non-retryable: Invalid queries, bad requests, empty data
|
||||
# Empty data means API has no data for the requested range - retrying won't help
|
||||
if "Invalid GraphQL query" in error_msg or "Bad request" in error_msg or "Empty data received" in error_msg:
|
||||
# Non-retryable: Invalid queries and bad requests are permanent client errors.
|
||||
if "Invalid GraphQL query" in error_msg or "Bad request" in error_msg:
|
||||
return False, 0
|
||||
|
||||
# Empty data is usually permanent (API has no data for the requested range),
|
||||
# but can be returned transiently during a Tibber outage. Retry a limited
|
||||
# number of times with exponential backoff before giving up.
|
||||
if "Empty data received" in error_msg:
|
||||
if retry < self._max_empty_data_retries:
|
||||
delay = min(self._retry_delay * (2**retry), 30)
|
||||
return True, delay
|
||||
return False, 0
|
||||
|
||||
# Rate limits - only retry if server explicitly says so
|
||||
|
|
|
|||
|
|
@ -219,9 +219,13 @@ def _check_price_info_empty(data: dict) -> bool:
|
|||
and len(subscription["priceInfo"]["today"]) > 0
|
||||
)
|
||||
|
||||
# Only require today's data - yesterday is optional
|
||||
# If subscription exists but no today data, that's a structural problem
|
||||
is_empty = not has_today
|
||||
# Consider the response valid if EITHER today's intervals OR the priceInfoRange
|
||||
# edges are present. Some markets/accounts (e.g. NL hourly accounts) return an
|
||||
# empty priceInfo(QUARTER_HOURLY).today while priceInfoRange still delivers the
|
||||
# full set of intervals. Requiring today's data alone would wrongly classify
|
||||
# such a complete response as "empty" and block setup. priceInfoRange is the
|
||||
# authoritative source the interval pool uses, so its presence means we have data.
|
||||
is_empty = not has_today and not has_yesterday
|
||||
|
||||
_LOGGER_DETAILS.debug(
|
||||
"Price info check - priceInfoRange: %s, today: %s, is_empty: %s",
|
||||
|
|
|
|||
Loading…
Reference in a new issue