feat(services): add energy/tax fields to get_chartdata action

Add four optional parameters to the get_chartdata service:
- include_energy: Include raw energy/spot price (default: false)
- include_tax: Include tax component (default: false)
- energy_field: Custom field name (default: energy_price)
- tax_field: Custom field name (default: tax)

Custom field names allow direct compatibility with ApexCharts
and other charting tools without post-processing.

All code paths (all/segments/none insert_nulls modes) and the
last-interval handler include energy/tax when enabled.

Added translations for all 5 languages (en, de, nl, nb, sv).

Impact: Users can include price composition data in chart exports,
enabling visual breakdowns of energy cost vs. taxes in dashboards.
This commit is contained in:
Julian Pawlowski 2026-04-09 18:27:53 +00:00
parent edabb49309
commit d1b25e9cfe
7 changed files with 139 additions and 0 deletions

View file

@ -219,6 +219,16 @@ get_chartdata:
default: false default: false
selector: selector:
boolean: boolean:
include_energy:
required: false
default: false
selector:
boolean:
include_tax:
required: false
default: false
selector:
boolean:
start_time_field: start_time_field:
required: false required: false
example: time example: time
@ -246,6 +256,14 @@ get_chartdata:
required: false required: false
selector: selector:
text: text:
energy_field:
required: false
selector:
text:
tax_field:
required: false
selector:
text:
arrays_of_arrays: arrays_of_arrays:
collapsed: true collapsed: true
fields: fields:

View file

