Hoppa till huvudinnehåll

Beräkna elnätsavgiften för en månad

Den här guiden räknar ut vad en kund betalar i elnätsavgift för en månad, utifrån en tariff och kundens förbrukning per timme. Du kan använda den för att visa kunder deras förväntade kostnad, för att jämföra tariffer eller för att se hur mycket det skulle löna sig att flytta förbrukning.

Det här behöver du​

  • En tariff, hämtad med GET /tariffs/{id}. Exemplet använder GN6KW ("Tidsindelad 6 kW, Max 63A"), som har en högre effektavgift vardagar 07:00–20:00 under vintern.
  • Förbrukning per timme för månaden: medeleffekten i kW för varje timme. Det är samma tal som antalet kWh som användes under timmen. Du får den från kundens elmätare, till exempel via Home Assistant eller mätarens eget API. Exemplet skapar påhittad data i stället.

Så fungerar det​

Koden går igenom tariffens tre avgiftsgrupper (se Avgifter och priskomponenter):

  1. Fast pris: årsavgiften delat med 12.
  2. Energipris: för varje komponent, de kWh som använts medan komponenten gäller × priset. Här ingår energiskatten.
  3. Effektpris:
    • För peak-komponenter: ta för varje dygn den högsta timmen medan komponenten gäller. Ta sedan medelvärdet av de högsta dygnen (oftast 3) och multiplicera med priset.
    • För dynamic-komponenter: hämta timpriserna för månaden och summera effekt × pris för varje timme.

En komponent räknas bara de timmar då den gäller: dess validPeriod ska omfatta datumet, och tid och dag ska stämma med dess recurringPeriods och kalendermönster. Se När en komponent gäller.

Kod​

Kodexemplen är på engelska.

monthly_cost.py
"""Calculate the grid fee for one month, from a tariff and hourly consumption.

Requires Python 3.9 or later. Run with: python monthly_cost.py
"""
import json
import urllib.parse
import urllib.request
from datetime import date, datetime, timedelta

BASE_URL = "https://api.uat.goteborgenergi.cloud/gridtariff/v0"
TARIFF_ID = "3e90a7bf-fd2a-4134-8c79-49611e7fffef" # GN6KW, 2026
MONTH = "2026-02"
# Name your application, so it can be told apart from other clients.
USER_AGENT = "grid-tariff-example/1.0"


def example_consumption():
"""Hourly consumption for the month.

Each entry is the average power in kW during the hour, which equals the
energy in kWh used that hour. Timestamps are in Swedish local time with
UTC offset, like in the API. Replace this with data from your meter.
"""
hours = []
for day in range(1, 29):
for hour in range(24):
evening = 17 <= hour < 21
hours.append({
"start": f"{MONTH}-{day:02d}T{hour:02d}:00:00+01:00",
"kW": 3 + (day % 5) * 0.4 if evening else 0.8,
})
return hours


def get_json(path):
request = urllib.request.Request(BASE_URL + path, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request) as response:
return json.load(response)


def in_calendar_pattern(day, reference, calendar_patterns):
pattern = next(p for p in calendar_patterns if p["reference"] == reference)
weekday = date.fromisoformat(day).isoweekday() # Monday = 1 ... Sunday = 7
return weekday in pattern["days"] or day in pattern["dates"]


def is_active(component, start, calendar_patterns):
"""Is the component active during the hour starting at `start`?"""
day, time = start[:10], start[11:19]
valid = component["validPeriod"]
if not valid["fromIncluding"] <= day < valid["toExcluding"]:
return False
if not component.get("recurringPeriods"):
return True

for recurring in component["recurringPeriods"]:
if recurring["frequency"] != "P1D":
raise ValueError(f"Unsupported frequency {recurring['frequency']}")
for period in recurring["activePeriods"]:
to = "24:00:00" if period["toExcluding"] == "00:00:00" else period["toExcluding"]
refs = period["calendarPatternReferences"]
if (
period["fromIncluding"] <= time < to
and any(in_calendar_pattern(day, r, calendar_patterns) for r in refs["include"])
and not any(in_calendar_pattern(day, r, calendar_patterns) for r in refs["exclude"])
):
return True
return False


def peak_power(component, consumption, calendar_patterns):
"""Average of the N highest daily peaks among the hours where the component is active."""
settings = component["peakIdentificationSettings"]
if settings["peakIdentificationPeriod"] != "P1D" or settings["peakDuration"] != "PT1H":
raise ValueError(f"Unsupported peak settings in {component['name']}")
daily_peaks = {}
for hour in consumption:
if is_active(component, hour["start"], calendar_patterns):
day = hour["start"][:10]
daily_peaks[day] = max(daily_peaks.get(day, 0), hour["kW"])
highest = sorted(daily_peaks.values(), reverse=True)
highest = highest[: settings["numberOfPeaksForAverageCalculation"]]
return sum(highest) / len(highest) if highest else 0


