Stromkosten mit Tibber simulieren

Mit ** gekennzeichnete Links auf dieser Seite sind Affiliatelinks.

Stromkosten mit Tibber simulieren
Stromkosten mit Tibber simulieren
  • 13.10.2023
  • Visualisierung
  • Datenbanken

Dynamische Stromtarife sind eine starke Idee: Man übermittelt regelmäßig den eigenen Zählerstand und bekommt dann die Energie zum Börsenkurs (EPEX). Dieser lässt sich für die Zukunft ermitteln und somit kann man seine Verbräuche an den Preis anpassen. Damit verhält man sich automatisch “netzdienlich” und spart unterm Strich auch noch - Win/Win, oder? Ich möchte einmal errechnen, ob ich trotz günstigem Stromtarif mit fixem Preis noch etwas sparen könnte. Dafür importiere ich die Börsenpreise des aktuellen Jahres in meine InfluxDB und kann dann meine Verbräuche dagegen legen.

Info: Natürlich ist in der Vergangenheit nicht berücksichtigt, wann der Strom gerade möglichst günstig war. Es ergibt also nur bedingt Sinn die historischen Werte zu vergleichen.

Video

Hausbau-Kurs

Daten in InfluxDB schreiben

Über den ioBroker kann ich die Daten relativ einfach mit einem kurzen JavaScript in die InfluxDB importieren:

// v0.1
const axios = require('axios').default;

const influxDbInstance = 'influxdb.0';
const token = 'cIpi....';
const measurement = 'energy-stats';

const startDate = new Date('2023-01-01');
const interval = 7;
const endDate = new Date('2023-10-12');

async function start() {
    const influxDbInstanceConfig = await getObjectAsync(`system.adapter.${influxDbInstance}`);

    const protocol = influxDbInstanceConfig.native.protocol;
    const host = influxDbInstanceConfig.native.host;
    const port = influxDbInstanceConfig.native.port;
    const org = influxDbInstanceConfig.native.organization;
    const bucket = influxDbInstanceConfig.native.dbname;

    while (startDate.getTime() < endDate.getTime()) {
        const endInterval = new Date(startDate.getTime() - 1);
        endInterval.setUTCDate(endInterval.getUTCDate() + interval);

        try {
            console.log(`Start from ${startDate.toISOString()} to ${endInterval.toISOString()}`);

            const result = await axios.get(`https://api.energy-charts.info/price_spot_market?bzn=DE-LU&start=${startDate.toISOString()}&end=${endInterval.toISOString()}`);

            const timeStamps = result.data['xAxisValues (Unix timestamp)'];
            const values = result.data['Day Ahead Auction (DE-LU) (EUR/MWh)'];

            const data = [];

            for (let i = 0; i < timeStamps.length; i++) {
                if (!isNaN(values[i])) {
                    // console.log(`Found ${new Date(timeStamps[i] * 1000).toISOString()}: ${values[i] / 1000} €/kWh`);
                    data.push(`${measurement} priceInEpex=${(values[i] / 1000).toFixed(4)} ${timeStamps[i]}000000000`);
                }
            }

            await axios.post(`${protocol}://${host}:${port}/api/v2/write?bucket=${bucket}&org=${org}`, data.join('\n'), {
                headers: {
                    'Content-Type': 'text/plain',
                    'Authorization': `Token ${token}`
                }
            });

        } catch (err) {
            console.error(err);
        }

        startDate.setUTCDate(startDate.getUTCDate() + interval);
    }

    console.log('Finished');
}
start();

Dashboard

from(bucket: "smarthome")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "energy-stats")
  |> filter(fn: (r) => r._field == "importedWh" or r._field == "priceIn" or r._field == "priceInEpex")
  |> aggregateWindow(every: 1h, fn: last, createEmpty: true, timeSrc: "_start")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> difference(columns: ["importedWh"])
  |> map(fn: (r) => ({r with _priceInTotal: r.importedWh / 1000.0 * r.priceIn}))
  |> map(fn: (r) => ({r with _priceInTotalEpex: (r.importedWh / 1000.0 * r.priceInEpex * 1.19) + (r.importedWh / 1000.0 * 0.1751)}))
  |> drop(columns: ["importedWh", "priceIn", "priceInEpex"])

Task

Das errechnen der Daten auf den einzelnen Zeilen und Millionen Einträgen dauert ggf. extrem lange. Also werden auch diese Werte mit einem Task heruntergebrochen. Wie genau das geht, habe ich in einem anderen Beitrag schon ausführlich erklärt.

Einmalig Daten errechnen:

import "date"

fullHourTime = date.truncate(t: now(), unit: 1h)

from(bucket: "smarthome")
  |> range(start: 2023-01-01T00:00:00.000Z, stop: fullHourTime)
  |> filter(fn: (r) => r._field == "importedWh" or r._field == "priceInEpex")
  |> aggregateWindow(every: 1h, fn: last, createEmpty: true, timeSrc: "_start")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> filter(fn: (r) => r.importedWh > 0)
  |> difference(columns: ["importedWh"])
  |> map(fn: (r) => ({r with _value: r.importedWh / 1000.0 * r.priceInEpex}))
  |> to(
    bucket: "smarthome-history",
    fieldFn: (r) => ({ "importedEuroEpex": r._value })
  )

Task für regelmäßige Ausführung (jeden Tag 1d):

import "date"

fullHourTime = date.truncate(t: now(), unit: 1h)
startTime = date.sub(from: fullHourTime, d: 1d)

from(bucket: "smarthome")
    |> range(start: startTime, stop: fullHourTime)
    |> filter(fn: (r) => r._measurement == "energy-stats")
    |> filter(fn: (r) => r._field == "importedWh" or r._field == "priceInEpex")
    |> aggregateWindow(every: 1h, fn: last, createEmpty: true, timeSrc: "_start")
    |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> filter(fn: (r) => r.importedWh > 0)
    |> difference(columns: ["importedWh"])
    |> map(fn: (r) => ({r with _value: r.importedWh / 1000.0 * r.priceInEpex}))
    |> to(
      bucket: "smarthome-history",
      fieldFn: (r) => ({ "importedEuroEpex": r._value })
    )

Damit kann dann recht einfach eine Tabelle erstellt werden:

import "timezone"
import "date"

option location = timezone.location(name: "Europe/Berlin")

from(bucket: "smarthome-history")
  |> range(start: date.truncate(t: today(), unit: 1y))
  |> filter(fn: (r) => r._measurement == "energy-stats")
  |> filter(fn: (r) => r._field == "importedWh" or r._field == "importedEuro" or r._field == "importedEuroEpex")
  |> aggregateWindow(every: 1mo, fn: sum, createEmpty: true, timeSrc: "_start")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({r with importedEuroEpex: (r.importedEuroEpex * 1.19) + (r.importedWh / 1000.0 * 0.1751)}))
  |> sort(columns: ["_time"])
Du willst mehr?

Smart-Home-Trainings von A-Z

Steig' noch tiefer in die Themen ein und meistere Deine Projekte! Über 15.000 Teilnehmer konnten sich schon von der Qualität der Online-Kurse überzeugen.