@ -280,6 +280,8 @@ CHARTDATA_SERVICE_SCHEMA: Final = vol.Schema(
vol.Optional("include_level", default=False): bool, vol.Optional("include_level", default=False): bool,
vol.Optional("include_rating_level", default=False): bool, vol.Optional("include_rating_level", default=False): bool,
vol.Optional("include_average", default=False): bool, vol.Optional("include_average", default=False): bool,
vol.Optional("include_energy", default=False): bool,
vol.Optional("include_tax", default=False): bool,
vol.Optional("level_filter"): vol.All( vol.Optional("level_filter"): vol.All(
vol.Coerce(list), vol.Coerce(list),
normalize_level_filter, normalize_level_filter,
@ -310,6 +312,8 @@ CHARTDATA_SERVICE_SCHEMA: Final = vol.Schema(
vol.Optional("level_field", default="level"): str, vol.Optional("level_field", default="level"): str,
vol.Optional("rating_level_field", default="rating_level"): str, vol.Optional("rating_level_field", default="rating_level"): str,
vol.Optional("average_field", default="average"): str, vol.Optional("average_field", default="average"): str,
vol.Optional("energy_field", default="energy_price"): str,
vol.Optional("tax_field", default="tax"): str,
vol.Optional("data_key", default="data"): str, vol.Optional("data_key", default="data"): str,
vol.Optional("metadata", default="include"): vol.In(["include", "only", "none"]), vol.Optional("metadata", default="include"): vol.In(["include", "only", "none"]),
} }
@ -368,6 +372,8 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
level_field = call.data.get("level_field", "level") level_field = call.data.get("level_field", "level")
rating_level_field = call.data.get("rating_level_field", "rating_level") rating_level_field = call.data.get("rating_level_field", "rating_level")
average_field = call.data.get("average_field", "average") average_field = call.data.get("average_field", "average")
energy_field = call.data.get("energy_field", "energy_price")
tax_field = call.data.get("tax_field", "tax")
data_key = call.data.get("data_key", "data") data_key = call.data.get("data_key", "data")
resolution = call.data.get("resolution", "interval") resolution = call.data.get("resolution", "interval")
output_format = call.data.get("output_format", "array_of_objects") output_format = call.data.get("output_format", "array_of_objects")
@ -377,6 +383,8 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
include_level = call.data.get("include_level", False) include_level = call.data.get("include_level", False)
include_rating_level = call.data.get("include_rating_level", False) include_rating_level = call.data.get("include_rating_level", False)
include_average = call.data.get("include_average", False) include_average = call.data.get("include_average", False)
include_energy = call.data.get("include_energy", False)
include_tax = call.data.get("include_tax", False)
insert_nulls = call.data.get("insert_nulls", "none") insert_nulls = call.data.get("insert_nulls", "none")
connect_segments = call.data.get("connect_segments", False) connect_segments = call.data.get("connect_segments", False)
add_trailing_null = call.data.get("add_trailing_null", False) add_trailing_null = call.data.get("add_trailing_null", False)
@ -432,6 +440,10 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
include_rating_level = True include_rating_level = True
if average_field in array_fields_template: if average_field in array_fields_template:
include_average = True include_average = True
if energy_field in array_fields_template:
include_energy = True
if tax_field in array_fields_template:
include_tax = True
# Get thresholds from config for rating aggregation # Get thresholds from config for rating aggregation
threshold_low = coordinator.config_entry.options.get( threshold_low = coordinator.config_entry.options.get(
@ -470,6 +482,25 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
# Get price data for all requested days # Get price data for all requested days
chart_data = [] chart_data = []
def _add_energy_tax_fields(data_point: dict, interval: dict, price: float | None) -> None:
"""Add energy and tax fields to a data point if requested and price is not None."""
if price is None:
return
if include_energy and "energy" in interval:
energy_val = interval["energy"]
if energy_val is not None:
energy_val = round(float(energy_val) * 100, 2) if subunit_currency else round(float(energy_val), 4)
if round_decimals is not None:
energy_val = round(energy_val, round_decimals)
data_point[energy_field] = energy_val
if include_tax and "tax" in interval:
tax_val = interval["tax"]
if tax_val is not None:
tax_val = round(float(tax_val) * 100, 2) if subunit_currency else round(float(tax_val), 4)
if round_decimals is not None:
tax_val = round(tax_val, round_decimals)
data_point[tax_field] = tax_val
# Build set of timestamps that match period_filter if specified # Build set of timestamps that match period_filter if specified
period_timestamps = None period_timestamps = None
if period_filter: if period_filter:
@ -610,6 +641,9 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
if include_average and day_key and day_key in day_averages: if include_average and day_key and day_key in day_averages:
data_point[average_field] = day_averages[day_key] data_point[average_field] = day_averages[day_key]
# Add energy/tax if requested
_add_energy_tax_fields(data_point, interval, price)
chart_data.append(data_point) chart_data.append(data_point)
elif insert_nulls == "segments" and (level_filter or rating_level_filter): elif insert_nulls == "segments" and (level_filter or rating_level_filter):
@ -667,6 +701,8 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
if include_average and day_key and day_key in day_averages: if include_average and day_key and day_key in day_averages:
data_point[average_field] = day_averages[day_key] data_point[average_field] = day_averages[day_key]
_add_energy_tax_fields(data_point, interval, converted_price)
chart_data.append(data_point) chart_data.append(data_point)
# AFTER the real point: Add END-BRIDGE to draw vertical line DOWN to previous price # AFTER the real point: Add END-BRIDGE to draw vertical line DOWN to previous price
@ -806,6 +842,9 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
day_key = _get_day_key_for_interval(last_start_time) day_key = _get_day_key_for_interval(last_start_time)
if include_average and day_key and day_key in day_averages: if include_average and day_key and day_key in day_averages:
last_data_point[average_field] = day_averages[day_key] last_data_point[average_field] = day_averages[day_key]
_add_energy_tax_fields(last_data_point, last_interval, converted_last_price)
chart_data.append(last_data_point) chart_data.append(last_data_point)
# Extend to end of selected time range (midnight after last day) # Extend to end of selected time range (midnight after last day)
@ -882,6 +921,8 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
if include_average and day_key and day_key in day_averages: if include_average and day_key and day_key in day_averages:
data_point[average_field] = day_averages[day_key] data_point[average_field] = day_averages[day_key]
_add_energy_tax_fields(data_point, interval, price)
chart_data.append(data_point) chart_data.append(data_point)
# Remove trailing null values ONLY for insert_nulls='segments' mode. # Remove trailing null values ONLY for insert_nulls='segments' mode.

View file

@ -1174,6 +1174,14 @@
"name": "Durchschnitt einschließen", "name": "Durchschnitt einschließen",
"description": "Den Tagesdurchschnittspreis in jedem Datenpunkt zum Vergleich einschließen." "description": "Den Tagesdurchschnittspreis in jedem Datenpunkt zum Vergleich einschließen."
}, },
"include_energy": {
"name": "Energiepreis einschließen",
"description": "Den reinen Energie-/Spotpreis (ohne Steuern und Gebühren) in jedem Datenpunkt einschließen. Dies ist das 'energy'-Feld aus der Tibber-API, nützlich für Einspeisungs-/Salderungsberechnungen."
},
"include_tax": {
"name": "Steueranteil einschließen",
"description": "Den Steueranteil des Preises in jedem Datenpunkt einschließen. Dies ist das 'tax'-Feld aus der Tibber-API, das Gebühren, Steuern und Netzentgelte darstellt."
},
"level_filter": { "level_filter": {
"name": "Preisniveau-Filter", "name": "Preisniveau-Filter",
"description": "Intervalle filtern, um nur bestimmte Tibber-Preisniveaus einzuschließen (sehr günstig/günstig/normal/teuer/sehr teuer). Falls nicht angegeben, werden alle Niveaus eingeschlossen." "description": "Intervalle filtern, um nur bestimmte Tibber-Preisniveaus einzuschließen (sehr günstig/günstig/normal/teuer/sehr teuer). Falls nicht angegeben, werden alle Niveaus eingeschlossen."
@ -1222,6 +1230,14 @@
"name": "Durchschnitts-Feldname", "name": "Durchschnitts-Feldname",
"description": "Benutzerdefinierter Name für das Durchschnitts-Feld in der Ausgabe. Standard ist 'average', falls nicht angegeben. Wird nur verwendet, wenn include_average aktiviert ist." "description": "Benutzerdefinierter Name für das Durchschnitts-Feld in der Ausgabe. Standard ist 'average', falls nicht angegeben. Wird nur verwendet, wenn include_average aktiviert ist."
}, },
"energy_field": {
"name": "Energiepreis-Feldname",
"description": "Benutzerdefinierter Name für das Energiepreis-Feld in der Ausgabe. Standard ist 'energy_price', falls nicht angegeben. Wird nur verwendet, wenn include_energy aktiviert ist."
},
"tax_field": {
"name": "Steuer-Feldname",
"description": "Benutzerdefinierter Name für das Steuer-Feld in der Ausgabe. Standard ist 'tax', falls nicht angegeben. Wird nur verwendet, wenn include_tax aktiviert ist."
},
"metadata": { "metadata": {
"name": "Metadaten", "name": "Metadaten",
"description": "Steuerung der Metadaten-Einbindung in der Antwort. 'include' (Standard): Gibt Chart-Daten und Metadaten mit Preisstatistiken, Währungsinformationen, Y-Achsen-Vorschlägen und Zeitbereich zurück. 'only': Gibt nur Metadaten zurück ohne Chart-Daten zu verarbeiten (schnell, nützlich für dynamische Y-Achsen-Konfiguration). 'none': Gibt nur Chart-Daten ohne Metadaten zurück." "description": "Steuerung der Metadaten-Einbindung in der Antwort. 'include' (Standard): Gibt Chart-Daten und Metadaten mit Preisstatistiken, Währungsinformationen, Y-Achsen-Vorschlägen und Zeitbereich zurück. 'only': Gibt nur Metadaten zurück ohne Chart-Daten zu verarbeiten (schnell, nützlich für dynamische Y-Achsen-Konfiguration). 'none': Gibt nur Chart-Daten ohne Metadaten zurück."

View file

@ -1178,6 +1178,14 @@
"name": "Include Average", "name": "Include Average",
"description": "Include the daily average price in each data point for comparison." "description": "Include the daily average price in each data point for comparison."
}, },
"include_energy": {
"name": "Include Energy Price",
"description": "Include the raw energy/spot price (excluding taxes and fees) in each data point. This is the 'energy' field from the Tibber API, useful for feed-in/net metering calculations."
},
"include_tax": {
"name": "Include Tax",
"description": "Include the tax component of the price in each data point. This is the 'tax' field from the Tibber API, representing fees, taxes, and grid charges."
},
"level_filter": { "level_filter": {
"name": "Level Filter", "name": "Level Filter",
"description": "Filter intervals to include only specific Tibber price levels (very cheap/cheap/normal/expensive/very expensive). If not specified, all levels are included." "description": "Filter intervals to include only specific Tibber price levels (very cheap/cheap/normal/expensive/very expensive). If not specified, all levels are included."
@ -1226,6 +1234,14 @@
"name": "Average Field Name", "name": "Average Field Name",
"description": "Custom name for the average field in the output. Defaults to 'average' if not specified. Only used when include_average is enabled." "description": "Custom name for the average field in the output. Defaults to 'average' if not specified. Only used when include_average is enabled."
}, },
"energy_field": {
"name": "Energy Field Name",
"description": "Custom name for the energy price field in the output. Defaults to 'energy_price' if not specified. Only used when include_energy is enabled."
},
"tax_field": {
"name": "Tax Field Name",
"description": "Custom name for the tax field in the output. Defaults to 'tax' if not specified. Only used when include_tax is enabled."
},
"metadata": { "metadata": {
"name": "Metadata", "name": "Metadata",
"description": "Control metadata inclusion in the response. 'include' (default): Returns both chart data and metadata with price statistics, currency info, Y-axis suggestions, and time range. 'only': Returns only metadata without processing chart data (fast, useful for dynamic Y-axis configuration). 'none': Returns only chart data without metadata." "description": "Control metadata inclusion in the response. 'include' (default): Returns both chart data and metadata with price statistics, currency info, Y-axis suggestions, and time range. 'only': Returns only metadata without processing chart data (fast, useful for dynamic Y-axis configuration). 'none': Returns only chart data without metadata."

View file

@ -1174,6 +1174,14 @@
"name": "Inkluder gjennomsnitt", "name": "Inkluder gjennomsnitt",
"description": "Inkluder daglig gjennomsnittspris i hvert datapunkt for sammenligning." "description": "Inkluder daglig gjennomsnittspris i hvert datapunkt for sammenligning."
}, },
"include_energy": {
"name": "Inkluder energipris",
"description": "Inkluder rå energi-/spotpris (ekskludert skatter og avgifter) i hvert datapunkt. Dette er 'energy'-feltet fra Tibber-APIet, nyttig for innmatings-/nettomålingsberegninger."
},
"include_tax": {
"name": "Inkluder skatt",
"description": "Inkluder skatteandelen av prisen i hvert datapunkt. Dette er 'tax'-feltet fra Tibber-APIet, som representerer avgifter, skatter og nettleie."
},
"level_filter": { "level_filter": {
"name": "Prisnivåfilter", "name": "Prisnivåfilter",
"description": "Filtrer intervaller for å bare inkludere spesifikke Tibber-prisnivåer (veldig billig/billig/normal/dyr/veldig dyr). Hvis ikke angitt, inkluderes alle nivåer." "description": "Filtrer intervaller for å bare inkludere spesifikke Tibber-prisnivåer (veldig billig/billig/normal/dyr/veldig dyr). Hvis ikke angitt, inkluderes alle nivåer."
@ -1222,6 +1230,14 @@
"name": "Gjennomsnittsfelt-navn", "name": "Gjennomsnittsfelt-navn",
"description": "Tilpasset navn for gjennomsnittsfeltet i utdata. Standard er 'average'. Brukes bare når include_average er aktivert." "description": "Tilpasset navn for gjennomsnittsfeltet i utdata. Standard er 'average'. Brukes bare når include_average er aktivert."
}, },
"energy_field": {
"name": "Energiprisfelt-navn",
"description": "Tilpasset navn for energiprisfeltet i utdata. Standard er 'energy_price'. Brukes bare når include_energy er aktivert."
},
"tax_field": {
"name": "Skattefelt-navn",
"description": "Tilpasset navn for skattefeltet i utdata. Standard er 'tax'. Brukes bare når include_tax er aktivert."
},
"metadata": { "metadata": {
"name": "Metadata", "name": "Metadata",
"description": "Kontroller metadata-inkludering i svaret. 'include' (standard): Returnerer både diagramdata og metadata med prisstatistikk, valutainformasjon, Y-akse forslag og tidsperiode. 'only': Returnerer bare metadata uten å behandle diagramdata (raskt, nyttig for dynamisk Y-akse konfigurasjon). 'none': Returnerer bare diagramdata uten metadata." "description": "Kontroller metadata-inkludering i svaret. 'include' (standard): Returnerer både diagramdata og metadata med prisstatistikk, valutainformasjon, Y-akse forslag og tidsperiode. 'only': Returnerer bare metadata uten å behandle diagramdata (raskt, nyttig for dynamisk Y-akse konfigurasjon). 'none': Returnerer bare diagramdata uten metadata."

View file

@ -1174,6 +1174,14 @@
"name": "Gemiddelde opnemen", "name": "Gemiddelde opnemen",
"description": "Dagelijkse gemiddelde prijs opnemen in elk gegevenspunt ter vergelijking." "description": "Dagelijkse gemiddelde prijs opnemen in elk gegevenspunt ter vergelijking."
}, },
"include_energy": {
"name": "Energieprijs opnemen",
"description": "De kale energie-/spotprijs (exclusief belastingen en heffingen) opnemen in elk gegevenspunt. Dit is het 'energy'-veld uit de Tibber-API, handig voor teruglever-/salderingsberekeningen."
},
"include_tax": {
"name": "Belasting opnemen",
"description": "Het belastingdeel van de prijs opnemen in elk gegevenspunt. Dit is het 'tax'-veld uit de Tibber-API, dat heffingen, belastingen en netwerkkosten vertegenwoordigt."
},
"level_filter": { "level_filter": {
"name": "Prijsniveaufilter", "name": "Prijsniveaufilter",
"description": "Intervallen filteren om alleen specifieke Tibber-prijsniveaus op te nemen (zeer goedkoop/goedkoop/normaal/duur/zeer duur). Indien niet opgegeven, worden alle niveaus opgenomen." "description": "Intervallen filteren om alleen specifieke Tibber-prijsniveaus op te nemen (zeer goedkoop/goedkoop/normaal/duur/zeer duur). Indien niet opgegeven, worden alle niveaus opgenomen."
@ -1222,6 +1230,14 @@
"name": "Gemiddelde veld-naam", "name": "Gemiddelde veld-naam",
"description": "Aangepaste naam voor het gemiddelde veld in de uitvoer. Standaard is 'average'. Alleen gebruikt wanneer include_average is ingeschakeld." "description": "Aangepaste naam voor het gemiddelde veld in de uitvoer. Standaard is 'average'. Alleen gebruikt wanneer include_average is ingeschakeld."
}, },
"energy_field": {
"name": "Energieprijs veld-naam",
"description": "Aangepaste naam voor het energieprijsveld in de uitvoer. Standaard is 'energy_price'. Alleen gebruikt wanneer include_energy is ingeschakeld."
},
"tax_field": {
"name": "Belasting veld-naam",
"description": "Aangepaste naam voor het belastingveld in de uitvoer. Standaard is 'tax'. Alleen gebruikt wanneer include_tax is ingeschakeld."
},
"metadata": { "metadata": {
"name": "Metadata", "name": "Metadata",
"description": "Beheer metadata-opname in het antwoord. 'include' (standaard): Retourneert zowel grafiekdata als metadata met prijsstatistieken, valuta-info, Y-as suggesties en tijdsbereik. 'only': Retourneert alleen metadata zonder grafiekdata te verwerken (snel, handig voor dynamische Y-as configuratie). 'none': Retourneert alleen grafiekdata zonder metadata." "description": "Beheer metadata-opname in het antwoord. 'include' (standaard): Retourneert zowel grafiekdata als metadata met prijsstatistieken, valuta-info, Y-as suggesties en tijdsbereik. 'only': Retourneert alleen metadata zonder grafiekdata te verwerken (snel, handig voor dynamische Y-as configuratie). 'none': Retourneert alleen grafiekdata zonder metadata."

View file

@ -1178,6 +1178,14 @@
"name": "Inkludera medelvärde", "name": "Inkludera medelvärde",
"description": "Inkludera dagligt medelpris i varje datapunkt för jämförelse." "description": "Inkludera dagligt medelpris i varje datapunkt för jämförelse."
}, },
"include_energy": {
"name": "Inkludera energipris",
"description": "Inkludera rått energi-/spotpris (exklusive skatter och avgifter) i varje datapunkt. Detta är 'energy'-fältet från Tibber-API:et, användbart för inmatnings-/nettomätningsberäkningar."
},
"include_tax": {
"name": "Inkludera skatt",
"description": "Inkludera skattedelen av priset i varje datapunkt. Detta är 'tax'-fältet från Tibber-API:et, som representerar avgifter, skatter och nätavgifter."
},
"level_filter": { "level_filter": {
"name": "Nivåfilter", "name": "Nivåfilter",
"description": "Filtrera intervaller för att endast inkludera specifika Tibber-prisnivåer (mycket billigt/billigt/normalt/dyrt/mycket dyrt). Om inget anges inkluderas alla nivåer." "description": "Filtrera intervaller för att endast inkludera specifika Tibber-prisnivåer (mycket billigt/billigt/normalt/dyrt/mycket dyrt). Om inget anges inkluderas alla nivåer."
@ -1226,6 +1234,14 @@
"name": "Medelvärdesfältnamn", "name": "Medelvärdesfältnamn",
"description": "Anpassat namn för medelvärdesfältet i utdatan. Standard är 'average' om inget anges. Används endast när include_average är aktiverad." "description": "Anpassat namn för medelvärdesfältet i utdatan. Standard är 'average' om inget anges. Används endast när include_average är aktiverad."
}, },
"energy_field": {
"name": "Energiprisfältnamn",
"description": "Anpassat namn för energiprisfältet i utdatan. Standard är 'energy_price' om inget anges. Används endast när include_energy är aktiverad."
},
"tax_field": {
"name": "Skattefältnamn",
"description": "Anpassat namn för skattefältet i utdatan. Standard är 'tax' om inget anges. Används endast när include_tax är aktiverad."
},
"metadata": { "metadata": {
"name": "Metadata", "name": "Metadata",
"description": "Kontrollera inkludering av metadata i svaret. 'include' (standard): Returnerar både diagramdata och metadata med prisstatistik, valutainfo, Y-axelförslag och tidsintervall. 'only': Returnerar endast metadata utan att bearbeta diagramdata (snabbt, användbart för dynamisk Y-axelkonfiguration). 'none': Returnerar endast diagramdata utan metadata." "description": "Kontrollera inkludering av metadata i svaret. 'include' (standard): Returnerar både diagramdata och metadata med prisstatistik, valutainfo, Y-axelförslag och tidsintervall. 'only': Returnerar endast metadata utan att bearbeta diagramdata (snabbt, användbart för dynamisk Y-axelkonfiguration). 'none': Returnerar endast diagramdata utan metadata."