mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
feat(sensors): add chart_metadata sensor for lightweight chart configuration
Implemented new chart_metadata diagnostic sensor that provides essential chart configuration values (yaxis_min, yaxis_max, gradient_stop) as attributes, enabling dynamic chart configuration without requiring async service calls in templates. Sensor implementation: - New chart_metadata.py module with metadata-only service calls - Automatically calls get_chartdata with metadata="only" parameter - Refreshes on coordinator updates (new price data or user data) - State values: "pending", "ready", "error" - Enabled by default (critical for chart features) ApexCharts YAML generator integration: - Checks for chart_metadata sensor availability before generation - Uses template variables to read sensor attributes dynamically - Fallback to fixed values (gradient_stop=50%) if sensor unavailable - Creates separate notifications for two independent issues: 1. Chart metadata sensor disabled (reduced functionality warning) 2. Required custom cards missing (YAML won't work warning) - Both notifications explain YAML generation context and provide complete fix instructions with regeneration requirement Configuration: - Supports configuration.yaml: tibber_prices.chart_metadata_config - Optional parameters: day, minor_currency, resolution - Defaults to minor_currency=True for ApexCharts compatibility Translation additions: - Entity name and state translations (all 5 languages) - Notification messages for sensor unavailable and missing cards - best_price_period_name for tooltip formatter Binary sensor improvements: - tomorrow_data_available now enabled by default (critical for automations) - data_lifecycle_status now enabled by default (critical for debugging) Impact: Users get dynamic chart configuration with optimized Y-axis scaling and gradient positioning without manual calculations. ApexCharts YAML generation now provides clear, actionable notifications when issues occur, ensuring users understand why functionality is limited and how to fix it.
This commit is contained in:
parent
ac6f1e0955
commit
6922e52368
19 changed files with 702 additions and 94 deletions
|
|
@ -21,6 +21,7 @@ from homeassistant.loader import async_get_loaded_integration
|
|||
from .api import TibberPricesApiClient
|
||||
from .const import (
|
||||
DATA_CHART_CONFIG,
|
||||
DATA_CHART_METADATA_CONFIG,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
async_load_standard_translations,
|
||||
|
|
@ -100,6 +101,16 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
|||
LOGGER.debug("No chart_export configuration found in configuration.yaml")
|
||||
hass.data[DOMAIN][DATA_CHART_CONFIG] = {}
|
||||
|
||||
# Extract chart_metadata config if present
|
||||
chart_metadata_config = domain_config.get("chart_metadata", {})
|
||||
|
||||
if chart_metadata_config:
|
||||
LOGGER.debug("Loaded chart_metadata configuration from configuration.yaml")
|
||||
hass.data[DOMAIN][DATA_CHART_METADATA_CONFIG] = chart_metadata_config
|
||||
else:
|
||||
LOGGER.debug("No chart_metadata configuration found in configuration.yaml")
|
||||
hass.data[DOMAIN][DATA_CHART_METADATA_CONFIG] = {}
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ ENTITY_DESCRIPTIONS = (
|
|||
icon="mdi:calendar-check",
|
||||
device_class=None, # No specific device_class = shows generic "On/Off"
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=True, # Critical for automations
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="has_ventilation_system",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ LOGGER = logging.getLogger(__package__)
|
|||
|
||||
# Data storage keys
|
||||
DATA_CHART_CONFIG = "chart_config" # Key for chart export config in hass.data
|
||||
DATA_CHART_METADATA_CONFIG = "chart_metadata_config" # Key for chart metadata config in hass.data
|
||||
|
||||
# Configuration keys
|
||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
{
|
||||
"apexcharts": {
|
||||
"title_rating_level": "Preisphasen Tagesverlauf",
|
||||
"title_level": "Preisniveau"
|
||||
"title_level": "Preisniveau",
|
||||
"best_price_period_name": "Beste Preisperiode",
|
||||
"notification": {
|
||||
"metadata_sensor_unavailable": {
|
||||
"title": "Tibber Prices: ApexCharts YAML mit eingeschränkter Funktionalität generiert",
|
||||
"message": "Du hast gerade eine ApexCharts-Card-Konfiguration über die Entwicklerwerkzeuge generiert. Der Chart-Metadaten-Sensor ist aktuell deaktiviert, daher zeigt das generierte YAML nur **Basisfunktionalität** (Auto-Skalierung, fester Gradient bei 50%).\n\n**Für volle Funktionalität** (optimierte Skalierung, dynamische Verlaufsfarben):\n1. [Tibber Prices Integration öffnen](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n2. Aktiviere den 'Chart Metadata' Sensor\n3. **Generiere das YAML erneut** über die Entwicklerwerkzeuge\n4. **Ersetze den alten YAML-Code** in deinem Dashboard durch die neue Version\n\n⚠️ Nur den Sensor zu aktivieren reicht nicht - du musst das YAML neu generieren und ersetzen!"
|
||||
},
|
||||
"missing_cards": {
|
||||
"title": "Tibber Prices: ApexCharts YAML kann nicht verwendet werden",
|
||||
"message": "Du hast gerade eine ApexCharts-Card-Konfiguration über die Entwicklerwerkzeuge generiert, aber das generierte YAML **funktioniert nicht**, weil erforderliche Custom Cards fehlen.\n\n**Fehlende Cards:**\n{cards}\n\n**Um das generierte YAML zu nutzen:**\n1. Klicke auf die obigen Links, um die fehlenden Cards über HACS zu installieren\n2. Starte Home Assistant neu (manchmal erforderlich)\n3. **Generiere das YAML erneut** über die Entwicklerwerkzeuge\n4. Füge das YAML zu deinem Dashboard hinzu\n\n⚠️ Der aktuelle YAML-Code funktioniert nicht, bis alle Cards installiert sind!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_interval_price": {
|
||||
|
|
@ -437,6 +448,11 @@
|
|||
"description": "Datenexport für Dashboard-Integrationen",
|
||||
"long_description": "Dieser Sensor ruft den get_chartdata-Service mit deiner konfigurierten YAML-Konfiguration auf und stellt das Ergebnis als Entity-Attribute bereit. Der Status zeigt 'ready' wenn Daten verfügbar sind, 'error' bei Fehlern, oder 'pending' vor dem ersten Aufruf. Perfekt für Dashboard-Integrationen wie ApexCharts, die Preisdaten aus Entity-Attributen lesen.",
|
||||
"usage_tips": "Konfiguriere die YAML-Parameter in den Integrationsoptionen entsprechend deinem get_chartdata-Service-Aufruf. Der Sensor aktualisiert automatisch bei Preisdaten-Updates (typischerweise nach Mitternacht und wenn morgige Daten eintreffen). Greife auf die Service-Response-Daten direkt über die Entity-Attribute zu - die Struktur entspricht exakt dem, was get_chartdata zurückgibt."
|
||||
},
|
||||
"chart_metadata": {
|
||||
"description": "Leichtgewichtige Metadaten für Diagrammkonfiguration",
|
||||
"long_description": "Liefert wesentliche Diagrammkonfigurationswerte als Sensor-Attribute. Nützlich für jede Diagrammkarte, die Y-Achsen-Grenzen oder Gradientenpositionierung benötigt. Der Sensor ruft get_chartdata im Nur-Metadaten-Modus auf (keine Datenverarbeitung) und extrahiert: yaxis_min, yaxis_max (vorgeschlagener Y-Achsenbereich) und gradient_stop (durchschnittliche Preisposition 0-100%). Der Status spiegelt das Service-Call-Ergebnis wider: 'ready' bei Erfolg, 'error' bei Fehler, 'pending' während der Initialisierung.",
|
||||
"usage_tips": "Konfiguriere über configuration.yaml unter tibber_prices.chart_metadata_config (optional: day, minor_currency, resolution). Der Sensor aktualisiert sich automatisch bei Preisdatenänderungen. Greife auf Metadaten aus Attributen zu: yaxis_min, yaxis_max, gradient_stop. Verwende mit config-template-card oder jedem Tool, das Entity-Attribute liest - perfekt für dynamische Diagrammkonfiguration ohne manuelle Berechnungen."
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
{
|
||||
"apexcharts": {
|
||||
"title_rating_level": "Price Phases Daily Progress",
|
||||
"title_level": "Price Level"
|
||||
"title_level": "Price Level",
|
||||
"best_price_period_name": "Best Price Period",
|
||||
"notification": {
|
||||
"metadata_sensor_unavailable": {
|
||||
"title": "Tibber Prices: ApexCharts YAML Generated with Limited Functionality",
|
||||
"message": "You just generated an ApexCharts card configuration via Developer Tools. The Chart Metadata sensor is currently disabled, so the generated YAML will only show **basic functionality** (auto-scale axis, fixed gradient at 50%).\n\n**To enable full functionality** (optimized scaling, dynamic gradient colors):\n1. [Open Tibber Prices Integration](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n2. Enable the 'Chart Metadata' sensor\n3. **Generate the YAML again** via Developer Tools\n4. **Replace the old YAML** in your dashboard with the new version\n\n⚠️ Simply enabling the sensor is not enough - you must regenerate and replace the YAML code!"
|
||||
},
|
||||
"missing_cards": {
|
||||
"title": "Tibber Prices: ApexCharts YAML Cannot Be Used",
|
||||
"message": "You just generated an ApexCharts card configuration via Developer Tools, but the generated YAML **will not work** because required custom cards are missing.\n\n**Missing cards:**\n{cards}\n\n**To use the generated YAML:**\n1. Click the links above to install the missing cards from HACS\n2. Restart Home Assistant (sometimes needed)\n3. **Generate the YAML again** via Developer Tools\n4. Add the YAML to your dashboard\n\n⚠️ The current YAML code will not work until all cards are installed!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_interval_price": {
|
||||
|
|
@ -469,6 +480,11 @@
|
|||
"description": "Data export for dashboard integrations",
|
||||
"long_description": "This binary sensor calls the get_chartdata service with your configured YAML parameters and exposes the result as entity attributes. The state is 'on' when the service call succeeds and data is available, 'off' when the call fails or no configuration is set. Perfect for dashboard integrations like ApexCharts that need to read price data from entity attributes.",
|
||||
"usage_tips": "Configure the YAML parameters in the integration options to match your get_chartdata service call. The sensor will automatically refresh when price data updates (typically after midnight and when tomorrow's data arrives). Access the service response data directly from the entity's attributes - the structure matches exactly what get_chartdata returns."
|
||||
},
|
||||
"chart_metadata": {
|
||||
"description": "Lightweight metadata for chart configuration",
|
||||
"long_description": "Provides essential chart configuration values as sensor attributes. Useful for any chart card that needs Y-axis bounds or gradient positioning. The sensor calls get_chartdata with metadata-only mode (no data processing) and extracts: yaxis_min, yaxis_max (suggested Y-axis range), and gradient_stop (average price position 0-100%). The state reflects the service call result: 'ready' when successful, 'error' on failure, 'pending' during initialization.",
|
||||
"usage_tips": "Configure via configuration.yaml under tibber_prices.chart_metadata_config (optional: day, minor_currency, resolution). The sensor automatically refreshes when price data updates. Access metadata from attributes: yaxis_min, yaxis_max, gradient_stop. Use with config-template-card or any tool that reads entity attributes - perfect for dynamic chart configuration without manual calculations."
|
||||
}
|
||||
},
|
||||
"home_types": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
{
|
||||
"apexcharts": {
|
||||
"title_rating_level": "Prisfaser daglig fremgang",
|
||||
"title_level": "Prisnivå"
|
||||
"title_rating_level": "Prisfaser dagsfremdrift",
|
||||
"title_level": "Prisnivå",
|
||||
"best_price_period_name": "Beste prisperiode",
|
||||
"notification": {
|
||||
"metadata_sensor_unavailable": {
|
||||
"title": "Tibber Prices: ApexCharts YAML generert med begrenset funksjonalitet",
|
||||
"message": "Du har nettopp generert en ApexCharts-kort-konfigurasjon via Utviklerverktøy. Diagram-metadata-sensoren er deaktivert, så den genererte YAML-en vil bare vise **grunnleggende funksjonalitet** (auto-skalering, fast gradient på 50%).\n\n**For full funksjonalitet** (optimert skalering, dynamiske gradientfarger):\n1. [Åpne Tibber Prices-integrasjonen](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n2. Aktiver 'Chart Metadata'-sensoren\n3. **Generer YAML-en på nytt** via Utviklerverktøy\n4. **Erstatt den gamle YAML-en** i dashbordet ditt med den nye versjonen\n\n⚠️ Det er ikke nok å bare aktivere sensoren - du må regenerere og erstatte YAML-koden!"
|
||||
},
|
||||
"missing_cards": {
|
||||
"title": "Tibber Prices: ApexCharts YAML kan ikke brukes",
|
||||
"message": "Du har nettopp generert en ApexCharts-kort-konfigurasjon via Utviklerverktøy, men den genererte YAML-en **vil ikke fungere** fordi nødvendige tilpassede kort mangler.\n\n**Manglende kort:**\n{cards}\n\n**For å bruke den genererte YAML-en:**\n1. Klikk på lenkene ovenfor for å installere de manglende kortene fra HACS\n2. Start Home Assistant på nytt (noen ganger nødvendig)\n3. **Generer YAML-en på nytt** via Utviklerverktøy\n4. Legg til YAML-en i dashbordet ditt\n\n⚠️ Den nåværende YAML-koden vil ikke fungere før alle kort er installert!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_interval_price": {
|
||||
|
|
@ -437,6 +448,11 @@
|
|||
"description": "Dataeksport for dashboardintegrasjoner",
|
||||
"long_description": "Denne sensoren kaller get_chartdata-tjenesten med din konfigurerte YAML-konfigurasjon og eksponerer resultatet som entitetsattributter. Status viser 'ready' når data er tilgjengelig, 'error' ved feil, eller 'pending' før første kall. Perfekt for dashboardintegrasjoner som ApexCharts som trenger å lese prisdata fra entitetsattributter.",
|
||||
"usage_tips": "Konfigurer YAML-parametrene i integrasjonsinnstillingene for å matche get_chartdata-tjenestekallet ditt. Sensoren vil automatisk oppdatere når prisdata oppdateres (typisk etter midnatt og når morgendagens data ankommer). Få tilgang til tjenesteresponsdataene direkte fra entitetens attributter - strukturen matcher nøyaktig det get_chartdata returnerer."
|
||||
},
|
||||
"chart_metadata": {
|
||||
"description": "Lettvekts metadata for diagramkonfigurasjon",
|
||||
"long_description": "Gir essensielle diagramkonfigurasjonsverdier som sensorattributter. Nyttig for ethvert diagramkort som trenger Y-aksegrenser eller gradientposisjonering. Sensoren kaller get_chartdata med kun-metadata-modus (ingen databehandling) og trekker ut: yaxis_min, yaxis_max (foreslått Y-akseområde) og gradient_stop (gjennomsnittlig prisposisjon 0-100%). Status reflekterer tjenestekallresultatet: 'ready' ved suksess, 'error' ved feil, 'pending' under initialisering.",
|
||||
"usage_tips": "Konfigurer via configuration.yaml under tibber_prices.chart_metadata_config (valgfritt: day, minor_currency, resolution). Sensoren oppdateres automatisk når prisdata endres. Få tilgang til metadata fra attributter: yaxis_min, yaxis_max, gradient_stop. Bruk med config-template-card eller ethvert verktøy som leser entitetsattributter - perfekt for dynamisk diagramkonfigurasjon uten manuelle beregninger."
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
{
|
||||
"apexcharts": {
|
||||
"title_rating_level": "Prijsfasen dagelijkse voortgang",
|
||||
"title_level": "Prijsniveau"
|
||||
"title_rating_level": "Prijsfasen dagverloop",
|
||||
"title_level": "Prijsniveau",
|
||||
"best_price_period_name": "Beste prijsperiode",
|
||||
"notification": {
|
||||
"metadata_sensor_unavailable": {
|
||||
"title": "Tibber Prices: ApexCharts YAML gegenereerd met beperkte functionaliteit",
|
||||
"message": "Je hebt zojuist een ApexCharts-kaartconfiguratie gegenereerd via Ontwikkelaarstools. De grafiek-metadata-sensor is momenteel uitgeschakeld, dus de gegenereerde YAML toont alleen **basisfunctionaliteit** (auto-schaal as, vaste verloop op 50%).\n\n**Voor volledige functionaliteit** (geoptimaliseerde schaling, dynamische verloopkleuren):\n1. [Open Tibber Prices-integratie](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n2. Schakel de 'Chart Metadata'-sensor in\n3. **Genereer de YAML opnieuw** via Ontwikkelaarstools\n4. **Vervang de oude YAML** in je dashboard door de nieuwe versie\n\n⚠️ Alleen de sensor inschakelen is niet genoeg - je moet de YAML opnieuw genereren en vervangen!"
|
||||
},
|
||||
"missing_cards": {
|
||||
"title": "Tibber Prices: ApexCharts YAML kan niet worden gebruikt",
|
||||
"message": "Je hebt zojuist een ApexCharts-kaartconfiguratie gegenereerd via Ontwikkelaarstools, maar de gegenereerde YAML **zal niet werken** omdat vereiste aangepaste kaarten ontbreken.\n\n**Ontbrekende kaarten:**\n{cards}\n\n**Om de gegenereerde YAML te gebruiken:**\n1. Klik op de bovenstaande links om de ontbrekende kaarten te installeren vanuit HACS\n2. Herstart Home Assistant (soms nodig)\n3. **Genereer de YAML opnieuw** via Ontwikkelaarstools\n4. Voeg de YAML toe aan je dashboard\n\n⚠️ De huidige YAML-code werkt niet totdat alle kaarten zijn geïnstalleerd!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_interval_price": {
|
||||
|
|
@ -437,6 +448,11 @@
|
|||
"description": "Data-export voor dashboard-integraties",
|
||||
"long_description": "Deze sensor roept de get_chartdata-service aan met jouw geconfigureerde YAML-configuratie en stelt het resultaat beschikbaar als entiteitsattributen. De status toont 'ready' wanneer data beschikbaar is, 'error' bij fouten, of 'pending' voor de eerste aanroep. Perfekt voor dashboard-integraties zoals ApexCharts die prijsgegevens uit entiteitsattributen moeten lezen.",
|
||||
"usage_tips": "Configureer de YAML-parameters in de integratie-opties om overeen te komen met jouw get_chartdata-service-aanroep. De sensor wordt automatisch bijgewerkt wanneer prijsgegevens worden bijgewerkt (typisch na middernacht en wanneer gegevens van morgen binnenkomen). Krijg toegang tot de service-responsgegevens direct vanuit de entiteitsattributen - de structuur komt exact overeen met wat get_chartdata retourneert."
|
||||
},
|
||||
"chart_metadata": {
|
||||
"description": "Lichtgewicht metadata voor diagramconfiguratie",
|
||||
"long_description": "Biedt essentiële diagramconfiguratiewaarden als sensorattributen. Nuttig voor elke grafiekkaart die Y-as-grenzen of gradiëntpositionering nodig heeft. De sensor roept get_chartdata aan in alleen-metadata-modus (geen dataverwerking) en extraheert: yaxis_min, yaxis_max (gesuggereerd Y-asbereik) en gradient_stop (gemiddelde prijspositie 0-100%). De status weerspiegelt het service-aanroepresultaat: 'ready' bij succes, 'error' bij fouten, 'pending' tijdens initialisatie.",
|
||||
"usage_tips": "Configureer via configuration.yaml onder tibber_prices.chart_metadata_config (optioneel: day, minor_currency, resolution). De sensor wordt automatisch bijgewerkt bij prijsgegevenswijzigingen. Krijg toegang tot metadata vanuit attributen: yaxis_min, yaxis_max, gradient_stop. Gebruik met config-template-card of elk hulpmiddel dat entiteitsattributen leest - perfect voor dynamische diagramconfiguratie zonder handmatige berekeningen."
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
{
|
||||
"apexcharts": {
|
||||
"title_rating_level": "Prisfaser daglig framsteg",
|
||||
"title_level": "Prisnivå"
|
||||
"title_rating_level": "Prisfaser dagsprogress",
|
||||
"title_level": "Prisnivå",
|
||||
"best_price_period_name": "Bästa prisperiod",
|
||||
"notification": {
|
||||
"metadata_sensor_unavailable": {
|
||||
"title": "Tibber Prices: ApexCharts YAML genererad med begränsad funktionalitet",
|
||||
"message": "Du har precis genererat en ApexCharts-kortkonfiguration via Utvecklarverktyg. Diagram-metadata-sensorn är inaktiverad, så den genererade YAML:en visar bara **grundläggande funktionalitet** (auto-skalning, fast gradient vid 50%).\n\n**För full funktionalitet** (optimerad skalning, dynamiska gradientfärger):\n1. [Öppna Tibber Prices-integrationen](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n2. Aktivera 'Chart Metadata'-sensorn\n3. **Generera YAML:en igen** via Utvecklarverktyg\n4. **Ersätt den gamla YAML:en** i din instrumentpanel med den nya versionen\n\n⚠️ Det räcker inte att bara aktivera sensorn - du måste regenerera och ersätta YAML-koden!"
|
||||
},
|
||||
"missing_cards": {
|
||||
"title": "Tibber Prices: ApexCharts YAML kan inte användas",
|
||||
"message": "Du har precis genererat en ApexCharts-kortkonfiguration via Utvecklarverktyg, men den genererade YAML:en **kommer inte att fungera** eftersom nödvändiga anpassade kort saknas.\n\n**Saknade kort:**\n{cards}\n\n**För att använda den genererade YAML:en:**\n1. Klicka på länkarna ovan för att installera de saknade korten från HACS\n2. Starta om Home Assistant (ibland nödvändigt)\n3. **Generera YAML:en igen** via Utvecklarverktyg\n4. Lägg till YAML:en i din instrumentpanel\n\n⚠️ Den nuvarande YAML-koden fungerar inte förrän alla kort är installerade!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_interval_price": {
|
||||
|
|
@ -434,9 +445,14 @@
|
|||
"usage_tips": "Använd detta för att övervaka din abonnemangsstatus. Ställ in varningar om statusen ändras från 'Aktiv' för att säkerställa oavbruten service."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Dataexport för instrumentpanelsintegrationer",
|
||||
"long_description": "Denna sensor anropar get_chartdata-tjänsten med din konfigurerade YAML-konfiguration och exponerar resultatet som entitetsattribut. Statusen visar 'ready' när data är tillgänglig, 'error' vid fel, eller 'pending' före första anropet. Perfekt för instrumentpanelsintegrationer som ApexCharts som behöver läsa prisdata från entitetsattribut.",
|
||||
"usage_tips": "Konfigurera YAML-parametrarna i integrationsinställningarna för att matcha ditt get_chartdata-tjänsteanrop. Sensorn uppdateras automatiskt när prisdata uppdateras (vanligtvis efter midnatt och när morgondagens data anländer). Få åtkomst till tjänstesvarsdata direkt från entitetens attribut - strukturen matchar exakt vad get_chartdata returnerar."
|
||||
"description": "Dataexport för dashboard-integrationer",
|
||||
"long_description": "Denna sensor anropar get_chartdata-tjänsten med din konfigurerade YAML-konfiguration och exponerar resultatet som entitetsattribut. Statusen visar 'ready' när data är tillgänglig, 'error' vid fel, eller 'pending' före första anropet. Perfekt för dashboard-integrationer som ApexCharts som behöver läsa prisdata från entitetsattribut.",
|
||||
"usage_tips": "Konfigurera YAML-parametrarna i integrationsalternativen för att matcha ditt get_chartdata-tjänstanrop. Sensorn uppdateras automatiskt när prisdata uppdateras (vanligtvis efter midnatt och när morgondagens data anländer). Få tillgång till tjänstesvarsdata direkt från entitetens attribut - strukturen matchar exakt vad get_chartdata returnerar."
|
||||
},
|
||||
"chart_metadata": {
|
||||
"description": "Lättviktig metadata för diagramkonfiguration",
|
||||
"long_description": "Tillhandahåller väsentliga diagramkonfigurationsvärden som sensorattribut. Användbart för vilket diagramkort som helst som behöver Y-axelgränser eller gradientpositionering. Sensorn anropar get_chartdata med endast-metadata-läge (ingen databehandling) och extraherar: yaxis_min, yaxis_max (föreslagen Y-axelomfång) och gradient_stop (genomsnittlig prisposition 0-100%). Statusen återspeglar tjänstanropsresultatet: 'ready' vid framgång, 'error' vid fel, 'pending' under initialisering.",
|
||||
"usage_tips": "Konfigurera via configuration.yaml under tibber_prices.chart_metadata_config (valfritt: day, minor_currency, resolution). Sensorn uppdateras automatiskt vid pris dataändringar. Få tillgång till metadata från attribut: yaxis_min, yaxis_max, gradient_stop. Använd med config-template-card eller vilket verktyg som helst som läser entitetsattribut - perfekt för dynamisk diagramkonfiguration utan manuella beräkningar."
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ async def call_chartdata_service_async(
|
|||
# Add required entry_id parameter
|
||||
service_params["entry_id"] = config_entry.entry_id
|
||||
|
||||
# Make sure metadata is never requested for this sensor
|
||||
service_params["metadata"] = "none"
|
||||
|
||||
# Call get_chartdata service using official HA service system
|
||||
try:
|
||||
response = await hass.services.async_call(
|
||||
|
|
|
|||
149
custom_components/tibber_prices/sensor/chart_metadata.py
Normal file
149
custom_components/tibber_prices/sensor/chart_metadata.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""Chart metadata export functionality for Tibber Prices sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from custom_components.tibber_prices.const import DATA_CHART_METADATA_CONFIG, DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def call_chartdata_service_for_metadata_async(
|
||||
hass: HomeAssistant,
|
||||
coordinator: TibberPricesDataUpdateCoordinator,
|
||||
config_entry: TibberPricesConfigEntry,
|
||||
) -> tuple[dict | None, str | None]:
|
||||
"""
|
||||
Call get_chartdata service with configuration from configuration.yaml for metadata (async).
|
||||
|
||||
Returns:
|
||||
Tuple of (response, error_message).
|
||||
If successful: (response_dict, None)
|
||||
If failed: (None, error_string)
|
||||
|
||||
"""
|
||||
# Get configuration from hass.data (loaded from configuration.yaml)
|
||||
domain_data = hass.data.get(DOMAIN, {})
|
||||
chart_metadata_config = domain_data.get(DATA_CHART_METADATA_CONFIG, {})
|
||||
|
||||
# Use chart_metadata_config directly (already a dict from async_setup)
|
||||
service_params = dict(chart_metadata_config) if chart_metadata_config else {}
|
||||
|
||||
# Add required entry_id parameter
|
||||
service_params["entry_id"] = config_entry.entry_id
|
||||
|
||||
# Force metadata to "only" - this sensor ONLY provides metadata
|
||||
service_params["metadata"] = "only"
|
||||
|
||||
# Default to minor_currency=True for ApexCharts compatibility (can be overridden in configuration.yaml)
|
||||
if "minor_currency" not in service_params:
|
||||
service_params["minor_currency"] = True
|
||||
|
||||
# Call get_chartdata service using official HA service system
|
||||
try:
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"get_chartdata",
|
||||
service_params,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
except Exception as ex:
|
||||
coordinator.logger.exception("Chart metadata service call failed")
|
||||
return None, str(ex)
|
||||
else:
|
||||
return response, None
|
||||
|
||||
|
||||
def get_chart_metadata_state(
|
||||
chart_metadata_response: dict | None,
|
||||
chart_metadata_error: str | None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Return state for chart_metadata sensor.
|
||||
|
||||
Args:
|
||||
chart_metadata_response: Last service response (or None)
|
||||
chart_metadata_error: Last error message (or None)
|
||||
|
||||
Returns:
|
||||
"error" if error occurred
|
||||
"ready" if metadata available
|
||||
"pending" if no data yet
|
||||
|
||||
"""
|
||||
if chart_metadata_error:
|
||||
return "error"
|
||||
if chart_metadata_response:
|
||||
return "ready"
|
||||
return "pending"
|
||||
|
||||
|
||||
def build_chart_metadata_attributes(
|
||||
chart_metadata_response: dict | None,
|
||||
chart_metadata_last_update: datetime | None,
|
||||
chart_metadata_error: str | None,
|
||||
) -> dict[str, object] | None:
|
||||
"""
|
||||
Return chart metadata from last service call as attributes.
|
||||
|
||||
Attribute order: timestamp, error (if any), metadata fields (at the end).
|
||||
|
||||
Args:
|
||||
chart_metadata_response: Last service response (should contain "metadata" key)
|
||||
chart_metadata_last_update: Timestamp of last update
|
||||
chart_metadata_error: Error message if service call failed
|
||||
|
||||
Returns:
|
||||
Dict with timestamp, optional error, and metadata fields.
|
||||
|
||||
"""
|
||||
# Build base attributes with timestamp FIRST
|
||||
attributes: dict[str, object] = {
|
||||
"timestamp": chart_metadata_last_update,
|
||||
}
|
||||
|
||||
# Add error message if service call failed
|
||||
if chart_metadata_error:
|
||||
attributes["error"] = chart_metadata_error
|
||||
|
||||
if not chart_metadata_response:
|
||||
# No data - only timestamp (and error if present)
|
||||
return attributes
|
||||
|
||||
# Extract metadata from response (get_chartdata returns {"metadata": {...}})
|
||||
metadata = chart_metadata_response.get("metadata", {})
|
||||
|
||||
# Extract the fields we care about for charts
|
||||
# These are the universal chart metadata fields useful for any chart card
|
||||
if metadata:
|
||||
yaxis_suggested = metadata.get("yaxis_suggested", {})
|
||||
price_stats = metadata.get("price_stats", {})
|
||||
combined_stats = price_stats.get("combined", {})
|
||||
|
||||
# Add yaxis bounds (useful for all chart cards)
|
||||
if "min" in yaxis_suggested:
|
||||
attributes["yaxis_min"] = yaxis_suggested["min"]
|
||||
if "max" in yaxis_suggested:
|
||||
attributes["yaxis_max"] = yaxis_suggested["max"]
|
||||
|
||||
# Add gradient stop position (useful for gradient-based charts)
|
||||
if "avg_position" in combined_stats:
|
||||
avg_position = combined_stats["avg_position"]
|
||||
attributes["gradient_stop"] = round(avg_position * 100)
|
||||
|
||||
# Add currency info (useful for labeling)
|
||||
if "currency" in metadata:
|
||||
attributes["currency"] = metadata["currency"]
|
||||
|
||||
# Add resolution info (interval duration in minutes)
|
||||
if "resolution" in metadata:
|
||||
attributes["resolution"] = metadata["resolution"]
|
||||
|
||||
return attributes
|
||||
|
|
@ -70,6 +70,11 @@ from .chart_data import (
|
|||
call_chartdata_service_async,
|
||||
get_chart_data_state,
|
||||
)
|
||||
from .chart_metadata import (
|
||||
build_chart_metadata_attributes,
|
||||
call_chartdata_service_for_metadata_async,
|
||||
get_chart_metadata_state,
|
||||
)
|
||||
from .helpers import aggregate_level_data, aggregate_rating_data
|
||||
from .value_getters import get_value_getter_mapping
|
||||
|
||||
|
|
@ -117,6 +122,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
self._chart_data_last_update = None # Track last service call timestamp
|
||||
self._chart_data_error = None # Track last service call error
|
||||
self._chart_data_response = None # Store service response for attributes
|
||||
# Chart metadata (for chart_metadata sensor)
|
||||
self._chart_metadata_last_update = None # Track last service call timestamp
|
||||
self._chart_metadata_error = None # Track last service call error
|
||||
self._chart_metadata_response = None # Store service response for attributes
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
|
|
@ -138,6 +147,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
if self.entity_description.key == "chart_data_export":
|
||||
await self._refresh_chart_data()
|
||||
|
||||
# For chart_metadata, trigger initial service call
|
||||
if self.entity_description.key == "chart_metadata":
|
||||
await self._refresh_chart_metadata()
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""When entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
|
@ -198,6 +211,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
# Schedule async refresh as a task (we're in a callback)
|
||||
self.hass.async_create_task(self._refresh_chart_data())
|
||||
|
||||
# Refresh chart metadata when coordinator updates (new price data or user data)
|
||||
if self.entity_description.key == "chart_metadata":
|
||||
# Schedule async refresh as a task (we're in a callback)
|
||||
self.hass.async_create_task(self._refresh_chart_metadata())
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _get_value_getter(self) -> Callable | None:
|
||||
|
|
@ -216,6 +234,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
get_next_avg_n_hours_value=self._get_next_avg_n_hours_value,
|
||||
get_data_timestamp=self._get_data_timestamp,
|
||||
get_chart_data_export_value=self._get_chart_data_export_value,
|
||||
get_chart_metadata_value=self._get_chart_metadata_value,
|
||||
)
|
||||
return handlers.get(self.entity_description.key)
|
||||
|
||||
|
|
@ -831,6 +850,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
if key == "chart_data_export":
|
||||
return self._get_chart_data_export_attributes()
|
||||
|
||||
# Special handling for chart_metadata - returns metadata in attributes
|
||||
if key == "chart_metadata":
|
||||
return self._get_chart_metadata_attributes()
|
||||
|
||||
# Prepare cached data that attribute builders might need
|
||||
cached_data = {
|
||||
"trend_attributes": self._trend_calculator.get_trend_attributes(),
|
||||
|
|
@ -906,3 +929,36 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
chart_data_last_update=self._chart_data_last_update,
|
||||
chart_data_error=self._chart_data_error,
|
||||
)
|
||||
|
||||
def _get_chart_metadata_value(self) -> str | None:
|
||||
"""Return state for chart_metadata sensor."""
|
||||
return get_chart_metadata_state(
|
||||
chart_metadata_response=self._chart_metadata_response,
|
||||
chart_metadata_error=self._chart_metadata_error,
|
||||
)
|
||||
|
||||
async def _refresh_chart_metadata(self) -> None:
|
||||
"""Refresh chart metadata by calling get_chartdata service with metadata=only."""
|
||||
response, error = await call_chartdata_service_for_metadata_async(
|
||||
hass=self.hass,
|
||||
coordinator=self.coordinator,
|
||||
config_entry=self.coordinator.config_entry,
|
||||
)
|
||||
self._chart_metadata_response = response
|
||||
time = self.coordinator.time
|
||||
self._chart_metadata_last_update = time.now()
|
||||
self._chart_metadata_error = error
|
||||
# Trigger state update after refresh
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _get_chart_metadata_attributes(self) -> dict[str, object] | None:
|
||||
"""
|
||||
Return chart metadata from last service call as attributes.
|
||||
|
||||
Delegates to chart_metadata module for attribute building.
|
||||
"""
|
||||
return build_chart_metadata_attributes(
|
||||
chart_metadata_response=self._chart_metadata_response,
|
||||
chart_metadata_last_update=self._chart_metadata_last_update,
|
||||
chart_metadata_error=self._chart_metadata_error,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -843,6 +843,7 @@ DIAGNOSTIC_SENSORS = (
|
|||
options=["cached", "fresh", "refreshing", "searching_tomorrow", "turnover_pending", "error"],
|
||||
state_class=None, # Status value: no statistics
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=True, # Critical for debugging
|
||||
),
|
||||
# Home metadata from user data
|
||||
SensorEntityDescription(
|
||||
|
|
@ -1003,6 +1004,16 @@ DIAGNOSTIC_SENSORS = (
|
|||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False, # Opt-in
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="chart_metadata",
|
||||
translation_key="chart_metadata",
|
||||
name="Chart Metadata",
|
||||
icon="mdi:chart-box-outline",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["pending", "ready", "error"],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=True, # Critical for chart features
|
||||
),
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
|||
get_next_avg_n_hours_value: Callable[[int], float | None],
|
||||
get_data_timestamp: Callable[[], datetime | None],
|
||||
get_chart_data_export_value: Callable[[], str | None],
|
||||
get_chart_metadata_value: Callable[[], str | None],
|
||||
) -> dict[str, Callable]:
|
||||
"""
|
||||
Build mapping from entity key to value getter callable.
|
||||
|
|
@ -61,6 +62,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
|||
get_next_avg_n_hours_value: Method for next N-hour average forecasts
|
||||
get_data_timestamp: Method for data timestamp sensor
|
||||
get_chart_data_export_value: Method for chart data export sensor
|
||||
get_chart_metadata_value: Method for chart metadata sensor
|
||||
|
||||
Returns:
|
||||
Dictionary mapping entity keys to their value getter callables.
|
||||
|
|
@ -275,4 +277,6 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
|||
),
|
||||
# Chart data export sensor
|
||||
"chart_data_export": get_chart_data_export_value,
|
||||
# Chart metadata sensor
|
||||
"chart_metadata": get_chart_metadata_value,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Response: YAML configuration dict for ApexCharts card
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
|
@ -35,7 +35,6 @@ from custom_components.tibber_prices.const import (
|
|||
format_price_unit_minor,
|
||||
get_translation,
|
||||
)
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
|
|
@ -46,9 +45,11 @@ from homeassistant.helpers.entity_registry import (
|
|||
)
|
||||
|
||||
from .formatters import get_level_translation
|
||||
from .get_chartdata import handle_chartdata
|
||||
from .helpers import get_entry_and_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
# Service constants
|
||||
APEXCHARTS_YAML_SERVICE_NAME: Final = "get_apexcharts_yaml"
|
||||
ATTR_DAY: Final = "day"
|
||||
|
|
@ -179,7 +180,90 @@ def _get_current_price_entity(entity_registry: EntityRegistry, entry_id: str) ->
|
|||
)
|
||||
|
||||
|
||||
async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa: PLR0912, PLR0915
|
||||
def _check_custom_cards_installed(hass: Any) -> dict[str, bool]:
|
||||
"""
|
||||
Check if required custom cards are installed via HACS/Lovelace resources.
|
||||
|
||||
Args:
|
||||
hass: Home Assistant instance
|
||||
|
||||
Returns:
|
||||
Dictionary with card names as keys and installation status as bool values
|
||||
|
||||
"""
|
||||
installed_cards = {"apexcharts-card": False, "config-template-card": False}
|
||||
|
||||
# Access Lovelace resources via the new API (2026.2+)
|
||||
lovelace_data = hass.data.get("lovelace")
|
||||
if lovelace_data and hasattr(lovelace_data, "resources"):
|
||||
try:
|
||||
# ResourceStorageCollection has async_items() method
|
||||
resources = lovelace_data.resources
|
||||
if hasattr(resources, "async_items") and hasattr(resources, "data") and isinstance(resources.data, dict):
|
||||
# Can't use await here, so we check the internal storage
|
||||
for resource in resources.data.values():
|
||||
url = resource.get("url", "") if isinstance(resource, dict) else ""
|
||||
if "apexcharts-card" in url:
|
||||
installed_cards["apexcharts-card"] = True
|
||||
if "config-template-card" in url:
|
||||
installed_cards["config-template-card"] = True
|
||||
except (AttributeError, TypeError):
|
||||
# Fallback: can't determine, assume not installed
|
||||
pass
|
||||
|
||||
return installed_cards
|
||||
|
||||
|
||||
def _get_sensor_disabled_notification(language: str) -> dict[str, str]:
|
||||
"""Get notification texts for disabled chart metadata sensor."""
|
||||
title = get_translation(["apexcharts", "notification", "metadata_sensor_unavailable", "title"], language)
|
||||
message = get_translation(["apexcharts", "notification", "metadata_sensor_unavailable", "message"], language)
|
||||
|
||||
if not title:
|
||||
title = get_translation(["apexcharts", "notification", "metadata_sensor_unavailable", "title"], "en")
|
||||
if not message:
|
||||
message = get_translation(["apexcharts", "notification", "metadata_sensor_unavailable", "message"], "en")
|
||||
|
||||
if not title:
|
||||
title = "Tibber Prices: Chart Metadata Sensor Disabled"
|
||||
if not message:
|
||||
message = (
|
||||
"The Chart Metadata sensor is currently disabled. "
|
||||
"Enable it to get optimized chart scaling and gradient colors.\n\n"
|
||||
"[Open Tibber Prices Integration](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)\n\n"
|
||||
"After enabling the sensor, regenerate the ApexCharts YAML."
|
||||
)
|
||||
|
||||
return {"title": title, "message": message}
|
||||
|
||||
|
||||
def _get_missing_cards_notification(language: str, missing_cards: list[str]) -> dict[str, str]:
|
||||
"""Get notification texts for missing custom cards."""
|
||||
title = get_translation(["apexcharts", "notification", "missing_cards", "title"], language)
|
||||
message = get_translation(["apexcharts", "notification", "missing_cards", "message"], language)
|
||||
|
||||
if not title:
|
||||
title = get_translation(["apexcharts", "notification", "missing_cards", "title"], "en")
|
||||
if not message:
|
||||
message = get_translation(["apexcharts", "notification", "missing_cards", "message"], "en")
|
||||
|
||||
if not title:
|
||||
title = "Tibber Prices: Missing Custom Cards"
|
||||
if not message:
|
||||
message = (
|
||||
"The following custom cards are required but not installed:\n"
|
||||
"{cards}\n\n"
|
||||
"Please click the links above to install them from HACS."
|
||||
)
|
||||
|
||||
# Replace {cards} placeholder
|
||||
cards_list = "\n".join(missing_cards)
|
||||
message = message.replace("{cards}", cards_list)
|
||||
|
||||
return {"title": title, "message": message}
|
||||
|
||||
|
||||
async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa: PLR0912, PLR0915, C901
|
||||
"""
|
||||
Return YAML snippet for ApexCharts card.
|
||||
|
||||
|
|
@ -313,9 +397,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
# ApexCharts card requires direct entity data for extremas feature, not dynamically generated data
|
||||
|
||||
# Get translated name for best price periods (needed for tooltip formatter)
|
||||
best_price_name = (
|
||||
get_translation(["binary_sensor", "best_price_period", "name"], user_language) or "Best Price Period"
|
||||
)
|
||||
best_price_name = get_translation(["apexcharts", "best_price_period_name"], user_language) or "Best Price Period"
|
||||
|
||||
# Add best price period highlight overlay (vertical bands from top to bottom)
|
||||
if highlight_best_price and entity_map:
|
||||
|
|
@ -549,52 +631,96 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
if current_price_sensor:
|
||||
trigger_entities.append(current_price_sensor)
|
||||
|
||||
# Pre-calculate metadata server-side for dynamic yaxis and gradient
|
||||
# This avoids async issues with config-template-card variables
|
||||
# Create service call to get metadata
|
||||
metadata_call = ServiceCall(
|
||||
hass=hass,
|
||||
domain="tibber_prices",
|
||||
service="get_chartdata",
|
||||
data={
|
||||
"entry_id": entry_id,
|
||||
"minor_currency": True,
|
||||
"metadata": "only",
|
||||
},
|
||||
context=call.context,
|
||||
return_response=True,
|
||||
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
||||
# The chart_metadata sensor provides yaxis_min, yaxis_max, and gradient_stop
|
||||
# as attributes, avoiding the need for async service calls in templates
|
||||
chart_metadata_sensor = next(
|
||||
(
|
||||
entity.entity_id
|
||||
for entity in entity_registry.entities.values()
|
||||
if entity.config_entry_id == entry_id
|
||||
and entity.unique_id
|
||||
and entity.unique_id.endswith("_chart_metadata")
|
||||
),
|
||||
None,
|
||||
)
|
||||
metadata_response = await handle_chartdata(metadata_call)
|
||||
metadata = metadata_response.get("metadata", {})
|
||||
|
||||
# Extract values with fallbacks
|
||||
yaxis_min = metadata.get("yaxis_suggested", {}).get("min", 0)
|
||||
yaxis_max = metadata.get("yaxis_suggested", {}).get("max", 100)
|
||||
avg_position = metadata.get("price_stats", {}).get("combined", {}).get("avg_position", 0.5)
|
||||
gradient_stop = round(avg_position * 100)
|
||||
# Track warning if sensor not available
|
||||
metadata_warning = None
|
||||
use_sensor_metadata = False
|
||||
|
||||
return {
|
||||
# Check if sensor exists and is ready
|
||||
if chart_metadata_sensor:
|
||||
metadata_state = hass.states.get(chart_metadata_sensor)
|
||||
if metadata_state and metadata_state.state == "ready":
|
||||
# Sensor ready - will use template variables
|
||||
use_sensor_metadata = True
|
||||
else:
|
||||
# Sensor not ready - will show notification
|
||||
metadata_warning = True
|
||||
else:
|
||||
# Sensor not found - will show notification
|
||||
metadata_warning = True
|
||||
|
||||
# Set fallback values if sensor not used
|
||||
if not use_sensor_metadata:
|
||||
gradient_stop = 50
|
||||
|
||||
# Build yaxis config (only include min/max if not None)
|
||||
yaxis_price_config = {
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": True,
|
||||
},
|
||||
}
|
||||
|
||||
gradient_stops = [gradient_stop, 100]
|
||||
entities_list = trigger_entities
|
||||
else:
|
||||
# Use template variables to read sensor dynamically
|
||||
# Add chart_metadata sensor to entities list
|
||||
entities_list = [*trigger_entities, chart_metadata_sensor]
|
||||
|
||||
# Build yaxis config with template variables
|
||||
yaxis_price_config = {
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"min": "${v_yaxis_min}",
|
||||
"max": "${v_yaxis_max}",
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": False,
|
||||
},
|
||||
}
|
||||
|
||||
gradient_stops = ["${v_gradient_stop}", 100]
|
||||
|
||||
# Build variables dict
|
||||
variables_dict = {"v_graph_span": template_graph_span}
|
||||
if use_sensor_metadata:
|
||||
# Add dynamic metadata variables from sensor
|
||||
variables_dict.update(
|
||||
{
|
||||
"v_yaxis_min": f"states['{chart_metadata_sensor}'].attributes.yaxis_min",
|
||||
"v_yaxis_max": f"states['{chart_metadata_sensor}'].attributes.yaxis_max",
|
||||
"v_gradient_stop": f"states['{chart_metadata_sensor}'].attributes.gradient_stop",
|
||||
}
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
"type": "custom:config-template-card",
|
||||
"variables": {
|
||||
"v_graph_span": template_graph_span,
|
||||
},
|
||||
"entities": trigger_entities,
|
||||
"variables": variables_dict,
|
||||
"entities": entities_list,
|
||||
"card": {
|
||||
**result,
|
||||
"span": {"start": "minute", "offset": "-120min"},
|
||||
"graph_span": "${v_graph_span}",
|
||||
"yaxis": [
|
||||
{
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"min": yaxis_min,
|
||||
"max": yaxis_max,
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": False,
|
||||
},
|
||||
},
|
||||
yaxis_price_config,
|
||||
{
|
||||
"id": "highlight",
|
||||
"min": 0,
|
||||
|
|
@ -615,12 +741,53 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
"opacityFrom": 0.7,
|
||||
"opacityTo": 0.25,
|
||||
"gradientToColors": ["#transparent"],
|
||||
"stops": [gradient_stop, 100],
|
||||
"stops": gradient_stops,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Create separate notifications for different issues
|
||||
if metadata_warning:
|
||||
# Notification 1: Chart Metadata Sensor disabled
|
||||
notification_texts = _get_sensor_disabled_notification(user_language)
|
||||
await hass.services.async_call(
|
||||
"persistent_notification",
|
||||
"create",
|
||||
{
|
||||
"message": notification_texts["message"],
|
||||
"title": notification_texts["title"],
|
||||
"notification_id": f"tibber_prices_chart_metadata_{entry_id}",
|
||||
},
|
||||
)
|
||||
|
||||
# Check which custom cards are installed (always check, independent of sensor state)
|
||||
installed_cards = _check_custom_cards_installed(hass)
|
||||
missing_cards = [
|
||||
"[apexcharts-card](https://my.home-assistant.io/redirect/hacs_repository/?owner=RomRider&repository=apexcharts-card)"
|
||||
if not installed_cards["apexcharts-card"]
|
||||
else None,
|
||||
"[config-template-card](https://my.home-assistant.io/redirect/hacs_repository/?owner=iantrich&repository=config-template-card)"
|
||||
if not installed_cards["config-template-card"]
|
||||
else None,
|
||||
]
|
||||
missing_cards = [card for card in missing_cards if card] # Filter out None
|
||||
|
||||
if missing_cards:
|
||||
# Notification 2: Missing Custom Cards
|
||||
notification_texts = _get_missing_cards_notification(user_language, missing_cards)
|
||||
await hass.services.async_call(
|
||||
"persistent_notification",
|
||||
"create",
|
||||
{
|
||||
"message": notification_texts["message"],
|
||||
"title": notification_texts["title"],
|
||||
"notification_id": f"tibber_prices_missing_cards_{entry_id}",
|
||||
},
|
||||
)
|
||||
|
||||
return result_dict
|
||||
# Rolling window modes (day is None or rolling_window): Dynamic offset
|
||||
# Add graph_span to base config (48h window)
|
||||
result["graph_span"] = graph_span_value
|
||||
|
|
@ -630,36 +797,90 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
# If 'off' (no tomorrow data) → offset +0d (show yesterday+today)
|
||||
template_value = f"states['{tomorrow_data_sensor}'].state === 'on' ? '+1d' : '+0d'"
|
||||
|
||||
# Pre-calculate metadata server-side for dynamic yaxis and gradient
|
||||
# This avoids async issues with config-template-card variables
|
||||
# Create service call to get metadata
|
||||
metadata_call = ServiceCall(
|
||||
hass=hass,
|
||||
domain="tibber_prices",
|
||||
service="get_chartdata",
|
||||
data={
|
||||
"entry_id": entry_id,
|
||||
"minor_currency": True,
|
||||
"metadata": "only",
|
||||
},
|
||||
context=call.context,
|
||||
return_response=True,
|
||||
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
||||
# The chart_metadata sensor provides yaxis_min, yaxis_max, and gradient_stop
|
||||
# as attributes, avoiding the need for async service calls in templates
|
||||
chart_metadata_sensor = next(
|
||||
(
|
||||
entity.entity_id
|
||||
for entity in entity_registry.entities.values()
|
||||
if entity.config_entry_id == entry_id
|
||||
and entity.unique_id
|
||||
and entity.unique_id.endswith("_chart_metadata")
|
||||
),
|
||||
None,
|
||||
)
|
||||
metadata_response = await handle_chartdata(metadata_call)
|
||||
metadata = metadata_response.get("metadata", {})
|
||||
|
||||
# Extract values with fallbacks
|
||||
yaxis_min = metadata.get("yaxis_suggested", {}).get("min", 0)
|
||||
yaxis_max = metadata.get("yaxis_suggested", {}).get("max", 100)
|
||||
avg_position = metadata.get("price_stats", {}).get("combined", {}).get("avg_position", 0.5)
|
||||
gradient_stop = round(avg_position * 100)
|
||||
# Track warning if sensor not available
|
||||
metadata_warning = None
|
||||
use_sensor_metadata = False
|
||||
|
||||
return {
|
||||
# Check if sensor exists and is ready
|
||||
if chart_metadata_sensor:
|
||||
metadata_state = hass.states.get(chart_metadata_sensor)
|
||||
if metadata_state and metadata_state.state == "ready":
|
||||
# Sensor ready - will use template variables
|
||||
use_sensor_metadata = True
|
||||
else:
|
||||
# Sensor not ready - will show notification
|
||||
metadata_warning = True
|
||||
else:
|
||||
# Sensor not found - will show notification
|
||||
metadata_warning = True
|
||||
|
||||
# Set fallback values if sensor not used
|
||||
if not use_sensor_metadata:
|
||||
gradient_stop = 50
|
||||
|
||||
# Build yaxis config (only include min/max if not None)
|
||||
yaxis_price_config = {
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": True,
|
||||
},
|
||||
}
|
||||
|
||||
gradient_stops = [gradient_stop, 100]
|
||||
entities_list = [tomorrow_data_sensor]
|
||||
else:
|
||||
# Use template variables to read sensor dynamically
|
||||
# Add chart_metadata sensor to entities list
|
||||
entities_list = [tomorrow_data_sensor, chart_metadata_sensor]
|
||||
|
||||
# Build yaxis config with template variables
|
||||
yaxis_price_config = {
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"min": "${v_yaxis_min}",
|
||||
"max": "${v_yaxis_max}",
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": False,
|
||||
},
|
||||
}
|
||||
|
||||
gradient_stops = ["${v_gradient_stop}", 100]
|
||||
|
||||
# Build variables dict
|
||||
variables_dict = {"v_offset": template_value}
|
||||
if use_sensor_metadata:
|
||||
# Add dynamic metadata variables from sensor
|
||||
variables_dict.update(
|
||||
{
|
||||
"v_yaxis_min": f"states['{chart_metadata_sensor}'].attributes.yaxis_min",
|
||||
"v_yaxis_max": f"states['{chart_metadata_sensor}'].attributes.yaxis_max",
|
||||
"v_gradient_stop": f"states['{chart_metadata_sensor}'].attributes.gradient_stop",
|
||||
}
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
"type": "custom:config-template-card",
|
||||
"variables": {
|
||||
"v_offset": template_value,
|
||||
},
|
||||
"entities": [tomorrow_data_sensor],
|
||||
"variables": variables_dict,
|
||||
"entities": entities_list,
|
||||
"card": {
|
||||
**result,
|
||||
"span": {
|
||||
|
|
@ -667,17 +888,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
"offset": "${v_offset}",
|
||||
},
|
||||
"yaxis": [
|
||||
{
|
||||
"id": "price",
|
||||
"decimals": 2,
|
||||
"min": yaxis_min,
|
||||
"max": yaxis_max,
|
||||
"apex_config": {
|
||||
"title": {"text": price_unit},
|
||||
"decimalsInFloat": 0,
|
||||
"forceNiceScale": False,
|
||||
},
|
||||
},
|
||||
yaxis_price_config,
|
||||
{
|
||||
"id": "highlight",
|
||||
"min": 0,
|
||||
|
|
@ -698,13 +909,54 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
|||
"opacityFrom": 0.7,
|
||||
"opacityTo": 0.25,
|
||||
"gradientToColors": ["#transparent"],
|
||||
"stops": [gradient_stop, 100],
|
||||
"stops": gradient_stops,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Create separate notifications for different issues
|
||||
if metadata_warning:
|
||||
# Notification 1: Chart Metadata Sensor disabled
|
||||
notification_texts = _get_sensor_disabled_notification(user_language)
|
||||
await hass.services.async_call(
|
||||
"persistent_notification",
|
||||
"create",
|
||||
{
|
||||
"message": notification_texts["message"],
|
||||
"title": notification_texts["title"],
|
||||
"notification_id": f"tibber_prices_chart_metadata_{entry_id}",
|
||||
},
|
||||
)
|
||||
|
||||
# Check which custom cards are installed (always check, independent of sensor state)
|
||||
installed_cards = _check_custom_cards_installed(hass)
|
||||
missing_cards = [
|
||||
"[apexcharts-card](https://my.home-assistant.io/redirect/hacs_repository/?owner=RomRider&repository=apexcharts-card)"
|
||||
if not installed_cards["apexcharts-card"]
|
||||
else None,
|
||||
"[config-template-card](https://my.home-assistant.io/redirect/hacs_repository/?owner=iantrich&repository=config-template-card)"
|
||||
if not installed_cards["config-template-card"]
|
||||
else None,
|
||||
]
|
||||
missing_cards = [card for card in missing_cards if card] # Filter out None
|
||||
|
||||
if missing_cards:
|
||||
# Notification 2: Missing Custom Cards
|
||||
notification_texts = _get_missing_cards_notification(user_language, missing_cards)
|
||||
await hass.services.async_call(
|
||||
"persistent_notification",
|
||||
"create",
|
||||
{
|
||||
"message": notification_texts["message"],
|
||||
"title": notification_texts["title"],
|
||||
"notification_id": f"tibber_prices_missing_cards_{entry_id}",
|
||||
},
|
||||
)
|
||||
|
||||
return result_dict
|
||||
|
||||
# Fallback if sensor not found
|
||||
if day == "rolling_window_autozoom":
|
||||
# Fallback: show today with 24h span
|
||||
|
|
|
|||
|
|
@ -799,6 +799,14 @@
|
|||
"ready": "Bereit",
|
||||
"error": "Fehler"
|
||||
}
|
||||
},
|
||||
"chart_metadata": {
|
||||
"name": "Diagramm-Metadaten",
|
||||
"state": {
|
||||
"pending": "Ausstehend",
|
||||
"ready": "Bereit",
|
||||
"error": "Fehler"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -795,6 +795,14 @@
|
|||
"ready": "Ready",
|
||||
"error": "Error"
|
||||
}
|
||||
},
|
||||
"chart_metadata": {
|
||||
"name": "Chart Metadata",
|
||||
"state": {
|
||||
"pending": "Pending",
|
||||
"ready": "Ready",
|
||||
"error": "Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -795,6 +795,14 @@
|
|||
"ready": "Klar",
|
||||
"error": "Feil"
|
||||
}
|
||||
},
|
||||
"chart_metadata": {
|
||||
"name": "Diagrammetadata",
|
||||
"state": {
|
||||
"pending": "Venter",
|
||||
"ready": "Klar",
|
||||
"error": "Feil"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -785,6 +785,14 @@
|
|||
"error": "Fout"
|
||||
}
|
||||
},
|
||||
"chart_metadata": {
|
||||
"name": "Grafiekmetadata",
|
||||
"state": {
|
||||
"pending": "In afwachting",
|
||||
"ready": "Klaar",
|
||||
"error": "Fout"
|
||||
}
|
||||
},
|
||||
"data_lifecycle_status": {
|
||||
"name": "Datalevenscyclus-status",
|
||||
"state": {
|
||||
|
|
|
|||
|
|
@ -795,6 +795,14 @@
|
|||
"ready": "Redo",
|
||||
"error": "Fel"
|
||||
}
|
||||
},
|
||||
"chart_metadata": {
|
||||
"name": "Diagrammetadata",
|
||||
"state": {
|
||||
"pending": "Väntar",
|
||||
"ready": "Redo",
|
||||
"error": "Fel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue