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änderGN6KW("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):
- Fast pris: årsavgiften delat med 12.
- Energipris: för varje komponent, de kWh som använts medan komponenten gäller × priset. Här ingår energiskatten.
- 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.
- För
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.
- Python
- JavaScript
- C#
"""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")
// Calculate the grid fee for one month, from a tariff and hourly consumption.
// Requires Node.js 20 or later. Run with: node monthly-cost.mjs
const BASE_URL = 'https://api.uat.goteborgenergi.cloud/gridtariff/v0';
const TARIFF_ID = '3e90a7bf-fd2a-4134-8c79-49611e7fffef'; // GN6KW, 2026
const MONTH = '2026-02';
// 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.
function exampleConsumption() {
const hours = [];
for (let day = 1; day <= 28; day++) {
for (let hour = 0; hour < 24; hour++) {
const evening = hour >= 17 && hour < 21;
const date = `${MONTH}-${String(day).padStart(2, '0')}`;
hours.push({
start: `${date}T${String(hour).padStart(2, '0')}:00:00+01:00`,
kW: evening ? 3 + (day % 5) * 0.4 : 0.8,
});
}
}
return hours;
}
async function getJson(path) {
const response = await fetch(BASE_URL + path);
if (!response.ok) {
throw new Error(`${path}: ${response.status} ${await response.text()}`);
}
return response.json();
}
// ISO weekday: Monday = 1 ... Sunday = 7
function isoWeekday(date) {
return new Date(`${date}T00:00:00Z`).getUTCDay() || 7;
}
function inCalendarPattern(date, reference, calendarPatterns) {
const pattern = calendarPatterns.find((p) => p.reference === reference);
return pattern.days.includes(isoWeekday(date)) || pattern.dates.includes(date);
}
// Is the component active during the hour starting at `start`?
function isActive(component, start, calendarPatterns) {
const date = start.slice(0, 10);
const time = start.slice(11, 19);
const {fromIncluding, toExcluding} = component.validPeriod;
if (date < fromIncluding || date >= toExcluding) return false;
if (!component.recurringPeriods) return true;
return component.recurringPeriods.some((recurring) => {
if (recurring.frequency !== 'P1D') {
throw new Error(`Unsupported frequency ${recurring.frequency}`);
}
return recurring.activePeriods.some((period) => {
const to = period.toExcluding === '00:00:00' ? '24:00:00' : period.toExcluding;
const {include, exclude} = period.calendarPatternReferences;
return (
time >= period.fromIncluding &&
time < to &&
include.some((ref) => inCalendarPattern(date, ref, calendarPatterns)) &&
!exclude.some((ref) => inCalendarPattern(date, ref, calendarPatterns))
);
});
});
}
// Average of the N highest daily peaks among the hours where the component is active
function peakPower(component, consumption, calendarPatterns) {
const settings = component.peakIdentificationSettings;
if (settings.peakIdentificationPeriod !== 'P1D' || settings.peakDuration !== 'PT1H') {
throw new Error(`Unsupported peak settings in ${component.name}`);
}
const dailyPeaks = new Map();
for (const hour of consumption) {
if (!isActive(component, hour.start, calendarPatterns)) continue;
const date = hour.start.slice(0, 10);
dailyPeaks.set(date, Math.max(dailyPeaks.get(date) ?? 0, hour.kW));
}
const highest = [...dailyPeaks.values()]
.sort((a, b) => b - a)
.slice(0, settings.numberOfPeaksForAverageCalculation);
return highest.length ? highest.reduce((sum, kW) => sum + kW, 0) / highest.length : 0;
}
async function hourlyPrices(component, consumption) {
const from = encodeURIComponent(consumption[0].start);
const lastStart = new Date(consumption.at(-1).start);
// The API doesn't accept fractional seconds, so drop the ".000" from toISOString()
const to = encodeURIComponent(
new Date(lastStart.getTime() + 3600_000).toISOString().replace('.000Z', 'Z'),
);
const prices = await getJson(
`/prices?componentId=${component.id}&fromIncluding=${from}&toExcluding=${to}`,
);
return new Map(
[...prices.actual, ...prices.preview].map((p) => [Date.parse(p.start), p]),
);
}
async function monthlyCost(tariff, calendarPatterns, consumption) {
const lines = [];
const add = (name, quantity, price, factor) =>
lines.push({
name,
quantity,
exVat: factor * price.priceExVat,
incVat: factor * price.priceIncVat,
});
const month = consumption[0].start.slice(0, 7);
const monthStart = `${month}-01`;
for (const c of tariff.fixedPrice?.components ?? []) {
if (c.validPeriod.fromIncluding > monthStart || c.validPeriod.toExcluding <= monthStart) continue;
const perMonth = {P1Y: 1 / 12, P1M: 1}[c.pricedPeriod];
if (perMonth === undefined) throw new Error(`Unsupported pricedPeriod ${c.pricedPeriod}`);
add(c.name, perMonth === 1 ? '1 month' : '1/12 year', c.price, perMonth);
}
for (const c of tariff.energyPrice?.components ?? []) {
if (c.type !== 'fixed') throw new Error(`Unsupported energy component type ${c.type}`);
const kWh = consumption
.filter((hour) => isActive(c, hour.start, calendarPatterns))
.reduce((sum, hour) => sum + hour.kW, 0);
if (kWh > 0) add(c.name, `${kWh.toFixed(1)} kWh`, c.price, kWh);
}
for (const c of tariff.powerPrice?.components ?? []) {
if (c.type === 'peak') {
if (c.unit !== 'kW') throw new Error(`Unsupported unit ${c.unit} in ${c.name}`);
const kW = peakPower(c, consumption, calendarPatterns);
if (kW > 0) add(c.name, `${kW.toFixed(2)} kW`, c.price, kW);
} else if (c.type === 'dynamic') {
const active = consumption.filter((hour) => isActive(c, hour.start, calendarPatterns));
if (!active.length) continue;
const prices = await hourlyPrices(c, active);
let exVat = 0;
let incVat = 0;
for (const hour of active) {
const price = prices.get(Date.parse(hour.start));
if (!price) throw new Error(`No price for ${hour.start}`);
exVat += hour.kW * price.priceExVat;
incVat += hour.kW * price.priceIncVat;
}
lines.push({name: c.name, quantity: `${active.length} hours`, exVat, incVat});
} else {
throw new Error(`Unsupported power component type ${c.type}`);
}
}
return lines;
}
const {tariff, calendarPatterns} = await getJson(`/tariffs/${TARIFF_ID}`);
const lines = await monthlyCost(tariff, calendarPatterns, exampleConsumption());
console.log(`${tariff.name} (${tariff.product}), ${MONTH}\n`);
for (const line of lines) {
console.log(
`${line.name.padEnd(32)} ${line.quantity.padStart(12)} ${line.incVat.toFixed(2).padStart(10)} SEK`,
);
}
const total = lines.reduce((sum, line) => sum + line.incVat, 0);
const totalExVat = lines.reduce((sum, line) => sum + line.exVat, 0);
console.log(`${'Total incl. VAT'.padEnd(45)} ${total.toFixed(2).padStart(10)} SEK`);
console.log(`${'Total excl. VAT'.padEnd(45)} ${totalExVat.toFixed(2).padStart(10)} SEK`);
// Calculate the grid fee for one month, from a tariff and hourly consumption.
// Requires .NET 8 or later. Put this in Program.cs of a console project
// (dotnet new console) and run with: dotnet run
using System.Globalization;
using System.Net.Http.Json;
const string BaseUrl = "https://api.uat.goteborgenergi.cloud/gridtariff/v0";
const string TariffId = "3e90a7bf-fd2a-4134-8c79-49611e7fffef"; // GN6KW, 2026
const string Month = "2026-02";
using var http = new HttpClient();
var invariant = CultureInfo.InvariantCulture;
var response = await http.GetFromJsonAsync<TariffResponse>($"{BaseUrl}/tariffs/{TariffId}")
?? throw new InvalidOperationException("Empty response");
var tariff = response.Tariff;
var patterns = response.CalendarPatterns;
var consumption = ExampleConsumption();
var lines = new List<Line>();
var monthStart = Month + "-01";
foreach (var c in tariff.FixedPrice?.Components ?? [])
{
if (Before(monthStart, c.ValidPeriod.FromIncluding) || !Before(monthStart, c.ValidPeriod.ToExcluding)) continue;
var perMonth = c.PricedPeriod switch
{
"P1Y" => 1m / 12,
"P1M" => 1m,
_ => throw new NotSupportedException($"Unsupported pricedPeriod {c.PricedPeriod}"),
};
lines.Add(Charge(c, perMonth == 1 ? "1 month" : "1/12 year", perMonth));
}
foreach (var c in tariff.EnergyPrice?.Components ?? [])
{
if (c.Type != "fixed") throw new NotSupportedException($"Unsupported energy component type {c.Type}");
var kWh = consumption.Where(h => IsActive(c, h.Start)).Sum(h => h.KW);
if (kWh > 0) lines.Add(Charge(c, $"{kWh:F1} kWh", kWh));
}
foreach (var c in tariff.PowerPrice?.Components ?? [])
{
if (c.Type == "peak")
{
if (c.Unit != "kW") throw new NotSupportedException($"Unsupported unit {c.Unit} in {c.Name}");
var kW = PeakPower(c);
if (kW > 0) lines.Add(Charge(c, $"{kW:F2} kW", kW));
}
else if (c.Type == "dynamic")
{
var active = consumption.Where(h => IsActive(c, h.Start)).ToList();
if (active.Count == 0) continue;
var prices = await HourlyPrices(c, active);
decimal exVat = 0, incVat = 0;
foreach (var hour in active)
{
if (!prices.TryGetValue(hour.Start, out var price))
throw new InvalidOperationException($"No price for {hour.Start}");
exVat += hour.KW * price.PriceExVat;
incVat += hour.KW * price.PriceIncVat;
}
lines.Add(new Line(c.Name, $"{active.Count} hours", exVat, incVat));
}
else
{
throw new NotSupportedException($"Unsupported power component type {c.Type}");
}
}
Console.WriteLine($"{tariff.Name} ({tariff.Product}), {Month}\n");
foreach (var line in lines)
Console.WriteLine($"{line.Name,-32} {line.Quantity,12} {line.IncVat,10:F2} SEK");
Console.WriteLine($"{"Total incl. VAT",-45} {lines.Sum(l => l.IncVat),10:F2} SEK");
Console.WriteLine($"{"Total excl. VAT",-45} {lines.Sum(l => l.ExVat),10:F2} SEK");
// Hourly consumption for the month. KW is the average power 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.
List<ConsumptionHour> ExampleConsumption()
{
var hours = new List<ConsumptionHour>();
for (var day = 1; day <= 28; day++)
{
for (var hour = 0; hour < 24; hour++)
{
var evening = hour >= 17 && hour < 21;
hours.Add(new ConsumptionHour(
DateTimeOffset.Parse($"{Month}-{day:D2}T{hour:D2}:00:00+01:00", invariant),
evening ? 3 + (day % 5) * 0.4m : 0.8m));
}
}
return hours;
}
static bool Before(string a, string b) => string.CompareOrdinal(a, b) < 0;
static Line Charge(Component c, string quantity, decimal factor) =>
new(c.Name, quantity, factor * c.Price!.PriceExVat, factor * c.Price.PriceIncVat);
bool InCalendarPattern(string date, string reference)
{
var pattern = patterns.First(p => p.Reference == reference);
var weekday = (int)DateOnly.ParseExact(date, "yyyy-MM-dd", invariant).DayOfWeek;
if (weekday == 0) weekday = 7; // Monday = 1 ... Sunday = 7
return pattern.Days.Contains(weekday) || pattern.Dates.Contains(date);
}
// Is the component active during the hour starting at `start`?
bool IsActive(Component c, DateTimeOffset start)
{
var date = start.ToString("yyyy-MM-dd", invariant);
var time = start.ToString("HH:mm:ss", invariant);
if (Before(date, c.ValidPeriod.FromIncluding) || !Before(date, c.ValidPeriod.ToExcluding)) return false;
if (c.RecurringPeriods is null) return true;
return c.RecurringPeriods.Any(recurring =>
{
if (recurring.Frequency != "P1D")
throw new NotSupportedException($"Unsupported frequency {recurring.Frequency}");
return recurring.ActivePeriods.Any(period =>
{
var to = period.ToExcluding == "00:00:00" ? "24:00:00" : period.ToExcluding;
var refs = period.CalendarPatternReferences;
return !Before(time, period.FromIncluding)
&& Before(time, to)
&& refs.Include.Any(r => InCalendarPattern(date, r))
&& !refs.Exclude.Any(r => InCalendarPattern(date, r));
});
});
}
// Average of the N highest daily peaks among the hours where the component is active
decimal PeakPower(Component c)
{
var settings = c.PeakIdentificationSettings!;
if (settings.PeakIdentificationPeriod != "P1D" || settings.PeakDuration != "PT1H")
throw new NotSupportedException($"Unsupported peak settings in {c.Name}");
var highest = consumption
.Where(h => IsActive(c, h.Start))
.GroupBy(h => h.Start.ToString("yyyy-MM-dd", invariant))
.Select(day => day.Max(h => h.KW))
.OrderByDescending(kW => kW)
.Take(settings.NumberOfPeaksForAverageCalculation)
.ToList();
return highest.Count > 0 ? highest.Average() : 0;
}
async Task<Dictionary<DateTimeOffset, HourlyPrice>> HourlyPrices(Component c, List<ConsumptionHour> hours)
{
var from = Uri.EscapeDataString(hours[0].Start.ToString("yyyy-MM-dd'T'HH:mm:sszzz", invariant));
var to = Uri.EscapeDataString(hours[^1].Start.AddHours(1).ToString("yyyy-MM-dd'T'HH:mm:sszzz", invariant));
var prices = await http.GetFromJsonAsync<PricesResponse>(
$"{BaseUrl}/prices?componentId={c.Id}&fromIncluding={from}&toExcluding={to}")
?? throw new InvalidOperationException("Empty response");
return prices.Actual.Concat(prices.Preview).ToDictionary(p => p.Start);
}
record ConsumptionHour(DateTimeOffset Start, decimal KW);
record Line(string Name, string Quantity, decimal ExVat, decimal IncVat);
record TariffResponse(Tariff Tariff, List<CalendarPattern> CalendarPatterns);
record Tariff(string Name, string Product, PriceGroup? FixedPrice, PriceGroup? EnergyPrice, PriceGroup? PowerPrice);
record PriceGroup(List<Component> Components);
record Component(
string Id, string Name, string Type, Period ValidPeriod, Price? Price, string? Unit,
string? PricedPeriod, PeakSettings? PeakIdentificationSettings, List<RecurringPeriod>? RecurringPeriods);
record Period(string FromIncluding, string ToExcluding);
record Price(decimal PriceExVat, decimal PriceIncVat);
record PeakSettings(string PeakIdentificationPeriod, string PeakDuration, int NumberOfPeaksForAverageCalculation);
record RecurringPeriod(string Frequency, List<ActivePeriod> ActivePeriods);
record ActivePeriod(string FromIncluding, string ToExcluding, PatternReferences CalendarPatternReferences);
record PatternReferences(List<string> Include, List<string> Exclude);
record CalendarPattern(string Reference, List<int> Days, List<string> Dates);
record PricesResponse(string Currency, string Unit, List<HourlyPrice> Actual, List<HourlyPrice> Preview);
record HourlyPrice(DateTimeOffset Start, DateTimeOffset End, decimal PriceExVat, decimal PriceIncVat);
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.
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.