feat(coordinator): implement repairs system for proactive user notifications

Add repair notification system with three auto-clearing repair types:
- Tomorrow data missing (after 18:00)
- API rate limit exceeded (3+ consecutive errors)
- Home not found in Tibber account

Includes:
- coordinator/repairs.py: Complete TibberPricesRepairManager implementation
- Enhanced API error handling with explicit 5xx handling
- Translations for 5 languages (EN, DE, NB, NL, SV)
- Developer documentation in docs/developer/docs/repairs-system.md

Impact: Users receive actionable notifications for important issues instead
of only seeing stale data in logs.
This commit is contained in:
Julian Pawlowski 2025-12-07 20:51:43 +00:00
parent 4bd90ccdee
commit 83be54d5ad
12 changed files with 802 additions and 8 deletions

View file

@ -886,7 +886,24 @@ class TibberPricesApiClient:
headers: dict | None = None,
query_type: TibberPricesQueryType = TibberPricesQueryType.USER,
) -> Any:
"""Get information from the API with rate limiting and retry logic."""
"""
Get information from the API with rate limiting and retry logic.
Exception Handling Strategy:
- AuthenticationError: Immediate raise, triggers reauth flow
- PermissionError: Immediate raise, non-retryable
- CommunicationError: Retry with exponential backoff
- ApiClientError (Rate Limit): Retry with Retry-After delay
- ApiClientError (Other): Retry only if explicitly retryable
- Network errors (aiohttp.ClientError, socket.gaierror, TimeoutError):
Converted to CommunicationError and retried
Retry Logic:
- Max retries: 5 (configurable via _max_retries)
- Base delay: 2 seconds (exponential backoff: 2s, 4s, 8s, 16s, 32s)
- Rate limit delay: Uses Retry-After header or falls back to exponential
- Caps: 30s for network errors, 120s for rate limits, 300s for Retry-After
"""
headers = headers or prepare_headers(self._access_token, self._version)
last_error: Exception | None = None

View file

@ -25,31 +25,79 @@ HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_FORBIDDEN = 403
HTTP_TOO_MANY_REQUESTS = 429
HTTP_INTERNAL_SERVER_ERROR = 500
HTTP_BAD_GATEWAY = 502
HTTP_SERVICE_UNAVAILABLE = 503
HTTP_GATEWAY_TIMEOUT = 504
def verify_response_or_raise(response: aiohttp.ClientResponse) -> None:
"""Verify that the response is valid."""
"""
Verify HTTP response and map to appropriate exceptions.
Error Mapping:
- 401 Unauthorized AuthenticationError (non-retryable)
- 403 Forbidden PermissionError (non-retryable)
- 429 Rate Limit ApiClientError with retry support
- 400 Bad Request ApiClientError (non-retryable, invalid query)
- 5xx Server Errors CommunicationError (retryable)
- Other errors Let aiohttp.raise_for_status() handle
"""
# Authentication failures - non-retryable
if response.status == HTTP_UNAUTHORIZED:
_LOGGER.error("Tibber API authentication failed - check access token")
raise TibberPricesApiClientAuthenticationError(TibberPricesApiClientAuthenticationError.INVALID_CREDENTIALS)
# Permission denied - non-retryable
if response.status == HTTP_FORBIDDEN:
_LOGGER.error("Tibber API access forbidden - insufficient permissions")
raise TibberPricesApiClientPermissionError(TibberPricesApiClientPermissionError.INSUFFICIENT_PERMISSIONS)
# Rate limiting - retryable with explicit delay
if response.status == HTTP_TOO_MANY_REQUESTS:
# Check for Retry-After header that Tibber might send
retry_after = response.headers.get("Retry-After", "unknown")
_LOGGER.warning("Tibber API rate limit exceeded - retry after %s seconds", retry_after)
raise TibberPricesApiClientError(TibberPricesApiClientError.RATE_LIMIT_ERROR.format(retry_after=retry_after))
# Bad request - non-retryable (invalid query)
if response.status == HTTP_BAD_REQUEST:
_LOGGER.error("Tibber API rejected request - likely invalid GraphQL query")
raise TibberPricesApiClientError(
TibberPricesApiClientError.INVALID_QUERY_ERROR.format(message="Bad request - likely invalid GraphQL query")
)
# Server errors 5xx - retryable (temporary server issues)
if response.status in (
HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_GATEWAY,
HTTP_SERVICE_UNAVAILABLE,
HTTP_GATEWAY_TIMEOUT,
):
_LOGGER.warning(
"Tibber API server error %d - temporary issue, will retry",
response.status,
)
# Let this be caught as aiohttp.ClientResponseError in _api_wrapper
# where it's converted to CommunicationError with retry logic
response.raise_for_status()
# All other HTTP errors - let aiohttp handle
response.raise_for_status()
async def verify_graphql_response(response_json: dict, query_type: TibberPricesQueryType) -> None:
"""Verify the GraphQL response for errors and data completeness, including empty data."""
"""
Verify GraphQL response and map error codes to appropriate exceptions.
GraphQL Error Code Mapping:
- UNAUTHENTICATED AuthenticationError (triggers reauth flow)
- FORBIDDEN PermissionError (non-retryable)
- RATE_LIMITED/TOO_MANY_REQUESTS ApiClientError (retryable)
- VALIDATION_ERROR/GRAPHQL_VALIDATION_FAILED ApiClientError (non-retryable)
- Other codes Generic ApiClientError (with code in message)
- Empty data ApiClientError (non-retryable, API has no data)
"""
if "errors" in response_json:
errors = response_json["errors"]
if not errors:

View file

@ -40,6 +40,7 @@ from .data_transformation import TibberPricesDataTransformer
from .listeners import TibberPricesListenerManager
from .midnight_handler import TibberPricesMidnightHandler
from .periods import TibberPricesPeriodCalculator
from .repairs import TibberPricesRepairManager
from .time_service import TibberPricesTimeService
_LOGGER = logging.getLogger(__name__)
@ -224,6 +225,11 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
config_entry=config_entry,
log_prefix=self._log_prefix,
)
self._repair_manager = TibberPricesRepairManager(
hass=hass,
entry_id=config_entry.entry_id,
home_name=config_entry.title,
)
# Register options update listener to invalidate config caches
config_entry.async_on_unload(config_entry.add_update_listener(self._handle_options_update))
@ -504,11 +510,14 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
- Timer #2: Quarter-hour entity updates
- Timer #3: Minute timing sensor updates
Also saves cache to persist any unsaved changes.
Also saves cache to persist any unsaved changes and clears all repairs.
"""
# Cancel all timers first
self._listener_manager.cancel_timers()
# Clear all repairs when integration is removed or disabled
await self._repair_manager.clear_all_repairs()
# Save cache to persist any unsaved data
# This ensures we don't lose data if HA is shutting down
try:
@ -633,6 +642,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Reset lifecycle state on error
self._is_fetching = False
self._lifecycle_state = "error"
# Track rate limit errors for repair system
await self._track_rate_limit_error(err)
# No separate lifecycle notification needed - error case returns data
# which triggers normal async_update_listeners()
return await self._data_fetcher.handle_api_error(
@ -640,8 +653,43 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._transform_data,
)
else:
# Check for repair conditions after successful update
await self._check_repair_conditions(result, current_time)
return result
async def _track_rate_limit_error(self, error: Exception) -> None:
"""Track rate limit errors for repair notification system."""
error_str = str(error).lower()
is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
async def _check_repair_conditions(
self,
result: dict[str, Any],
current_time: datetime,
) -> None:
"""Check and manage repair conditions after successful data update."""
# 1. Home not found detection (home was removed from Tibber account)
if result and result.get("_home_not_found"):
await self._repair_manager.create_home_not_found_repair()
# Remove the marker before returning to entities
result.pop("_home_not_found", None)
else:
# Home exists - clear any existing repair
await self._repair_manager.clear_home_not_found_repair()
# 2. Tomorrow data availability (after 18:00)
if result and "priceInfo" in result:
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
# 3. Clear rate limit tracking on successful API call
await self._repair_manager.clear_rate_limit_tracking()
async def load_cache(self) -> None:
"""Load cached data from storage."""
await self._data_fetcher.load_cache()

View file

@ -5,11 +5,9 @@ from __future__ import annotations
import asyncio
import logging
import secrets
from datetime import timedelta
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from datetime import timedelta
from custom_components.tibber_prices.api import (
TibberPricesApiClientAuthenticationError,
TibberPricesApiClientCommunicationError,
@ -242,6 +240,26 @@ class TibberPricesDataFetcher:
self._log("warning", "Home %s not found in user data, using EUR as default", home_id)
return "EUR"
def _check_home_exists(self, home_id: str) -> bool:
"""
Check if a home ID exists in cached user data.
Args:
home_id: The home ID to check.
Returns:
True if home exists, False otherwise.
"""
if not self._cached_user_data:
# No user data yet - assume home exists (will be checked on next update)
return True
viewer = self._cached_user_data.get("viewer", {})
homes = viewer.get("homes", [])
return any(home.get("id") == home_id for home in homes)
async def handle_main_entry_update(
self,
current_time: datetime,
@ -252,6 +270,17 @@ class TibberPricesDataFetcher:
# Update user data if needed (daily check)
user_data_updated = await self.update_user_data_if_needed(current_time)
# Check if this home still exists in user data after update
# This detects when a home was removed from the Tibber account
home_exists = self._check_home_exists(home_id)
if not home_exists:
self._log("warning", "Home ID %s not found in Tibber account", home_id)
# Return a special marker in the result that coordinator can check
# We still need to return valid data to avoid coordinator errors
result = transform_fn(self._cached_price_data or {})
result["_home_not_found"] = True # Special marker for coordinator
return result
# Check if we need to update price data
should_update = self.should_update_price_data(current_time)
@ -329,3 +358,37 @@ class TibberPricesDataFetcher:
def cached_user_data(self) -> dict[str, Any] | None:
"""Get cached user data."""
return self._cached_user_data
def has_tomorrow_data(self, price_info: list[dict[str, Any]]) -> bool:
"""
Check if tomorrow's price data is available.
Args:
price_info: List of price intervals from coordinator data.
Returns:
True if at least one interval from tomorrow is present.
"""
if not price_info:
return False
# Get tomorrow's date
now = self.time.now()
tomorrow = (self.time.as_local(now) + timedelta(days=1)).date()
# Check if any interval is from tomorrow
for interval in price_info:
if "startsAt" not in interval:
continue
# startsAt is already a datetime object after _transform_data()
interval_time = interval["startsAt"]
if isinstance(interval_time, str):
# Fallback: parse if still string (shouldn't happen with transformed data)
interval_time = self.time.parse_datetime(interval_time)
if interval_time and self.time.as_local(interval_time).date() == tomorrow:
return True
return False

View file

@ -0,0 +1,228 @@
"""
Repair issue management for Tibber Prices integration.
This module handles creation and cleanup of repair issues that notify users
about problems requiring attention in the Home Assistant UI.
Repair Types:
1. Tomorrow Data Missing - Warns when tomorrow's price data is unavailable after 18:00
2. Persistent Rate Limits - Warns when API rate limiting persists after multiple errors
3. Home Not Found - Warns when a home no longer exists in the Tibber account
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from custom_components.tibber_prices.const import DOMAIN
from homeassistant.helpers import issue_registry as ir
if TYPE_CHECKING:
from datetime import datetime
from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__)
# Repair issue tracking thresholds
TOMORROW_DATA_WARNING_HOUR = 18 # Warn after 18:00 if tomorrow data missing
RATE_LIMIT_WARNING_THRESHOLD = 3 # Warn after 3 consecutive rate limit errors
class TibberPricesRepairManager:
"""Manage repair issues for Tibber Prices integration."""
def __init__(self, hass: HomeAssistant, entry_id: str, home_name: str) -> None:
"""
Initialize repair manager.
Args:
hass: Home Assistant instance
entry_id: Config entry ID for this home
home_name: Display name of the home (for user-friendly messages)
"""
self._hass = hass
self._entry_id = entry_id
self._home_name = home_name
# Track consecutive rate limit errors
self._rate_limit_error_count = 0
# Track if repairs are currently active
self._tomorrow_data_repair_active = False
self._rate_limit_repair_active = False
self._home_not_found_repair_active = False
async def check_tomorrow_data_availability(
self,
has_tomorrow_data: bool, # noqa: FBT001 - Clear meaning in context
current_time: datetime,
) -> None:
"""
Check if tomorrow data is available and create/clear repair as needed.
Creates repair if:
- Current hour >= 18:00 (after expected data availability)
- Tomorrow's data is missing
Clears repair if:
- Tomorrow's data is now available
Args:
has_tomorrow_data: Whether tomorrow's data is available
current_time: Current local datetime for hour check
"""
should_warn = current_time.hour >= TOMORROW_DATA_WARNING_HOUR and not has_tomorrow_data
if should_warn and not self._tomorrow_data_repair_active:
await self._create_tomorrow_data_repair()
elif not should_warn and self._tomorrow_data_repair_active:
await self._clear_tomorrow_data_repair()
async def track_rate_limit_error(self) -> None:
"""
Track rate limit error and create repair if threshold exceeded.
Increments rate limit error counter and creates repair issue
if threshold (3 consecutive errors) is reached.
"""
self._rate_limit_error_count += 1
if self._rate_limit_error_count >= RATE_LIMIT_WARNING_THRESHOLD and not self._rate_limit_repair_active:
await self._create_rate_limit_repair()
async def clear_rate_limit_tracking(self) -> None:
"""
Clear rate limit error tracking after successful API call.
Resets counter and clears any active repair issue.
"""
self._rate_limit_error_count = min(self._rate_limit_error_count, 0)
if self._rate_limit_repair_active:
await self._clear_rate_limit_repair()
async def create_home_not_found_repair(self) -> None:
"""
Create repair for home no longer found in Tibber account.
This indicates the home was deleted from the user's Tibber account
but the config entry still exists in Home Assistant.
"""
if self._home_not_found_repair_active:
return
_LOGGER.warning(
"Home '%s' not found in Tibber account - creating repair issue",
self._home_name,
)
ir.async_create_issue(
self._hass,
DOMAIN,
f"home_not_found_{self._entry_id}",
is_fixable=True,
severity=ir.IssueSeverity.ERROR,
translation_key="home_not_found",
translation_placeholders={
"home_name": self._home_name,
"entry_id": self._entry_id,
},
)
self._home_not_found_repair_active = True
async def clear_home_not_found_repair(self) -> None:
"""Clear home not found repair (home is available again or entry removed)."""
if not self._home_not_found_repair_active:
return
_LOGGER.debug("Clearing home not found repair for '%s'", self._home_name)
ir.async_delete_issue(
self._hass,
DOMAIN,
f"home_not_found_{self._entry_id}",
)
self._home_not_found_repair_active = False
async def clear_all_repairs(self) -> None:
"""
Clear all active repair issues.
Called during coordinator shutdown or entry removal.
"""
if self._tomorrow_data_repair_active:
await self._clear_tomorrow_data_repair()
if self._rate_limit_repair_active:
await self._clear_rate_limit_repair()
if self._home_not_found_repair_active:
await self.clear_home_not_found_repair()
async def _create_tomorrow_data_repair(self) -> None:
"""Create repair issue for missing tomorrow data."""
_LOGGER.warning(
"Tomorrow's price data missing after %d:00 for home '%s' - creating repair issue",
TOMORROW_DATA_WARNING_HOUR,
self._home_name,
)
ir.async_create_issue(
self._hass,
DOMAIN,
f"tomorrow_data_missing_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="tomorrow_data_missing",
translation_placeholders={
"home_name": self._home_name,
"warning_hour": str(TOMORROW_DATA_WARNING_HOUR),
},
)
self._tomorrow_data_repair_active = True
async def _clear_tomorrow_data_repair(self) -> None:
"""Clear tomorrow data repair issue."""
_LOGGER.debug("Tomorrow's data now available for '%s' - clearing repair issue", self._home_name)
ir.async_delete_issue(
self._hass,
DOMAIN,
f"tomorrow_data_missing_{self._entry_id}",
)
self._tomorrow_data_repair_active = False
async def _create_rate_limit_repair(self) -> None:
"""Create repair issue for persistent rate limiting."""
_LOGGER.warning(
"Persistent API rate limiting detected for home '%s' (%d consecutive errors) - creating repair issue",
self._home_name,
self._rate_limit_error_count,
)
ir.async_create_issue(
self._hass,
DOMAIN,
f"rate_limit_exceeded_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="rate_limit_exceeded",
translation_placeholders={
"home_name": self._home_name,
"error_count": str(self._rate_limit_error_count),
},
)
self._rate_limit_repair_active = True
async def _clear_rate_limit_repair(self) -> None:
"""Clear rate limit repair issue."""
_LOGGER.debug("Rate limiting resolved for '%s' - clearing repair issue", self._home_name)
ir.async_delete_issue(
self._hass,
DOMAIN,
f"rate_limit_exceeded_{self._entry_id}",
)
self._rate_limit_repair_active = False

