feat(api): add energy and tax fields to Tibber GraphQL queries

Request `energy` and `tax` fields alongside `total` in both
quarter-hourly price queries. These represent the raw spot price and
the tax/fee component that together make up the total consumer price.

Updated hourly aggregation in formatters.py to carry energy/tax
values through to aggregated output.

Impact: Enables downstream consumers (sensors, services) to expose
price composition data. Useful for solar feed-in compensation and
net metering (saldering) calculations where the raw energy price
is needed separately from taxes.
This commit is contained in:
Julian Pawlowski 2026-04-09 18:27:21 +00:00
parent e1da4cfa89
commit f5dcf04aab
2 changed files with 26 additions and 4 deletions

View file

@ -230,12 +230,12 @@ class TibberPricesApiClient:
priceInfoRange(resolution:QUARTER_HOURLY, first:192, after: "{cursor}") {{ priceInfoRange(resolution:QUARTER_HOURLY, first:192, after: "{cursor}") {{
pageInfo{{ count }} pageInfo{{ count }}
edges{{node{{ edges{{node{{
startsAt total level startsAt total energy tax level
}}}} }}}}
}} }}
priceInfo(resolution:QUARTER_HOURLY) {{ priceInfo(resolution:QUARTER_HOURLY) {{
today{{startsAt total level}} today{{startsAt total energy tax level}}
tomorrow{{startsAt total level}} tomorrow{{startsAt total energy tax level}}
}} }}
}} }}
}} }}
@ -500,7 +500,7 @@ class TibberPricesApiClient:
edges{{ edges{{
cursor cursor
node{{ node{{
startsAt total level startsAt total energy tax level
}} }}
}} }}
}} }}

View file

@ -103,6 +103,8 @@ def aggregate_to_hourly( # noqa: PLR0912
# Collect 5-interval rolling window: -2, -1, 0, +1, +2 # Collect 5-interval rolling window: -2, -1, 0, +1, +2
window_prices: list[float] = [] window_prices: list[float] = []
window_energy_prices: list[float] = []
window_tax_prices: list[float] = []
window_intervals: list[dict] = [] window_intervals: list[dict] = []
for offset in range(-2, 3): # -2, -1, 0, +1, +2 for offset in range(-2, 3): # -2, -1, 0, +1, +2
@ -113,6 +115,12 @@ def aggregate_to_hourly( # noqa: PLR0912
if price is not None: if price is not None:
window_prices.append(price) window_prices.append(price)
window_intervals.append(target_interval) window_intervals.append(target_interval)
energy = target_interval.get("energy")
tax = target_interval.get("tax")
if energy is not None:
window_energy_prices.append(energy)
if tax is not None:
window_tax_prices.append(tax)
# Calculate aggregated price based on user preference # Calculate aggregated price based on user preference
if window_prices: if window_prices:
@ -127,6 +135,20 @@ def aggregate_to_hourly( # noqa: PLR0912
"total": aggregated_price, "total": aggregated_price,
} }
# Add aggregated energy and tax prices
if window_energy_prices:
aggregated_energy = (
calculate_median(window_energy_prices) if use_median else calculate_mean(window_energy_prices)
)
if aggregated_energy is not None:
data_point["energy"] = aggregated_energy
if window_tax_prices:
aggregated_tax = (
calculate_median(window_tax_prices) if use_median else calculate_mean(window_tax_prices)
)
if aggregated_tax is not None:
data_point["tax"] = aggregated_tax
# Add aggregated level # Add aggregated level
if window_intervals: if window_intervals:
aggregated_level = aggregate_level_data(window_intervals) aggregated_level = aggregate_level_data(window_intervals)