def hourly_prices(component, consumption):
start = consumption[0]["start"]
end = (datetime.fromisoformat(consumption[-1]["start"]) + timedelta(hours=1)).isoformat()
query = urllib.parse.urlencode({
"componentId": component["id"],
"fromIncluding": start,
"toExcluding": end,
})
prices = get_json(f"/prices?{query}")
return {datetime.fromisoformat(p["start"]): p for p in prices["actual"] + prices["preview"]}


def monthly_cost(tariff, calendar_patterns, consumption):
lines = []

def add(name, quantity, price, factor):
lines.append({
"name": name,
"quantity": quantity,
"exVat": factor * price["priceExVat"],
"incVat": factor * price["priceIncVat"],
})

month_start = consumption[0]["start"][:7] + "-01"

for c in (tariff.get("fixedPrice") or {}).get("components", []):
valid = c["validPeriod"]
if not valid["fromIncluding"] <= month_start < valid["toExcluding"]:
continue
per_month = {"P1Y": 1 / 12, "P1M": 1}.get(c["pricedPeriod"])
if per_month is None:
raise ValueError(f"Unsupported pricedPeriod {c['pricedPeriod']}")
add(c["name"], "1 month" if per_month == 1 else "1/12 year", c["price"], per_month)

for c in (tariff.get("energyPrice") or {}).get("components", []):
if c["type"] != "fixed":
raise ValueError(f"Unsupported energy component type {c['type']}")
kwh = sum(h["kW"] for h in consumption if is_active(c, h["start"], calendar_patterns))
if kwh > 0:
add(c["name"], f"{kwh:.1f} kWh", c["price"], kwh)

for c in (tariff.get("powerPrice") or {}).get("components", []):
if c["type"] == "peak":
if c["unit"] != "kW":
raise ValueError(f"Unsupported unit {c['unit']} in {c['name']}")
kw = peak_power(c, consumption, calendar_patterns)
if kw > 0:
add(c["name"], f"{kw:.2f} kW", c["price"], kw)
elif c["type"] == "dynamic":
active = [h for h in consumption if is_active(c, h["start"], calendar_patterns)]
if not active:
continue
prices = hourly_prices(c, active)
ex_vat = inc_vat = 0
for hour in active:
price = prices.get(datetime.fromisoformat(hour["start"]))
if price is None:
raise ValueError(f"No price for {hour['start']}")
ex_vat += hour["kW"] * price["priceExVat"]
inc_vat += hour["kW"] * price["priceIncVat"]
lines.append({"name": c["name"], "quantity": f"{len(active)} hours",
"exVat": ex_vat, "incVat": inc_vat})
else:
raise ValueError(f"Unsupported power component type {c['type']}")
return lines


response = get_json(f"/tariffs/{TARIFF_ID}")
tariff = response["tariff"]
lines = monthly_cost(tariff, response["calendarPatterns"], example_consumption())

print(f"{tariff['name']} ({tariff['product']}), {MONTH}\n")
for line in lines:
print(f"{line['name']:<32} {line['quantity']:>12} {line['incVat']:>10.2f} SEK")
print(f"{'Total incl. VAT':<45} {sum(l['incVat'] for l in lines):>10.2f} SEK")
print(f"{'Total excl. VAT':<45} {sum(l['exVat'] for l in lines):>10.2f} SEK")

Exempel på utskrift:

Tidsindelad 6 kW, Max 63A (GN6KW), 2026-02

Fast avgift 1/12 year 200.00 SEK
Elöverföringsavgift 873.6 kWh 56.78 SEK
Energiskatt 873.6 kWh 393.12 SEK
Tidsindelad, effektavgift hög 4.60 kW 621.00 SEK
Tidsindelad, effektavgift låg 4.60 kW 0.00 SEK
Total incl. VAT 1270.90 SEK
Total excl. VAT 1016.72 SEK

Begränsningar​

Exemplet hanterar de komponenter som Göteborg Energis tariffer för hushåll och mindre företag använder. Det stoppar med ett fel om det stöter på något som det inte hanterar, i stället för att ge ett felaktigt svar:

  • Komponenter som kräver annan data än effekt per timme, som reaktiv effekt (kVAr) eller en abonnerad effekt.
  • Effekttoppar som mäts över andra perioder än en timme per dygn.

Det förutsätter också att alla tidpunkter för förbrukningen är i svensk lokal tid och att hela månaden använder samma version av tariffen.

tips

När du skickar ett tidsintervall till /prices ska du inte ta med bråkdelar av sekunder: 2026-02-01T00:00:00+01:00 och 2026-01-31T23:00:00Z fungerar, men 2026-01-31T23:00:00.000Z avvisas. I JavaScript lägger toISOString() alltid till .000, så ta bort det.