mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
add descriptions
This commit is contained in:
parent
51b028e9b7
commit
3d33d8d6bc
10 changed files with 629 additions and 98 deletions
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers.storage import Store
|
|||
from homeassistant.loader import async_get_loaded_integration
|
||||
|
||||
from .api import TibberPricesApiClient
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN, LOGGER, async_load_translations
|
||||
from .coordinator import STORAGE_VERSION, TibberPricesDataUpdateCoordinator
|
||||
from .data import TibberPricesData
|
||||
|
||||
|
|
@ -37,6 +37,13 @@ async def async_setup_entry(
|
|||
entry: TibberPricesConfigEntry,
|
||||
) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
# Preload translations to populate the cache
|
||||
await async_load_translations(hass, "en")
|
||||
|
||||
# Try to load translations for the user's configured language if not English
|
||||
if hass.config.language and hass.config.language != "en":
|
||||
await async_load_translations(hass, hass.config.language)
|
||||
|
||||
coordinator = TibberPricesDataUpdateCoordinator(
|
||||
hass=hass,
|
||||
entry=entry,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ from __future__ import annotations
|
|||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
|
|
@ -18,6 +15,8 @@ from homeassistant.const import EntityCategory
|
|||
from .entity import TibberPricesEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
|
|
@ -194,13 +193,63 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes."""
|
||||
async def async_extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes asynchronously."""
|
||||
try:
|
||||
if not self.coordinator.data or not self._attribute_getter:
|
||||
# Get the dynamic attributes if the getter is available
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
return self._attribute_getter()
|
||||
attributes = {}
|
||||
if self._attribute_getter:
|
||||
dynamic_attrs = self._attribute_getter()
|
||||
if dynamic_attrs:
|
||||
attributes.update(dynamic_attrs)
|
||||
|
||||
# Add descriptions from the custom translations file
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Get user's language preference
|
||||
language = self.hass.config.language if self.hass.config.language else "en"
|
||||
|
||||
# Import async function to get descriptions
|
||||
from .const import (
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
async_get_entity_description,
|
||||
)
|
||||
|
||||
# Add basic description
|
||||
description = await async_get_entity_description(
|
||||
self.hass, "binary_sensor", self.entity_description.translation_key, language, "description"
|
||||
)
|
||||
if description:
|
||||
attributes["description"] = description
|
||||
|
||||
# Check if extended descriptions are enabled in the config
|
||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
)
|
||||
|
||||
# Add extended descriptions if enabled
|
||||
if extended_descriptions:
|
||||
# Add long description if available
|
||||
long_desc = await async_get_entity_description(
|
||||
self.hass,
|
||||
"binary_sensor",
|
||||
self.entity_description.translation_key,
|
||||
language,
|
||||
"long_description",
|
||||
)
|
||||
if long_desc:
|
||||
attributes["long_description"] = long_desc
|
||||
|
||||
# Add usage tips if available
|
||||
usage_tips = await async_get_entity_description(
|
||||
self.hass, "binary_sensor", self.entity_description.translation_key, language, "usage_tips"
|
||||
)
|
||||
if usage_tips:
|
||||
attributes["usage_tips"] = usage_tips
|
||||
|
||||
except (KeyError, ValueError, TypeError) as ex:
|
||||
self.coordinator.logger.exception(
|
||||
|
|
@ -211,3 +260,72 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
},
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return attributes if attributes else None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes synchronously."""
|
||||
try:
|
||||
# Start with dynamic attributes if available
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
attributes = {}
|
||||
if self._attribute_getter:
|
||||
dynamic_attrs = self._attribute_getter()
|
||||
if dynamic_attrs:
|
||||
attributes.update(dynamic_attrs)
|
||||
|
||||
# Add descriptions from the cache (non-blocking)
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Get user's language preference
|
||||
language = self.hass.config.language if self.hass.config.language else "en"
|
||||
|
||||
# Import synchronous function to get cached descriptions
|
||||
from .const import (
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
get_entity_description,
|
||||
)
|
||||
|
||||
# Add basic description from cache
|
||||
description = get_entity_description(
|
||||
"binary_sensor", self.entity_description.translation_key, language, "description"
|
||||
)
|
||||
if description:
|
||||
attributes["description"] = description
|
||||
|
||||
# Check if extended descriptions are enabled in the config
|
||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
)
|
||||
|
||||
# Add extended descriptions if enabled (from cache only)
|
||||
if extended_descriptions:
|
||||
# Add long description if available in cache
|
||||
long_desc = get_entity_description(
|
||||
"binary_sensor", self.entity_description.translation_key, language, "long_description"
|
||||
)
|
||||
if long_desc:
|
||||
attributes["long_description"] = long_desc
|
||||
|
||||
# Add usage tips if available in cache
|
||||
usage_tips = get_entity_description(
|
||||
"binary_sensor", self.entity_description.translation_key, language, "usage_tips"
|
||||
)
|
||||
if usage_tips:
|
||||
attributes["usage_tips"] = usage_tips
|
||||
|
||||
except (KeyError, ValueError, TypeError) as ex:
|
||||
self.coordinator.logger.exception(
|
||||
"Error getting binary sensor attributes",
|
||||
extra={
|
||||
"error": str(ex),
|
||||
"entity": self.entity_description.key,
|
||||
},
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return attributes if attributes else None
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ from .api import (
|
|||
TibberPricesApiClientCommunicationError,
|
||||
TibberPricesApiClientError,
|
||||
)
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
|
||||
class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
|
@ -48,9 +53,7 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
_errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
name = await self._test_credentials(
|
||||
access_token=user_input[CONF_ACCESS_TOKEN]
|
||||
)
|
||||
name = await self._test_credentials(access_token=user_input[CONF_ACCESS_TOKEN])
|
||||
except TibberPricesApiClientAuthenticationError as exception:
|
||||
LOGGER.warning(exception)
|
||||
_errors["base"] = "auth"
|
||||
|
|
@ -74,14 +77,16 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
{
|
||||
vol.Required(
|
||||
CONF_ACCESS_TOKEN,
|
||||
default=(user_input or {}).get(
|
||||
CONF_ACCESS_TOKEN, vol.UNDEFINED
|
||||
),
|
||||
default=(user_input or {}).get(CONF_ACCESS_TOKEN, vol.UNDEFINED),
|
||||
): selector.TextSelector(
|
||||
selector.TextSelectorConfig(
|
||||
type=selector.TextSelectorType.TEXT,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
default=(user_input or {}).get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
): selector.BooleanSelector(),
|
||||
},
|
||||
),
|
||||
errors=_errors,
|
||||
|
|
@ -103,12 +108,9 @@ class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__()
|
||||
# Store the entry_id instead of the whole config_entry
|
||||
self._entry_id = config_entry.entry_id
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict | None = None
|
||||
) -> config_entries.ConfigFlowResult:
|
||||
async def async_step_init(self, user_input: dict | None = None) -> config_entries.ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
|
|
@ -122,20 +124,15 @@ class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
result = await client.async_test_connection()
|
||||
new_account_name = result["viewer"]["name"]
|
||||
|
||||
# Get the config entry using the entry_id
|
||||
config_entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||
if not config_entry:
|
||||
return self.async_abort(reason="entry_not_found")
|
||||
|
||||
# Check if this token is for the same account
|
||||
current_unique_id = config_entry.unique_id
|
||||
current_unique_id = self.config_entry.unique_id
|
||||
new_unique_id = slugify(new_account_name)
|
||||
|
||||
if current_unique_id != new_unique_id:
|
||||
# Token is for a different account
|
||||
errors["base"] = "different_account"
|
||||
else:
|
||||
# Update the config entry with the new access token
|
||||
# Update the config entry with the new access token and options
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
except TibberPricesApiClientAuthenticationError as exception:
|
||||
|
|
@ -148,25 +145,27 @@ class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
LOGGER.exception(exception)
|
||||
errors["base"] = "unknown"
|
||||
|
||||
# Get current config entry to get the current access token
|
||||
config_entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||
if not config_entry:
|
||||
return self.async_abort(reason="entry_not_found")
|
||||
|
||||
# If there's no user input or if there were errors, show the form
|
||||
schema = {
|
||||
# Build options schema
|
||||
options = {
|
||||
vol.Required(
|
||||
CONF_ACCESS_TOKEN,
|
||||
default=config_entry.data.get(CONF_ACCESS_TOKEN, ""),
|
||||
default=self.config_entry.data.get(CONF_ACCESS_TOKEN, ""),
|
||||
): selector.TextSelector(
|
||||
selector.TextSelectorConfig(
|
||||
type=selector.TextSelectorType.TEXT,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
self.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
),
|
||||
): selector.BooleanSelector(),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
data_schema=vol.Schema(options),
|
||||
errors=errors,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,181 @@
|
|||
"""Constants for tibber_prices."""
|
||||
"""Constants for the Tibber Price Analytics integration."""
|
||||
|
||||
from logging import Logger, getLogger
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
LOGGER: Logger = getLogger(__package__)
|
||||
import aiofiles
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
# Version of the integration
|
||||
VERSION = "1.0.0"
|
||||
|
||||
NAME = "Tibber Price Information & Ratings"
|
||||
VERSION = "0.1.0" # Must match version in manifest.json
|
||||
DOMAIN = "tibber_prices"
|
||||
ATTRIBUTION = "Data provided by https://tibber.com/"
|
||||
CONF_ACCESS_TOKEN = "access_token" # noqa: S105
|
||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||
|
||||
ATTRIBUTION = "Data provided by Tibber"
|
||||
|
||||
SCAN_INTERVAL = 60 * 5 # 5 minutes
|
||||
|
||||
DEFAULT_NAME = "Tibber Price Analytics"
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS = False
|
||||
|
||||
PRICE_LEVEL_NORMAL = "NORMAL"
|
||||
PRICE_LEVEL_CHEAP = "CHEAP"
|
||||
PRICE_LEVEL_VERY_CHEAP = "VERY_CHEAP"
|
||||
PRICE_LEVEL_EXPENSIVE = "EXPENSIVE"
|
||||
PRICE_LEVEL_VERY_EXPENSIVE = "VERY_EXPENSIVE"
|
||||
|
||||
PRICE_LEVEL_MAPPING = {
|
||||
PRICE_LEVEL_VERY_CHEAP: -2,
|
||||
PRICE_LEVEL_CHEAP: -1,
|
||||
PRICE_LEVEL_NORMAL: 0,
|
||||
PRICE_LEVEL_EXPENSIVE: 1,
|
||||
PRICE_LEVEL_VERY_EXPENSIVE: 2,
|
||||
}
|
||||
|
||||
SENSOR_TYPE_PRICE_LEVEL = "price_level"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
# Path to custom translations directory
|
||||
CUSTOM_TRANSLATIONS_DIR = Path(__file__).parent / "custom_translations"
|
||||
|
||||
# Cache for translations to avoid repeated file reads
|
||||
_TRANSLATIONS_CACHE: dict[str, dict] = {}
|
||||
|
||||
|
||||
async def async_load_translations(hass: HomeAssistant, language: str) -> dict:
|
||||
"""
|
||||
Load translations from file asynchronously.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
language: The language code to load
|
||||
|
||||
Returns:
|
||||
The loaded translations as a dictionary
|
||||
|
||||
"""
|
||||
# Use a key that includes the language parameter
|
||||
cache_key = f"{DOMAIN}_translations_{language}"
|
||||
|
||||
# Check if we have an instance in hass.data
|
||||
if cache_key in hass.data:
|
||||
return hass.data[cache_key]
|
||||
|
||||
# Check the module-level cache
|
||||
if language in _TRANSLATIONS_CACHE:
|
||||
return _TRANSLATIONS_CACHE[language]
|
||||
|
||||
# Determine the file path
|
||||
file_path = CUSTOM_TRANSLATIONS_DIR / f"{language}.json"
|
||||
if not file_path.exists():
|
||||
# Fall back to English if requested language not found
|
||||
file_path = CUSTOM_TRANSLATIONS_DIR / "en.json"
|
||||
if not file_path.exists():
|
||||
LOGGER.debug("No custom translations found at %s", file_path)
|
||||
empty_cache = {}
|
||||
_TRANSLATIONS_CACHE[language] = empty_cache
|
||||
hass.data[cache_key] = empty_cache
|
||||
return empty_cache
|
||||
|
||||
try:
|
||||
# Read the file asynchronously
|
||||
async with aiofiles.open(file_path, encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
translations = json.loads(content)
|
||||
|
||||
# Store in both caches for future calls
|
||||
_TRANSLATIONS_CACHE[language] = translations
|
||||
hass.data[cache_key] = translations
|
||||
|
||||
return translations
|
||||
|
||||
except (OSError, json.JSONDecodeError) as err:
|
||||
LOGGER.warning("Error loading custom translations file: %s", err)
|
||||
empty_cache = {}
|
||||
_TRANSLATIONS_CACHE[language] = empty_cache
|
||||
hass.data[cache_key] = empty_cache
|
||||
return empty_cache
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected error loading custom translations")
|
||||
empty_cache = {}
|
||||
_TRANSLATIONS_CACHE[language] = empty_cache
|
||||
hass.data[cache_key] = empty_cache
|
||||
return empty_cache
|
||||
|
||||
|
||||
async def async_get_entity_description(
|
||||
hass: HomeAssistant,
|
||||
entity_type: str,
|
||||
entity_key: str,
|
||||
language: str = "en",
|
||||
field: str = "description",
|
||||
) -> str | None:
|
||||
"""
|
||||
Get a specific field from the entity's custom translations asynchronously.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
entity_type: The type of entity (sensor, binary_sensor, etc.)
|
||||
entity_key: The key of the entity
|
||||
language: The language code (defaults to English)
|
||||
field: The field to retrieve (description, long_description, usage_tips)
|
||||
|
||||
Returns:
|
||||
The requested field's value if found, None otherwise
|
||||
|
||||
"""
|
||||
translations = await async_load_translations(hass, language)
|
||||
|
||||
# Check if entity exists in translations
|
||||
if entity_type in translations and entity_key in translations[entity_type]:
|
||||
# Get the entity data
|
||||
entity_data = translations[entity_type][entity_key]
|
||||
|
||||
# If entity_data is a string, return it only for description field
|
||||
if isinstance(entity_data, str) and field == "description":
|
||||
return entity_data
|
||||
|
||||
# If entity_data is a dict, look for the requested field
|
||||
if isinstance(entity_data, dict) and field in entity_data:
|
||||
return entity_data[field]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_entity_description(
|
||||
entity_type: str, entity_key: str, language: str = "en", field: str = "description"
|
||||
) -> str | None:
|
||||
"""
|
||||
Get entity description synchronously from the cache.
|
||||
|
||||
This function only accesses the cached translations to avoid blocking I/O.
|
||||
|
||||
Args:
|
||||
entity_type: The type of entity
|
||||
entity_key: The key of the entity
|
||||
language: The language code
|
||||
field: The field to retrieve
|
||||
|
||||
Returns:
|
||||
The requested field's value if found in cache, None otherwise
|
||||
|
||||
"""
|
||||
# Only return from cache to avoid blocking I/O
|
||||
if language in _TRANSLATIONS_CACHE:
|
||||
translations = _TRANSLATIONS_CACHE[language]
|
||||
if entity_type in translations and entity_key in translations[entity_type]:
|
||||
entity_data = translations[entity_type][entity_key]
|
||||
|
||||
if isinstance(entity_data, str) and field == "description":
|
||||
return entity_data
|
||||
|
||||
if isinstance(entity_data, dict) and field in entity_data:
|
||||
return entity_data[field]
|
||||
|
||||
return None
|
||||
|
|
|
|||
81
custom_components/tibber_prices/custom_translations/de.json
Normal file
81
custom_components/tibber_prices/custom_translations/de.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"sensor": {
|
||||
"current_price": {
|
||||
"description": "Der aktuelle Strompreis inklusive Steuern",
|
||||
"long_description": "Zeigt den Strompreis für die aktuelle Stunde, einschließlich aller Steuern und Gebühren",
|
||||
"usage_tips": "Verwenden Sie diesen Sensor für Automatisierungen, die auf den aktuellen Preis reagieren sollen"
|
||||
},
|
||||
"next_hour_price": {
|
||||
"description": "Der Strompreis für die nächste Stunde inklusive Steuern",
|
||||
"long_description": "Zeigt den Strompreis für die kommende Stunde, einschließlich aller Steuern und Gebühren",
|
||||
"usage_tips": "Perfekt für die Planung von Geräten, die in der nächsten Stunde basierend auf dem Preis laufen sollen"
|
||||
},
|
||||
"price_level": {
|
||||
"description": "Aktueller Preisstandanzeige (SEHR_GÜNSTIG bis SEHR_TEUER)",
|
||||
"long_description": "Zeigt das aktuelle Preisniveau auf einer Skala von sehr günstig bis sehr teuer an",
|
||||
"usage_tips": "Verwenden Sie dies für visuelle Anzeigen oder einfache Automatisierungen ohne Schwellenwertberechnung"
|
||||
},
|
||||
"lowest_price_today": {
|
||||
"description": "Der niedrigste Strompreis für den aktuellen Tag",
|
||||
"long_description": "Zeigt den niedrigsten Preis des aktuellen Tages an",
|
||||
"usage_tips": "Nützlich, um die optimale Zeit für den Betrieb energieintensiver Geräte zu finden"
|
||||
},
|
||||
"highest_price_today": {
|
||||
"description": "Der höchste Strompreis für den aktuellen Tag",
|
||||
"long_description": "Zeigt den höchsten Preis während des aktuellen Tages an",
|
||||
"usage_tips": "Hilfreich, um Spitzenpreiszeiten zu vermeiden"
|
||||
},
|
||||
"average_price_today": {
|
||||
"description": "Der durchschnittliche Strompreis für den aktuellen Tag",
|
||||
"long_description": "Berechnet den durchschnittlichen Preis über alle Stunden des aktuellen Tages",
|
||||
"usage_tips": "Als Grundlage für Preisvergleiche verwenden"
|
||||
},
|
||||
"hourly_rating": {
|
||||
"description": "Preisvergleich mit historischen Daten für die aktuelle Stunde",
|
||||
"long_description": "Zeigt, wie der Preis der aktuellen Stunde im Vergleich zu historischen Daten als prozentuale Differenz abschneidet",
|
||||
"usage_tips": "Hilft zu verstehen, ob die aktuellen Preise höher oder niedriger als üblich für diese Zeit sind"
|
||||
},
|
||||
"daily_rating": {
|
||||
"description": "Preisvergleich mit historischen Daten für den aktuellen Tag",
|
||||
"long_description": "Zeigt, wie der heutige Durchschnittspreis im Vergleich zu historischen Daten als prozentuale Differenz abschneidet",
|
||||
"usage_tips": "Nützlich, um zu verstehen, ob heute generell teuer oder günstig ist"
|
||||
},
|
||||
"monthly_rating": {
|
||||
"description": "Preisvergleich mit historischen Daten für den aktuellen Monat",
|
||||
"long_description": "Zeigt, wie der durchschnittliche Preis dieses Monats im Vergleich zu historischen Daten als prozentuale Differenz abschneidet",
|
||||
"usage_tips": "Hilfreich für die langfristige Energiebudgetplanung"
|
||||
},
|
||||
"data_timestamp": {
|
||||
"description": "Zeitstempel der neuesten Preisdaten von Tibber",
|
||||
"long_description": "Zeigt an, wann die Preisdaten zuletzt von der Tibber API aktualisiert wurden",
|
||||
"usage_tips": "Überwachen Sie dies, um sicherzustellen, dass Ihre Preisdaten aktuell sind"
|
||||
},
|
||||
"tomorrow_data_available": {
|
||||
"description": "Zeigt an, ob Preisdaten für morgen verfügbar sind",
|
||||
"long_description": "Zeigt an, ob vollständige, teilweise oder keine Preisdaten für morgen verfügbar sind",
|
||||
"usage_tips": "Verwenden Sie dies, um zu prüfen, ob Sie Geräte für morgen zuverlässig planen können"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_hour": {
|
||||
"description": "Zeigt an, ob die aktuelle Stunde den höchsten Preis des Tages hat",
|
||||
"long_description": "Wird während Stunden aktiv, die zu den teuersten des Tages gehören",
|
||||
"usage_tips": "In Automatisierungen verwenden, um den Betrieb von Geräten mit hohem Verbrauch während Spitzenzeiten zu vermeiden"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"description": "Zeigt an, ob die aktuelle Stunde den niedrigsten Preis des Tages hat",
|
||||
"long_description": "Wird während Stunden aktiv, die zu den günstigsten des Tages gehören",
|
||||
"usage_tips": "Perfekt, um energieintensive Geräte zu optimalen Zeiten zu aktivieren"
|
||||
},
|
||||
"connection": {
|
||||
"description": "Zeigt den Verbindungsstatus zur Tibber API an",
|
||||
"long_description": "Zeigt an, ob die Komponente erfolgreich eine Verbindung zur Tibber API herstellt",
|
||||
"usage_tips": "Überwachen Sie dies, um sicherzustellen, dass Ihre Preisdaten korrekt aktualisiert werden"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Julian Pawlowski",
|
||||
"version": "1.0.0",
|
||||
"last_updated": "2025-04-23"
|
||||
}
|
||||
}
|
||||
81
custom_components/tibber_prices/custom_translations/en.json
Normal file
81
custom_components/tibber_prices/custom_translations/en.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"sensor": {
|
||||
"current_price": {
|
||||
"description": "The current hour's electricity price including taxes",
|
||||
"long_description": "Shows the electricity price for the current hour, including all taxes and fees",
|
||||
"usage_tips": "Use this sensor for automations that should react to the current price"
|
||||
},
|
||||
"next_hour_price": {
|
||||
"description": "The next hour's electricity price including taxes",
|
||||
"long_description": "Shows the electricity price for the upcoming hour, including all taxes and fees",
|
||||
"usage_tips": "Perfect for scheduling devices to run in the next hour based on price"
|
||||
},
|
||||
"price_level": {
|
||||
"description": "Current price level indicator (VERY_CHEAP to VERY_EXPENSIVE)",
|
||||
"long_description": "Indicates the current price level on a scale from very cheap to very expensive",
|
||||
"usage_tips": "Use this for visual indicators or simple automations without needing to calculate thresholds"
|
||||
},
|
||||
"lowest_price_today": {
|
||||
"description": "The lowest electricity price for the current day",
|
||||
"long_description": "Shows the lowest price point available during the current day",
|
||||
"usage_tips": "Useful to find the optimal time to run energy-intensive appliances"
|
||||
},
|
||||
"highest_price_today": {
|
||||
"description": "The highest electricity price for the current day",
|
||||
"long_description": "Shows the highest price point during the current day",
|
||||
"usage_tips": "Helpful for avoiding peak price periods"
|
||||
},
|
||||
"average_price_today": {
|
||||
"description": "The average electricity price for the current day",
|
||||
"long_description": "Calculates the average price across all hours of the current day",
|
||||
"usage_tips": "Use as a baseline for price comparison"
|
||||
},
|
||||
"hourly_rating": {
|
||||
"description": "Price comparison with historical data for the current hour",
|
||||
"long_description": "Shows how the current hour's price compares to historical data as a percentage difference",
|
||||
"usage_tips": "Helps understand if current prices are higher or lower than usual for this time"
|
||||
},
|
||||
"daily_rating": {
|
||||
"description": "Price comparison with historical data for the current day",
|
||||
"long_description": "Shows how today's average price compares to historical data as a percentage difference",
|
||||
"usage_tips": "Useful to understand if today is generally expensive or cheap"
|
||||
},
|
||||
"monthly_rating": {
|
||||
"description": "Price comparison with historical data for the current month",
|
||||
"long_description": "Shows how this month's average price compares to historical data as a percentage difference",
|
||||
"usage_tips": "Helpful for long-term energy budget planning"
|
||||
},
|
||||
"data_timestamp": {
|
||||
"description": "Timestamp of the most recent price data received from Tibber",
|
||||
"long_description": "Shows when the price data was last updated from the Tibber API",
|
||||
"usage_tips": "Monitor this to ensure your price data is current"
|
||||
},
|
||||
"tomorrow_data_available": {
|
||||
"description": "Indicates if price data for tomorrow is available",
|
||||
"long_description": "Shows whether complete, partial, or no price data is available for tomorrow",
|
||||
"usage_tips": "Use this to check if you can schedule appliances for tomorrow reliably"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_hour": {
|
||||
"description": "Indicates whether the current hour has the highest price of the day",
|
||||
"long_description": "Becomes active during hours that are among the most expensive of the day",
|
||||
"usage_tips": "Use in automations to avoid running high-consumption devices during peak hours"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"description": "Indicates whether the current hour has the lowest price of the day",
|
||||
"long_description": "Becomes active during hours that are among the cheapest of the day",
|
||||
"usage_tips": "Perfect for triggering energy-intensive appliances during optimal times"
|
||||
},
|
||||
"connection": {
|
||||
"description": "Shows connection status to the Tibber API",
|
||||
"long_description": "Indicates whether the component is successfully connecting to the Tibber API",
|
||||
"usage_tips": "Monitor this to ensure your price data is being updated correctly"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Julian Pawlowski",
|
||||
"version": "1.0.0",
|
||||
"last_updated": "2025-04-23"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"domain": "tibber_prices",
|
||||
"name": "Tibber Price Information & Ratings",
|
||||
"codeowners": [
|
||||
"@jpawlowski"
|
||||
],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/jpawlowski/hass.tibber_prices",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues",
|
||||
"version": "0.1.0"
|
||||
"domain": "tibber_prices",
|
||||
"name": "Tibber Price Information & Ratings",
|
||||
"codeowners": [
|
||||
"@jpawlowski"
|
||||
],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/jpawlowski/hass.tibber_prices",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues",
|
||||
"version": "0.1.0",
|
||||
"requirements": [
|
||||
"aiofiles>=23.2.1"
|
||||
]
|
||||
}
|
||||
|
|
@ -393,25 +393,111 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes."""
|
||||
async def async_extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes asynchronously."""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
attributes = self._get_sensor_attributes()
|
||||
attributes = self._get_sensor_attributes() or {}
|
||||
|
||||
# Add translated description
|
||||
if attributes and self.hass is not None:
|
||||
base_key = "entity.sensor"
|
||||
key = f"{base_key}.{self.entity_description.translation_key}.description"
|
||||
language_config = getattr(self.hass.config, "language", None)
|
||||
if isinstance(language_config, dict):
|
||||
description = language_config.get(key)
|
||||
if description is not None:
|
||||
attributes = dict(attributes) # Make a copy before modifying
|
||||
attributes["description"] = description
|
||||
# Add description from the custom translations file
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Extract the base key (without _cents suffix if present)
|
||||
base_key = self.entity_description.translation_key
|
||||
base_key = base_key.removesuffix("_cents")
|
||||
|
||||
return attributes
|
||||
# Get user's language preference
|
||||
language = self.hass.config.language if self.hass.config.language else "en"
|
||||
|
||||
# Import only within the method to avoid circular imports
|
||||
from .const import (
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
async_get_entity_description,
|
||||
)
|
||||
|
||||
# Add basic description
|
||||
description = await async_get_entity_description(self.hass, "sensor", base_key, language, "description")
|
||||
if description:
|
||||
attributes["description"] = description
|
||||
|
||||
# Check if extended descriptions are enabled in the config
|
||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
)
|
||||
|
||||
# Add extended descriptions if enabled
|
||||
if extended_descriptions:
|
||||
# Add long description if available
|
||||
long_desc = await async_get_entity_description(
|
||||
self.hass, "sensor", base_key, language, "long_description"
|
||||
)
|
||||
if long_desc:
|
||||
attributes["long_description"] = long_desc
|
||||
|
||||
# Add usage tips if available
|
||||
usage_tips = await async_get_entity_description(self.hass, "sensor", base_key, language, "usage_tips")
|
||||
if usage_tips:
|
||||
attributes["usage_tips"] = usage_tips
|
||||
|
||||
return attributes if attributes else None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict | None:
|
||||
"""
|
||||
Return additional state attributes (synchronous version).
|
||||
|
||||
This synchronous method is required by Home Assistant and will
|
||||
first return basic attributes, then add cached descriptions
|
||||
without any blocking I/O operations.
|
||||
"""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
# Start with the basic attributes
|
||||
attributes = self._get_sensor_attributes() or {}
|
||||
|
||||
# Add descriptions from the cache if available (non-blocking)
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Extract the base key (without _cents suffix if present)
|
||||
base_key = self.entity_description.translation_key
|
||||
base_key = base_key.removesuffix("_cents")
|
||||
|
||||
# Get user's language preference
|
||||
language = self.hass.config.language if self.hass.config.language else "en"
|
||||
|
||||
# Import synchronous function to get cached descriptions
|
||||
from .const import (
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
get_entity_description,
|
||||
)
|
||||
|
||||
# Add basic description from cache
|
||||
description = get_entity_description("sensor", base_key, language, "description")
|
||||
if description:
|
||||
attributes["description"] = description
|
||||
|
||||
# Check if extended descriptions are enabled in the config
|
||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
)
|
||||
|
||||
# Add extended descriptions if enabled (from cache only)
|
||||
if extended_descriptions:
|
||||
# Add long description if available in cache
|
||||
long_desc = get_entity_description("sensor", base_key, language, "long_description")
|
||||
if long_desc:
|
||||
attributes["long_description"] = long_desc
|
||||
|
||||
# Add usage tips if available in cache
|
||||
usage_tips = get_entity_description("sensor", base_key, language, "usage_tips")
|
||||
if usage_tips:
|
||||
attributes["usage_tips"] = usage_tips
|
||||
|
||||
return attributes if attributes else None
|
||||
|
||||
def _get_sensor_attributes(self) -> dict | None:
|
||||
"""Get attributes based on sensor type."""
|
||||
|
|
|
|||
|
|
@ -41,48 +41,37 @@
|
|||
"entity": {
|
||||
"sensor": {
|
||||
"current_price": {
|
||||
"name": "Current Price",
|
||||
"description": "The current hour's electricity price including taxes"
|
||||
"name": "Current Price"
|
||||
},
|
||||
"next_hour_price": {
|
||||
"name": "Next Hour Price",
|
||||
"description": "The next hour's electricity price including taxes"
|
||||
"name": "Next Hour Price"
|
||||
},
|
||||
"price_level": {
|
||||
"name": "Price Level",
|
||||
"description": "Current price level indicator (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)"
|
||||
"name": "Price Level"
|
||||
},
|
||||
"lowest_price_today": {
|
||||
"name": "Lowest Price Today",
|
||||
"description": "The lowest electricity price for the current day"
|
||||
"name": "Lowest Price Today"
|
||||
},
|
||||
"highest_price_today": {
|
||||
"name": "Highest Price Today",
|
||||
"description": "The highest electricity price for the current day"
|
||||
"name": "Highest Price Today"
|
||||
},
|
||||
"average_price_today": {
|
||||
"name": "Average Price Today",
|
||||
"description": "The average electricity price for the current day"
|
||||
"name": "Average Price Today"
|
||||
},
|
||||
"hourly_rating": {
|
||||
"name": "Hourly Price Rating",
|
||||
"description": "Price comparison with historical data for the current hour (percentage difference)"
|
||||
"name": "Hourly Price Rating"
|
||||
},
|
||||
"daily_rating": {
|
||||
"name": "Daily Price Rating",
|
||||
"description": "Price comparison with historical data for the current day (percentage difference)"
|
||||
"name": "Daily Price Rating"
|
||||
},
|
||||
"monthly_rating": {
|
||||
"name": "Monthly Price Rating",
|
||||
"description": "Price comparison with historical data for the current month (percentage difference)"
|
||||
"name": "Monthly Price Rating"
|
||||
},
|
||||
"data_timestamp": {
|
||||
"name": "Last Data Available",
|
||||
"description": "Timestamp of the most recent price data received from Tibber"
|
||||
"name": "Last Data Available"
|
||||
},
|
||||
"tomorrow_data_available": {
|
||||
"name": "Tomorrow's Data Available",
|
||||
"description": "Indicates if price data for tomorrow is available (Yes/No/Partial)"
|
||||
"name": "Tomorrow's Data Available"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
12
hacs.json
12
hacs.json
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Tibber Price Information & Ratings",
|
||||
"homeassistant": "2025.4.2",
|
||||
"hacs": "2.0.1",
|
||||
"render_readme": true,
|
||||
"domains": [
|
||||
"sensor",
|
||||
"binary_sensor"
|
||||
]
|
||||
"name": "Tibber Price Information & Ratings",
|
||||
"homeassistant": "2025.4.2",
|
||||
"hacs": "2.0.1",
|
||||
"render_readme": true
|
||||
}
|
||||
Loading…
Reference in a new issue