mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 13:23:41 +00:00
Implemented comprehensive entity lifecycle patterns following Home Assistant best practices for proper state management and history tracking. Changes: - entity.py: Added available property to base class - Returns False when coordinator has no data or last_update_success=False - Prevents entities from showing stale data during errors - Auth failures trigger reauth flow via ConfigEntryAuthFailed - sensor/core.py: Added state restore and background task handling - Changed inheritance: SensorEntity → RestoreSensor - Restore native_value from SensorExtraStoredData in async_added_to_hass() - Chart sensors restore response data from attributes - Converted blocking service calls to background tasks using hass.async_create_task() - Eliminates 194ms setup warning by making async_added_to_hass non-blocking - binary_sensor/core.py: Added state restore and force_update - Changed inheritance: BinarySensorEntity → RestoreEntity + BinarySensorEntity - Restore is_on state in async_added_to_hass() - Added available property override for connection sensor (always True) - Added force_update property for connection sensor to track all state changes - Other binary sensors use base available logic - AGENTS.md: Documented entity lifecycle patterns in Common Pitfalls - Added "Entity Lifecycle & State Management" section - Documents available, state restore, and force_update patterns - Explains why each pattern matters for proper HA integration Impact: Entities no longer show stale data during errors, history has no gaps after HA restart, connection state changes are properly tracked, and config entry setup completes in <200ms (under HA threshold). All patterns verified against HA developer documentation: https://developers.home-assistant.io/docs/core/entity/
138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
"""TibberPricesEntity class."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from .const import ATTRIBUTION, DOMAIN, get_home_type_translation, get_translation
|
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
|
|
|
|
|
class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
|
|
"""TibberPricesEntity class."""
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None:
|
|
"""Initialize."""
|
|
super().__init__(coordinator)
|
|
|
|
# Get device information
|
|
home_name, home_id, home_type = self._get_device_info()
|
|
|
|
# Get configured language
|
|
language = coordinator.hass.config.language or "en"
|
|
|
|
# Get translated home type and attribution
|
|
translated_model = get_home_type_translation(home_type, language) if home_type else "Unknown"
|
|
# Get translated attribution, fallback to constant if translation not found
|
|
self._attr_attribution = get_translation(["attribution"], language) or ATTRIBUTION
|
|
|
|
self._attr_device_info = DeviceInfo(
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
identifiers={
|
|
(
|
|
DOMAIN,
|
|
coordinator.config_entry.unique_id or coordinator.config_entry.entry_id,
|
|
)
|
|
},
|
|
name=home_name,
|
|
manufacturer="Tibber",
|
|
model=translated_model,
|
|
serial_number=home_id if home_id else None,
|
|
configuration_url="https://developer.tibber.com/explorer",
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""
|
|
Return if entity is available.
|
|
|
|
Entity is unavailable when:
|
|
- Coordinator has not completed first update (no data yet)
|
|
- Coordinator has encountered an error (last_update_success = False)
|
|
|
|
Note: Auth failures are handled by coordinator's update method,
|
|
which raises ConfigEntryAuthFailed and triggers reauth flow.
|
|
"""
|
|
# Return False if coordinator not ready or has errors
|
|
# Return True if coordinator has data (bool conversion handles None/empty)
|
|
return self.coordinator.last_update_success and bool(self.coordinator.data)
|
|
|
|
def _get_device_info(self) -> tuple[str, str | None, str | None]:
|
|
"""Get device name, ID and type."""
|
|
user_profile = self.coordinator.get_user_profile()
|
|
is_subentry = bool(self.coordinator.config_entry.data.get("home_id"))
|
|
home_id = self.coordinator.config_entry.unique_id
|
|
home_type = None
|
|
|
|
if is_subentry:
|
|
home_name, home_id, home_type = self._get_subentry_device_info()
|
|
elif user_profile:
|
|
home_name = self._get_main_entry_device_info(user_profile)
|
|
else:
|
|
home_name, home_type = self._get_fallback_device_info()
|
|
|
|
return home_name, home_id, home_type
|
|
|
|
def _get_subentry_device_info(self) -> tuple[str, str | None, str | None]:
|
|
"""Get device info for subentry."""
|
|
home_data = self.coordinator.config_entry.data.get("home_data", {})
|
|
home_id = self.coordinator.config_entry.data.get("home_id")
|
|
|
|
# Get home details
|
|
address = home_data.get("address", {})
|
|
address1 = address.get("address1", "")
|
|
city = address.get("city", "")
|
|
app_nickname = home_data.get("appNickname", "")
|
|
home_type = home_data.get("type", "")
|
|
|
|
# Compose home name
|
|
if app_nickname and app_nickname.strip():
|
|
# If appNickname is set, use it as-is (don't add city)
|
|
home_name = app_nickname.strip()
|
|
elif address1:
|
|
# If no appNickname, use address and optionally add city
|
|
home_name = address1
|
|
if city:
|
|
home_name = f"{home_name}, {city}"
|
|
else:
|
|
# Fallback to home ID
|
|
home_name = f"Tibber Home {home_id}"
|
|
|
|
return home_name, home_id, home_type
|
|
|
|
def _get_main_entry_device_info(self, user_profile: dict) -> str:
|
|
"""Get device info for main entry."""
|
|
user_name = user_profile.get("name", "Tibber User")
|
|
user_email = user_profile.get("email", "")
|
|
home_name = f"Tibber - {user_name}"
|
|
if user_email:
|
|
home_name = f"{home_name} ({user_email})"
|
|
return home_name
|
|
|
|
def _get_fallback_device_info(self) -> tuple[str, str | None]:
|
|
"""Get fallback device info if user data not available yet."""
|
|
if not self.coordinator.data:
|
|
return "Tibber Home", None
|
|
|
|
try:
|
|
address1 = str(self.coordinator.data.get("address", {}).get("address1", ""))
|
|
city = str(self.coordinator.data.get("address", {}).get("city", ""))
|
|
app_nickname = str(self.coordinator.data.get("appNickname", ""))
|
|
home_type = str(self.coordinator.data.get("type", ""))
|
|
|
|
# Compose a nice name
|
|
if app_nickname and app_nickname.strip():
|
|
home_name = f"Tibber {app_nickname.strip()}"
|
|
elif address1:
|
|
home_name = f"Tibber {address1}"
|
|
if city:
|
|
home_name = f"{home_name}, {city}"
|
|
else:
|
|
home_name = "Tibber Home"
|
|
except (KeyError, IndexError, TypeError):
|
|
return "Tibber Home", None
|
|
else:
|
|
return home_name, home_type
|