From 94ef6ed4a6f6abcbcb357fb069325282e6c27159 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Wed, 23 Apr 2025 23:53:11 +0000 Subject: [PATCH] add friendly name --- custom_components/tibber_prices/const.py | 151 +++++++++++++++--- .../tibber_prices/custom_translations/de.json | 9 +- .../tibber_prices/custom_translations/en.json | 9 +- custom_components/tibber_prices/sensor.py | 99 +++++++----- 4 files changed, 207 insertions(+), 61 deletions(-) diff --git a/custom_components/tibber_prices/const.py b/custom_components/tibber_prices/const.py index d10a1be..81008bd 100644 --- a/custom_components/tibber_prices/const.py +++ b/custom_components/tibber_prices/const.py @@ -2,7 +2,9 @@ import json import logging +from collections.abc import Sequence from pathlib import Path +from typing import Any import aiofiles @@ -114,6 +116,75 @@ async def async_load_translations(hass: HomeAssistant, language: str) -> dict: return empty_cache +async def async_get_translation( + hass: HomeAssistant, + path: Sequence[str], + language: str = "en", +) -> Any: + """ + Get a translation value by path asynchronously. + + Args: + hass: HomeAssistant instance + path: A sequence of keys defining the path to the translation value + language: The language code (defaults to English) + + Returns: + The translation value if found, None otherwise + + """ + translations = await async_load_translations(hass, language) + + # Navigate to the requested path + current = translations + for key in path: + if not isinstance(current, dict) or key not in current: + return None + current = current[key] + + return current + + +def get_translation( + path: Sequence[str], + language: str = "en", +) -> Any: + """ + Get a translation value by path synchronously from the cache. + + This function only accesses the cached translations to avoid blocking I/O. + + Args: + path: A sequence of keys defining the path to the translation value + language: The language code (defaults to English) + + Returns: + The translation value if found in cache, None otherwise + + """ + # Only return from cache to avoid blocking I/O + if language not in _TRANSLATIONS_CACHE: + # Fall back to English if the requested language is not available + if language != "en" and "en" in _TRANSLATIONS_CACHE: + language = "en" + else: + return None + + # Navigate to the requested path + current = _TRANSLATIONS_CACHE[language] + for key in path: + if not isinstance(current, dict): + return None + if key not in current: + # Log the missing key for debugging + LOGGER.debug("Translation key '%s' not found in path %s for language %s", key, path, language) + return None + current = current[key] + + return current + + +# Convenience functions for backward compatibility and common usage patterns async def async_get_entity_description( hass: HomeAssistant, entity_type: str, @@ -135,26 +206,24 @@ async def async_get_entity_description( The requested field's value if found, None otherwise """ - translations = await async_load_translations(hass, language) + entity_data = await async_get_translation(hass, [entity_type, entity_key], 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] + # Handle the case where entity_data is a string (for description field) + if isinstance(entity_data, str) and field == "description": + return entity_data - # 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] + # Handle the case where entity_data is a dict + 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" + entity_type: str, + entity_key: str, + language: str = "en", + field: str = "description", ) -> str | None: """ Get entity description synchronously from the cache. @@ -171,16 +240,54 @@ def get_entity_description( 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] + entity_data = get_translation([entity_type, entity_key], language) - if isinstance(entity_data, str) and field == "description": - return entity_data + # Handle the case where entity_data is a string (for description field) + 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] + # Handle the case where entity_data is a dict + if isinstance(entity_data, dict) and field in entity_data: + return entity_data[field] return None + + +async def async_get_price_level_translation( + hass: HomeAssistant, + level: str, + language: str = "en", +) -> str | None: + """ + Get a localized translation for a price level asynchronously. + + Args: + hass: HomeAssistant instance + level: The price level (e.g., VERY_CHEAP, NORMAL, etc.) + language: The language code (defaults to English) + + Returns: + The localized price level if found, None otherwise + + """ + return await async_get_translation(hass, ["sensor", "price_level", "price_levels", level], language) + + +def get_price_level_translation( + level: str, + language: str = "en", +) -> str | None: + """ + Get a localized translation for a price level synchronously from the cache. + + This function only accesses the cached translations to avoid blocking I/O. + + Args: + level: The price level (e.g., VERY_CHEAP, NORMAL, etc.) + language: The language code (defaults to English) + + Returns: + The localized price level if found in cache, None otherwise + + """ + return get_translation(["sensor", "price_level", "price_levels", level], language) diff --git a/custom_components/tibber_prices/custom_translations/de.json b/custom_components/tibber_prices/custom_translations/de.json index 17e1bd6..9d3cd13 100644 --- a/custom_components/tibber_prices/custom_translations/de.json +++ b/custom_components/tibber_prices/custom_translations/de.json @@ -13,7 +13,14 @@ "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": "Verwende dies für visuelle Anzeigen oder einfache Automatisierungen ohne Schwellenwertberechnung" + "usage_tips": "Verwende dies für visuelle Anzeigen oder einfache Automatisierungen ohne Schwellenwertberechnung", + "price_levels": { + "VERY_CHEAP": "Sehr Günstig", + "CHEAP": "Günstig", + "NORMAL": "Normal", + "EXPENSIVE": "Teuer", + "VERY_EXPENSIVE": "Sehr Teuer" + } }, "lowest_price_today": { "description": "Der niedrigste Strompreis für den aktuellen Tag", diff --git a/custom_components/tibber_prices/custom_translations/en.json b/custom_components/tibber_prices/custom_translations/en.json index 5de89ba..6bb34f1 100644 --- a/custom_components/tibber_prices/custom_translations/en.json +++ b/custom_components/tibber_prices/custom_translations/en.json @@ -13,7 +13,14 @@ "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" + "usage_tips": "Use this for visual indicators or simple automations without needing to calculate thresholds", + "price_levels": { + "VERY_CHEAP": "Very Cheap", + "CHEAP": "Cheap", + "NORMAL": "Normal", + "EXPENSIVE": "Expensive", + "VERY_EXPENSIVE": "Very Expensive" + } }, "lowest_price_today": { "description": "The lowest electricity price for the current day", diff --git a/custom_components/tibber_prices/sensor.py b/custom_components/tibber_prices/sensor.py index 2491143..4ceb347 100644 --- a/custom_components/tibber_prices/sensor.py +++ b/custom_components/tibber_prices/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, datetime +from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -17,13 +17,13 @@ from homeassistant.const import CURRENCY_EURO, EntityCategory from homeassistant.util import dt as dt_util from .const import ( - PRICE_LEVEL_CHEAP, - PRICE_LEVEL_EXPENSIVE, + CONF_EXTENDED_DESCRIPTIONS, + DEFAULT_EXTENDED_DESCRIPTIONS, + DOMAIN, PRICE_LEVEL_MAPPING, - PRICE_LEVEL_NORMAL, - PRICE_LEVEL_VERY_CHEAP, - PRICE_LEVEL_VERY_EXPENSIVE, SENSOR_TYPE_PRICE_LEVEL, + async_get_entity_description, + get_entity_description, ) from .entity import TibberPricesEntity @@ -314,8 +314,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): # Calculate the exact target datetime (not just the hour) # This properly handles day boundaries - from datetime import timedelta - target_datetime = now.replace(microsecond=0) + timedelta(hours=hour_offset) target_hour = target_datetime.hour target_date = target_datetime.date() @@ -468,13 +466,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): # 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: @@ -526,13 +517,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): # 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: @@ -602,25 +586,66 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): self._add_price_level_attributes(attributes, current_hour_data["level"]) def _add_price_level_attributes(self, attributes: dict, level: str) -> None: - """Add price level specific attributes.""" - if level in PRICE_LEVEL_MAPPING: - attributes["level_value"] = PRICE_LEVEL_MAPPING[level] + """ + Add price level specific attributes. - # Add human-readable level descriptions - level_descriptions = { - PRICE_LEVEL_VERY_CHEAP: "Very low price compared to average", - PRICE_LEVEL_CHEAP: "Lower than average price", - PRICE_LEVEL_NORMAL: "Average price level", - PRICE_LEVEL_EXPENSIVE: "Higher than average price", - PRICE_LEVEL_VERY_EXPENSIVE: "Very high price compared to average", - } - if level in level_descriptions: - attributes["description"] = level_descriptions[level] + Args: + attributes: Dictionary to add attributes to + level: The price level value (e.g., VERY_CHEAP, NORMAL, etc.) + + """ + if level not in PRICE_LEVEL_MAPPING: + return + + # Add numeric value for sorting/comparison + attributes["level_value"] = PRICE_LEVEL_MAPPING[level] + + # Add the original English level as a reliable identifier for automations + attributes["level_id"] = level + + # Default to the original level value if no translation is found + friendly_name = level + + # Try to get localized friendly name from translations if Home Assistant is available + if self.hass: + # Get user's language preference (default to English) + language = self.hass.config.language or "en" + + # Use direct dictionary lookup for better performance and reliability + # This matches how async_get_entity_description works + cache_key = f"{DOMAIN}_translations_{language}" + if cache_key in self.hass.data: + translations = self.hass.data[cache_key] + + # Navigate through the translation dictionary + if ( + "sensor" in translations + and "price_level" in translations["sensor"] + and "price_levels" in translations["sensor"]["price_level"] + and level in translations["sensor"]["price_level"]["price_levels"] + ): + friendly_name = translations["sensor"]["price_level"]["price_levels"][level] + + # If we didn't find a translation in the current language, try English + if friendly_name == level and language != "en": + en_cache_key = f"{DOMAIN}_translations_en" + if en_cache_key in self.hass.data: + en_translations = self.hass.data[en_cache_key] + + # Try using English translation as fallback + if ( + "sensor" in en_translations + and "price_level" in en_translations["sensor"] + and "price_levels" in en_translations["sensor"]["price_level"] + and level in en_translations["sensor"]["price_level"]["price_levels"] + ): + friendly_name = en_translations["sensor"]["price_level"]["price_levels"][level] + + # Add the friendly name to attributes + attributes["friendly_name"] = friendly_name def _add_next_hour_attributes(self, attributes: dict) -> None: """Add attributes for next hour price sensors.""" - from datetime import timedelta - price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"] now = dt_util.now()