View file

@ -834,6 +834,18 @@
"homes_removed": {
"title": "Tibber-Häuser entfernt",
"description": "Wir haben erkannt, dass {count} Zuhause aus deinem Tibber-Konto entfernt wurde(n): {homes}. Bitte überprüfe deine Tibber-Integrationskonfiguration."
},
"tomorrow_data_missing": {
"title": "Preisdaten für morgen fehlen für {home_name}",
"description": "Die Strompreisdaten für morgen sind nach {warning_hour}:00 Uhr immer noch nicht verfügbar. Das ist ungewöhnlich, da Tibber normalerweise die Preise für morgen am Nachmittag veröffentlicht (ca. 13:00-14:00 Uhr MEZ).\n\nMögliche Ursachen:\n- Tibber hat die Preise für morgen noch nicht veröffentlicht\n- Temporäre API-Probleme\n- Dein Stromanbieter hat die Preise noch nicht an Tibber übermittelt\n\nDieses Problem löst sich automatisch, sobald die Daten für morgen verfügbar sind. Falls dies nach 20:00 Uhr weiterhin besteht, prüfe bitte die Tibber-App oder kontaktiere den Tibber-Support."
},
"rate_limit_exceeded": {
"title": "API-Ratenlimit erreicht für {home_name}",
"description": "Die Tibber-API hat diese Integration nach {error_count} aufeinanderfolgenden Fehlern ratenlimitiert. Das bedeutet, dass Anfragen zu häufig gestellt werden.\n\nDie Integration wird automatisch mit zunehmenden Verzögerungen erneut versuchen. Dieses Problem löst sich, sobald das Ratenlimit abläuft.\n\nFalls dies mehrere Stunden anhält, überprüfe:\n- Ob mehrere Home Assistant Instanzen denselben API-Token verwenden\n- Ob andere Anwendungen deinen Tibber-API-Token stark nutzen\n- Die Update-Frequenz reduzieren, falls du sie angepasst hast"
},
"home_not_found": {
"title": "Zuhause '{home_name}' nicht im Tibber-Konto gefunden",
"description": "Das in dieser Integration konfigurierte Zuhause (Eintrag-ID: {entry_id}) ist nicht mehr in deinem Tibber-Konto verfügbar. Dies passiert normalerweise, wenn:\n- Das Zuhause aus deinem Tibber-Konto gelöscht wurde\n- Das Zuhause zu einem anderen Tibber-Konto verschoben wurde\n- Der Zugriff auf dieses Zuhause widerrufen wurde\n\nBitte entferne diesen Integrationseintrag und füge ihn erneut hinzu, falls das Zuhause weiterhin überwacht werden soll. Um diesen Eintrag zu entfernen, gehe zu Einstellungen → Geräte & Dienste → Tibber Prices und lösche die Konfiguration '{home_name}'."
}
},
"services": {

View file

@ -834,6 +834,18 @@
"homes_removed": {
"title": "Tibber homes removed",
"description": "We detected that {count} home(s) have been removed from your Tibber account: {homes}. Please review your Tibber integration configuration."
},
"tomorrow_data_missing": {
"title": "Tomorrow's price data missing for {home_name}",
"description": "Tomorrow's electricity price data is still unavailable after {warning_hour}:00. This is unusual, as Tibber typically publishes tomorrow's prices in the afternoon (around 13:00-14:00 CET).\n\nPossible causes:\n- Tibber has not yet published tomorrow's prices\n- Temporary API issues\n- Your electricity provider has not submitted prices to Tibber\n\nThis issue will automatically resolve once tomorrow's data becomes available. If this persists beyond 20:00, please check the Tibber app or contact Tibber support."
},
"rate_limit_exceeded": {
"title": "API rate limit exceeded for {home_name}",
"description": "The Tibber API has rate-limited this integration after {error_count} consecutive errors. This means requests are being made too frequently.\n\nThe integration will automatically retry with increasing delays. This issue will resolve once the rate limit expires.\n\nIf this persists for several hours, consider:\n- Checking if multiple Home Assistant instances are using the same API token\n- Verifying no other applications are heavily using your Tibber API token\n- Reducing the update frequency if you've customized it"
},
"home_not_found": {
"title": "Home '{home_name}' not found in Tibber account",
"description": "The home configured in this integration (entry ID: {entry_id}) is no longer available in your Tibber account. This typically happens when:\n- The home was deleted from your Tibber account\n- The home was moved to a different Tibber account\n- Access to this home was revoked\n\nPlease remove this integration entry and re-add it if the home should still be monitored. To remove this entry, go to Settings → Devices & Services → Tibber Prices and delete the '{home_name}' configuration."
}
},
"services": {

View file

@ -834,6 +834,18 @@
"homes_removed": {
"title": "Tibber-hjem fjernet",
"description": "Vi oppdaget at {count} hjem har blitt fjernet fra din Tibber-konto: {homes}. Vennligst gjennomgå din Tibber-integrasjonskonfigurasjon."
},
"tomorrow_data_missing": {
"title": "Prisdata for i morgen mangler for {home_name}",
"description": "Strømprisdata for i morgen er fortsatt utilgjengelig etter {warning_hour}:00. Dette er uvanlig, da Tibber vanligvis publiserer morgendagens priser på ettermiddagen (rundt 13:00-14:00 CET).\n\nMulige årsaker:\n- Tibber har ikke publisert morgendagens priser ennå\n- Midlertidige API-problemer\n- Strømleverandøren din har ikke sendt inn priser til Tibber\n\nDette problemet vil løse seg automatisk når morgendagens data blir tilgjengelig. Hvis dette vedvarer etter 20:00, vennligst sjekk Tibber-appen eller kontakt Tibber-support."
},
"rate_limit_exceeded": {
"title": "API-hastighetsbegrensning overskredet for {home_name}",
"description": "Tibber-APIet har hastighetsbegrenset denne integrasjonen etter {error_count} påfølgende feil. Dette betyr at forespørsler blir gjort for hyppig.\n\nIntegrasjonen vil automatisk prøve på nytt med økende forsinkelser. Dette problemet vil løse seg når hastighetsbegrensningen utløper.\n\nHvis dette vedvarer i flere timer, vurder:\n- Å sjekke om flere Home Assistant-instanser bruker samme API-token\n- Å verifisere at ingen andre applikasjoner bruker Tibber-API-tokenet ditt mye\n- Å redusere oppdateringsfrekvensen hvis du har tilpasset den"
},
"home_not_found": {
"title": "Hjemmet '{home_name}' ble ikke funnet i Tibber-kontoen",
"description": "Hjemmet konfigurert i denne integrasjonen (oppførings-ID: {entry_id}) er ikke lenger tilgjengelig i Tibber-kontoen din. Dette skjer vanligvis når:\n- Hjemmet ble slettet fra Tibber-kontoen din\n- Hjemmet ble flyttet til en annen Tibber-konto\n- Tilgang til dette hjemmet ble tilbakekalt\n\nVennligst fjern denne integrasjonsoppføringen og legg den til på nytt hvis hjemmet fortsatt skal overvåkes. For å fjerne denne oppføringen, gå til Innstillinger → Enheter og tjenester → Tibber Prices og slett '{home_name}'-konfigurasjonen."
}
},
"services": {

View file

@ -834,6 +834,18 @@
"homes_removed": {
"title": "Tibber-huizen verwijderd",
"description": "We hebben gedetecteerd dat {count} huis/huizen zijn verwijderd van je Tibber-account: {homes}. Controleer je Tibber-integratieconfiguratie."
},
"tomorrow_data_missing": {
"title": "Prijsgegevens voor morgen ontbreken voor {home_name}",
"description": "De elektriciteitsprijsgegevens voor morgen zijn nog steeds niet beschikbaar na {warning_hour}:00 uur. Dit is ongebruikelijk, aangezien Tibber doorgaans de prijzen voor morgen in de middag publiceert (rond 13:00-14:00 CET).\n\nMogelijke oorzaken:\n- Tibber heeft de prijzen voor morgen nog niet gepubliceerd\n- Tijdelijke API-problemen\n- Je elektriciteitsleverancier heeft nog geen prijzen aan Tibber ingediend\n\nDit probleem wordt automatisch opgelost zodra de gegevens voor morgen beschikbaar zijn. Als dit na 20:00 uur aanhoudt, controleer dan de Tibber-app of neem contact op met Tibber-ondersteuning."
},
"rate_limit_exceeded": {
"title": "API-snelheidslimiet overschreden voor {home_name}",
"description": "De Tibber-API heeft deze integratie beperkt na {error_count} opeenvolgende fouten. Dit betekent dat verzoeken te vaak worden gedaan.\n\nDe integratie zal automatisch opnieuw proberen met toenemende vertragingen. Dit probleem wordt opgelost zodra de snelheidslimiet verloopt.\n\nAls dit meerdere uren aanhoudt, overweeg dan:\n- Controleren of meerdere Home Assistant-instanties hetzelfde API-token gebruiken\n- Verifiëren dat geen andere applicaties je Tibber-API-token intensief gebruiken\n- De updatefrequentie verminderen als je deze hebt aangepast"
},
"home_not_found": {
"title": "Huis '{home_name}' niet gevonden in Tibber-account",
"description": "Het huis geconfigureerd in deze integratie (entry ID: {entry_id}) is niet langer beschikbaar in je Tibber-account. Dit gebeurt meestal wanneer:\n- Het huis is verwijderd uit je Tibber-account\n- Het huis is verplaatst naar een ander Tibber-account\n- Toegang tot dit huis is ingetrokken\n\nVerwijder deze integratie-entry en voeg deze opnieuw toe als het huis nog steeds moet worden gemonitord. Om deze entry te verwijderen, ga naar Instellingen → Apparaten & Diensten → Tibber Prices en verwijder de '{home_name}'-configuratie."
}
},
"services": {

View file

@ -834,6 +834,18 @@
"homes_removed": {
"title": "Tibber-hem borttagna",
"description": "Vi upptäckte att {count} hem har tagits bort från ditt Tibber-konto: {homes}. Vänligen granska din Tibber-integrationskonfiguration."
},
"tomorrow_data_missing": {
"title": "Prisdata för imorgon saknas för {home_name}",
"description": "Elprisdata för imorgon är fortfarande otillgänglig efter {warning_hour}:00. Detta är ovanligt, eftersom Tibber vanligtvis publicerar morgondagens priser på eftermiddagen (runt 13:00-14:00 CET).\n\nMöjliga orsaker:\n- Tibber har inte publicerat morgondagens priser ännu\n- Tillfälliga API-problem\n- Din elleverantör har inte skickat in priser till Tibber\n\nDetta problem kommer att lösas automatiskt när morgondagens data blir tillgänglig. Om detta fortsätter efter 20:00, vänligen kontrollera Tibber-appen eller kontakta Tibber-support."
},
"rate_limit_exceeded": {
"title": "API-hastighetsgräns överskriden för {home_name}",
"description": "Tibber-API:et har hastighetsbegränsat denna integration efter {error_count} på varandra följande fel. Detta betyder att förfrågningar görs för ofta.\n\nIntegrationen kommer automatiskt att försöka igen med ökande fördröjningar. Detta problem kommer att lösas när hastighetsgränsen löper ut.\n\nOm detta fortsätter i flera timmar, överväg:\n- Att kontrollera om flera Home Assistant-instanser använder samma API-token\n- Att verifiera att inga andra applikationer använder din Tibber-API-token mycket\n- Att minska uppdateringsfrekvensen om du har anpassat den"
},
"home_not_found": {
"title": "Hemmet '{home_name}' hittades inte i Tibber-kontot",
"description": "Hemmet som konfigurerats i denna integration (post-ID: {entry_id}) är inte längre tillgängligt i ditt Tibber-konto. Detta händer vanligtvis när:\n- Hemmet togs bort från ditt Tibber-konto\n- Hemmet flyttades till ett annat Tibber-konto\n- Åtkomst till detta hem återkallades\n\nVänligen ta bort denna integrationspost och lägg till den igen om hemmet fortfarande ska övervakas. För att ta bort denna post, gå till Inställningar → Enheter och tjänster → Tibber Prices och ta bort '{home_name}'-konfigurationen."
}
},
"services": {

View file

@ -0,0 +1,330 @@
# Repairs System
The Tibber Prices integration includes a proactive repair notification system that alerts users to important issues requiring attention. This system leverages Home Assistant's built-in `issue_registry` to create user-facing notifications in the UI.
## Overview
The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle.
**Design Principles:**
- **Proactive**: Detect issues before they become critical
- **User-friendly**: Clear explanations with actionable guidance
- **Auto-clearing**: Repairs automatically disappear when conditions resolve
- **Non-blocking**: Integration continues to work even with active repairs
## Implemented Repair Types
### 1. Tomorrow Data Missing
**Issue ID:** `tomorrow_data_missing_{entry_id}`
**When triggered:**
- Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`)
- Tomorrow's electricity price data is still not available
**When cleared:**
- Tomorrow's data becomes available
- Automatically checks on every successful API update
**User impact:**
Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work.
**Implementation:**
```python
# In coordinator update cycle
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
```
**Translation placeholders:**
- `home_name`: Name of the affected home
- `warning_hour`: Hour after which warning appears (default: 18)
### 2. Rate Limit Exceeded
**Issue ID:** `rate_limit_exceeded_{entry_id}`
**When triggered:**
- Integration encounters 3 or more consecutive rate limit errors (HTTP 429)
- Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD`
**When cleared:**
- Successful API call completes (no rate limit error)
- Error counter resets to 0
**User impact:**
API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires.
**Implementation:**
```python
# In error handler
is_rate_limit = (
"429" in error_str
or "rate limit" in error_str
or "too many requests" in error_str
)
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
# On successful update
await self._repair_manager.clear_rate_limit_tracking()
```
**Translation placeholders:**
- `home_name`: Name of the affected home
- `error_count`: Number of consecutive rate limit errors
### 3. Home Not Found
**Issue ID:** `home_not_found_{entry_id}`
**When triggered:**
- Home configured in this integration is no longer present in Tibber account
- Detected during user data refresh (daily check)
**When cleared:**
- Home reappears in Tibber account (unlikely - manual cleanup expected)
- Integration entry is removed (shutdown cleanup)
**User impact:**
Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed.
**Implementation:**
```python
# After user data update
home_exists = self._data_fetcher._check_home_exists(home_id)
if not home_exists:
await self._repair_manager.create_home_not_found_repair()
else:
await self._repair_manager.clear_home_not_found_repair()
```
**Translation placeholders:**
- `home_name`: Name of the missing home
- `entry_id`: Config entry ID for reference
## Configuration Constants
Defined in `coordinator/constants.py`:
```python
TOMORROW_DATA_WARNING_HOUR = 18 # Hour after which to warn about missing tomorrow data
RATE_LIMIT_WARNING_THRESHOLD = 3 # Number of consecutive errors before creating repair
```
## Architecture
### Class Structure
```python
class TibberPricesRepairManager:
"""Manages repair issues for a single Tibber home."""
def __init__(
self,
hass: HomeAssistant,
entry_id: str,
home_name: str,
) -> None:
"""Initialize repair manager."""
self._hass = hass
self._entry_id = entry_id
self._home_name = home_name
# State tracking
self._tomorrow_data_repair_active = False
self._rate_limit_error_count = 0
self._rate_limit_repair_active = False
self._home_not_found_repair_active = False
```
### State Tracking
Each repair type maintains internal state to avoid redundant operations:
- **`_tomorrow_data_repair_active`**: Boolean flag, prevents creating duplicate repairs
- **`_rate_limit_error_count`**: Integer counter, tracks consecutive errors
- **`_rate_limit_repair_active`**: Boolean flag, tracks repair status
- **`_home_not_found_repair_active`**: Boolean flag, one-time repair (manual cleanup)
### Lifecycle Integration
**Coordinator Initialization:**
```python
self._repair_manager = TibberPricesRepairManager(
hass=hass,
entry_id=self.config_entry.entry_id,
home_name=self._home_name,
)
```
**Update Cycle Integration:**
```python
# Success path - check conditions
if result and "priceInfo" in result:
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
await self._repair_manager.clear_rate_limit_tracking()
# Error path - track rate limits
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
```
**Shutdown Cleanup:**
```python
async def async_shutdown(self) -> None:
"""Shut down coordinator and clean up."""
await self._repair_manager.clear_all_repairs()
# ... other cleanup ...
```
## Translation System
Repairs use Home Assistant's standard translation system. Translations are defined in:
- `/translations/en.json`
- `/translations/de.json`
- `/translations/nb.json`
- `/translations/nl.json`
- `/translations/sv.json`
**Structure:**
```json
{
"issues": {
"tomorrow_data_missing": {
"title": "Tomorrow's price data missing for {home_name}",
"description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2"
}
}
}
```
## Home Assistant Integration
Repairs appear in:
- **Settings → System → Repairs** (main repairs panel)
- **Notifications** (bell icon in UI shows repair count)
Repair properties:
- **`is_fixable=False`**: No automated fix available (user action required)
- **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical)
- **`translation_key`**: References `issues.{key}` in translation files
## Testing Repairs
### Tomorrow Data Missing
1. Wait until after 18:00 local time
2. Ensure integration has no tomorrow price data
3. Repair should appear in UI
4. When tomorrow data arrives (next API fetch), repair clears
**Manual trigger:**
```python
# Temporarily set warning hour to current hour for testing
TOMORROW_DATA_WARNING_HOUR = datetime.now().hour
```
### Rate Limit Exceeded
1. Simulate 3+ consecutive rate limit errors
2. Repair should appear after 3rd error
3. Successful API call clears the repair
**Manual test:**
- Reduce API polling interval to trigger rate limiting
- Or temporarily return HTTP 429 in API client
### Home Not Found
1. Remove home from Tibber account via app/web
2. Wait for user data refresh (daily check)
3. Repair appears indicating home is missing
4. Remove integration entry to clear repair
## Adding New Repair Types
To add a new repair type:
1. **Add constants** (if needed) in `coordinator/constants.py`
2. **Add state tracking** in `TibberPricesRepairManager.__init__`
3. **Implement check method** with create/clear logic
4. **Add translations** to all 5 language files
5. **Integrate into coordinator** update cycle or error handlers
6. **Add cleanup** to `clear_all_repairs()` method
7. **Document** in this file
**Example template:**
```python
async def check_new_condition(self, *, param: bool) -> None:
"""Check new condition and create/clear repair."""
should_warn = param # Your condition logic
if should_warn and not self._new_repair_active:
await self._create_new_repair()
elif not should_warn and self._new_repair_active:
await self._clear_new_repair()
async def _create_new_repair(self) -> None:
"""Create new repair issue."""
_LOGGER.warning("New issue detected - creating repair")
ir.async_create_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="new_issue",
translation_placeholders={
"home_name": self._home_name,
},
)
self._new_repair_active = True
async def _clear_new_repair(self) -> None:
"""Clear new repair issue."""
_LOGGER.debug("New issue resolved - clearing repair")
ir.async_delete_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
)
self._new_repair_active = False
```
## Best Practices
1. **Always use state tracking** - Prevents duplicate repair creation
2. **Auto-clear when resolved** - Improves user experience
3. **Clear on shutdown** - Prevents orphaned repairs
4. **Use descriptive issue IDs** - Include entry_id for multi-home setups
5. **Provide actionable guidance** - Tell users what they can do
6. **Use appropriate severity** - WARNING for most cases, ERROR only for critical
7. **Test all language translations** - Ensure placeholders work correctly
8. **Document expected behavior** - What triggers, what clears, what user should do
## Future Enhancements
Potential additions to the repairs system:
- **Stale data warning**: Alert when cache is >24 hours old with no API updates
- **Missing permissions**: Detect insufficient API token scopes
- **Config migration needed**: Notify users of breaking changes requiring reconfiguration
- **Extreme price alert**: Warn when prices exceed historical thresholds (optional, user-configurable)
## References
- Home Assistant Repairs Documentation: https://developers.home-assistant.io/docs/core/platform/repairs
- Issue Registry API: `homeassistant.helpers.issue_registry`
- Integration Constants: `custom_components/tibber_prices/const.py`
- Repair Manager Implementation: `custom_components/tibber_prices/coordinator/repairs.py`

View file

@ -25,7 +25,7 @@ const sidebars: SidebarsConfig = {
{
type: 'category',
label: '💻 Development',
items: ['setup', 'coding-guidelines', 'critical-patterns', 'debugging'],
items: ['setup', 'coding-guidelines', 'critical-patterns', 'repairs-system', 'debugging'],
collapsible: true,
collapsed: false,
},