In diesem Beitrag zeige ich dir, wie du die Verbrauchsdaten eines Shelly-Geräts auf einem ESP32-C3 Mikrocontroller speichern und anschließend bequem per REST-Schnittstelle als CSV-Datei exportieren kannst.
Im vorangegangenen Beitrag „Shelly 1PM + ESP32: Daten speichern & zeitgesteuert senden“ habe ich bereits erläutert, wie sich die Messwerte lokal sichern und automatisch an ThingSpeak übertragen lassen. Nun erweitern wir das Projekt um eine einfache und praktische Möglichkeit zur lokalen Datenweitergabe per Webbrowser.
Ein Leser hat daraufhin die sinnvolle Idee eingebracht, die gespeicherten Daten zusätzlich als CSV-Datei verfügbar zu machen – zum Beispiel für die Weiterverarbeitung in Excel oder zur lokalen Archivierung.
In diesem Beitrag zeige ich dir, wie du genau das umsetzen kannst: Der ESP32 stellt über einen kleinen integrierten Webserver die erfassten Messwerte als CSV-Datei zum Download bereit. So kannst du bequem über deinen Browser auf die Daten zugreifen – ganz ohne zusätzliche Tools oder Cloud-Zwang.
Inhaltsverzeichnis
- Rückblick – Aufzeichnen der Daten auf dem Mikrocontroller
- Auslesen der JSON-Daten vom Mikrocontroller
- Schaltung am ESP32-C3
- Programmieren einer REST-Schnittstelle in MicroPython
- Schritt 1 – initialisieren des Servers
- Schritt 2 – Anfragen akzeptieren und senden der Daten
- parse_http_request(request) – HTTP-Anfrage analysieren
- get_format_from_path(path) – Ausgabeformat bestimmen (JSON oder CSV)
- respond_with_csv(client, data) – CSV-Antwort an den Client senden
- respond_with_json(client, data) – JSON-Daten senden
- handle_rest_request(server) – REST-Endpunkt zentral verarbeiten
- Quellcode zum Abrufen der Daten vom ESP32 via REST-Schnittstelle
- Fazit
Rückblick – Aufzeichnen der Daten auf dem Mikrocontroller
Die vom Shelly gelieferten Messdaten (Spannung, Strom, Leistungsaufnahme) werden über die integrierte REST-Schnittstelle regelmäßig vom ESP32-C3 Mikrocontroller abgefragt und lokal im Flash-Speicher gesichert. Die Speicherung erfolgt im JSON-Format, strukturiert nach Datum und Zeitstempel, sodass die Daten später einfach verarbeitet oder exportiert werden können – auch bei fehlender Internetverbindung.
👉 Tipp: Eine ausführliche Schritt-für-Schritt-Anleitung zum Aufbau, zur Programmierung und zur Datenspeicherung findest du in meinem Beitrag: „Shelly 1PM + ESP32: Daten speichern & zeitgesteuert senden“.
Datenarchiv für 5 Tage – Speicher automatisch verwalten
Um eine längere Verfügbarkeit der Daten zu gewährleisten, werden die erfassten Werte nun nicht mehr direkt nach dem Upload an ThingSpeak gelöscht. Stattdessen bleiben sie für maximal 5 Tage auf dem Mikrocontroller gespeichert. So kannst du die Daten zusätzlich als CSV-Datei im Browser abrufen oder lokal sichern – auch nachträglich.
Außerdem wurde das Messintervall von 5 auf 15 Minuten angepasst, um den Speicherverbrauch pro Tag zu reduzieren. Bei einem Intervall von 15 Minuten entstehen ca. 96 Datensätze pro Tag, was selbst bei mehreren Tagen gut in den internen 4 MB Flash-Speicher passt.
Die Datensätze werden nun täglich geprüft:
Ist ein Datumseintrag älter als 5 Tage, wird dieser automatisch gelöscht.
So bleibt der Speicherplatz effizient verwaltet, und du erhältst trotzdem eine kurze Historie deiner Energiedaten – ohne manuelles Eingreifen.
Auslesen der JSON-Daten vom Mikrocontroller
Wie bereits erwähnt, werden die erfassten Messdaten auf dem Mikrocontroller im JSON-Format gespeichert. Es gibt mehrere Möglichkeiten, diese Daten auszulesen.
Die einfachste Methode besteht darin, die Datei direkt über die Entwicklungsumgebung Thonny vom Mikrocontroller auf den PC zu kopieren. Dazu ist allerdings eine aktive Verbindung über ein USB-Datenkabel erforderlich – was in manchen Szenarien nicht möglich oder praktisch ist.
Eine clevere Alternative bietet eine einfache REST-Schnittstelle, die wir mit wenigen Zeilen Code selbst einrichten können. Das ist einer der vielen Vorteile von MicroPython: Es ermöglicht uns, schnell einen kleinen Webserver zu realisieren, über den wir die Daten bequem per Browser abrufen können – ganz ohne physische Verbindung.
Schaltung am ESP32-C3
Im Grunde benötigt man keine zusätzliche Schaltung denn es spielt sich alles ohne zusätzliche Komponenten ab. Jedoch habe ich drei LEDs angeschlossen um den aktuellen Zustand zu visualisieren.
- Eine blaue LED für die aktive WiFi-Verbindung.
- Eine rote LED für eventuelle Fehler.
- Eine grüne LED als indikator für abruf der Daten vom Shelly.


Programmieren einer REST-Schnittstelle in MicroPython
In diesem Abschnitt programmieren wir die REST-Schnittstelle direkt auf dem ESP32 in MicroPython. Ziel ist es, zwei zentrale UseCases abzubilden:
- Download der Messdaten als CSV-Datei – zur einfachen Weiterverarbeitung z. B. in Microsoft Excel oder ähnlichen Anwendungen.
- Bereitstellung der Rohdaten im JSON-Format – ideal für Entwickler, APIs oder Tools, die die Daten maschinell weiterverarbeiten möchten.
Beide Varianten sind über den Webserver bequem über den Browser abrufbar – lokal, ohne Cloud-Anbindung, und ohne zusätzliche Software.


Der Download der CSV-Datei funktioniert besonders komfortabel: Der Chrome-Browser bietet die Datei beim Aufruf der entsprechenden URL direkt zum Speichern an – ganz ohne zusätzliches Zutun. Alles, was wir im Code tun müssen, ist, die CSV-Daten korrekt an den Client zu senden. Der Browser übernimmt den Rest automatisch.
Ich habe dir den Code bereits im Beitrag Shelly 1PM + ESP32: Daten speichern & zeitgesteuert senden ausführlich vorgestellt, hier soll es lediglich um die REST-Schnittstelle gehen. Du findest im nächsten Kapitel jedoch den kompletten Quellcode als ZIP-Datei zum download.
Schritt 1 – initialisieren des Servers
Da wir bereits eine aktive WiFi-Verbindung haben, können wir direkt mit der Initialisierung des Servers starten. Das Gute ist, das MicroPython dafür alles mitbringt und wir hier keine Zusätzlichen Module installieren müssen (wir müssen diese lediglich im Code importieren).
# Module für Netzwerk, Datenverarbeitung und Zugriff auf gespeicherte Daten import socket import ujson import datastore import util import logger from configuration import REST_SERVER_PORT # Initialisiert den REST-Server auf dem angegebenen Port (Standard: 80) def init_rest_server(port=REST_SERVER_PORT): addr = socket.getaddrinfo('0.0.0.0', port)[0][-1] # Ermittelt die Adresse s = socket.socket() # Erstellt einen TCP-Socket s.bind(addr) # Bindet den Socket an die Adresse s.listen(1) # Wartet auf Verbindungen (max. 1 gleichzeitig) s.setblocking(False) # Setzt den Socket auf nicht-blockierend (wichtig für Main-Loop) logger.debugLog(f'REST-Server läuft auf Port {port}') return s
Schritt 2 – Anfragen akzeptieren und senden der Daten
Die nächste Funktion beinhaltet das akzeptieren einer Anfrage vom Client und das senden der Daten.
parse_http_request(request)
– HTTP-Anfrage analysieren
Diese Funktion extrahiert aus der ersten Zeile der eingehenden HTTP-Anfrage die HTTP-Methode (z. B. GET
) sowie den angeforderten Pfad (z. B. /data?format=csv
). So wissen wir, auf welche Ressource zugegriffen werden soll.
def parse_http_request(request): try: request_line = request.split('\r\n')[0] method, path, _ = request_line.split() return method, path except: return None, None
get_format_from_path(path)
– Ausgabeformat bestimmen (JSON oder CSV)
Hier wird geprüft, ob in der URL ein format=
-Parameter übergeben wurde. Falls ja, wird das gewünschte Format zurückgegeben – standardmäßig ist json
voreingestellt. Beispiel:/data?format=csv
→ Ausgabe als CSV-Datei.
def get_format_from_path(path): format_type = "json" if '?' in path: query = path.split('?')[1] params = query.split('&') for p in params: if '=' in p: key, value = p.split('=') if key == 'format': format_type = value.lower() return format_type
respond_with_csv(client, data)
– CSV-Antwort an den Client senden
Diese Funktion übernimmt den CSV-Export: Die Daten werden in CSV umgewandelt und mit den passenden HTTP-Headern an den Client zurückgeschickt. Browser wie Chrome starten bei Content-Type: text/csv
automatisch den Dateidownload.
def respond_with_csv(client, data): csv_data = util.dict_to_csv(data) client.send("HTTP/1.1 200 OK\r\n") client.send("Content-Type: text/csv\r\n") client.send("Access-Control-Allow-Origin: *\r\n") client.send("Connection: close\r\n\r\n") client.send(csv_data)
respond_with_json(client, data)
– JSON-Daten senden
Hier werden die Daten als JSON an den Client gesendet. Das ist besonders hilfreich für Entwickler, APIs oder Automatisierungsskripte, die die Rohdaten weiterverarbeiten möchten.
def respond_with_json(client, data): json_data = ujson.dumps(data) client.send("HTTP/1.1 200 OK\r\n") client.send("Content-Type: application/json\r\n") client.send("Access-Control-Allow-Origin: *\r\n") client.send("Connection: close\r\n\r\n") client.send(json_data)
handle_rest_request(server)
– REST-Endpunkt zentral verarbeiten
Dies ist die Hauptfunktion, die die REST-Schnittstelle steuert. Sie akzeptiert eingehende Verbindungen, wertet die Anfrage aus, lädt die Daten und entscheidet anhand des Formats, ob JSON oder CSV gesendet werden soll. Für ungültige Pfade wird eine einfache 404-Meldung zurückgegeben.
def handle_rest_request(server): try: client, addr = server.accept() except OSError: return try: request = client.recv(1024).decode('utf-8') method, path = parse_http_request(request) if method == 'GET' and path.startswith('/data'): format_type = get_format_from_path(path) data = datastore.read_json_file() if format_type == 'csv': respond_with_csv(client, data) else: respond_with_json(client, data) else: client.send("HTTP/1.1 404 Not Found\r\n") client.send("Content-Type: text/plain\r\n\r\n") client.send("Seite nicht gefunden.") except Exception as e: logger.debugLog(f"Fehler beim Verarbeiten: {e}") finally: client.close()
Quellcode zum Abrufen der Daten vom ESP32 via REST-Schnittstelle
Den Quellcode habe ich in mehrere Dateien aufgeteilt und so deutlich übersichtlicher gestaltet.
- main.py => Hauptprogramm
- wifi.py => Aufbau der WiFi-Verbindung
- datastore.py => Funktionen zum Daten vom Mikrocontroller zu lesen und zu speichern
- led.py => LEDs steuern (WiFi, Fehler, Datenabruf)
- logger.py => Datenlogger für Fehler-/Infomeldungen
- util.py => Hilfsfunktionen für das Programm (Datumformatieren, etc.)
- configuration.py => Werte für die WiFi-Verbindung, Server-Port, etc.
Quellcode – main.py
# Standardbibliothek für Zeitfunktionen import time # Eigene Module import wifi # WLAN-Verbindung herstellen import led # LEDs zur Statusanzeige import fetchShellyData as shelly # Modul zum Abrufen von Shelly-Daten via HTTP import util # Hilfsfunktionen, z. B. zum Parsen der Daten oder RTC-Sync import logger # Debug-Logging import datastore # Daten lokal speichern/verwalten import machine # MicroPython-Hardwarefunktionen (z. B. Reset, Pin, etc.) import server as restServer # REST-API-Modul zum Bereitstellen von Daten # Zeitintervall zwischen zwei Datenabrufen (in Millisekunden) from configuration import FETCH_INTERVAL_MS # Zeitpunkt des letzten Abrufs merken letzter_aufruf = time.ticks_ms() # Aufgabe: Daten vom Shelly holen, aufbereiten und speichern def fetchData_task(): data = shelly.fetch_shelly_data() # Rohdaten vom Shelly abfragen (HTTP GET) shellyData = util.extract_shelly_values(data) # Wichtige Werte extrahieren (current, voltage, apower) led.receivedDataLED() # LED blinkt kurz zur Bestätigung des Dateneingangs datastore.read_json_file() # Vorherige Daten aus Datei laden datastore.remove_old_entries() # Alte Einträge ggf. löschen (z. B. > 5 Tage alt) datastore.add_entry(shellyData) # Aktuelle Daten hinzufügen datastore.save_data_to_file() # Daten wieder in Datei speichern # Hauptprogramm try: led.setErrorLED(False) # Fehler-LED ausschalten led.setWiFiActive(False) # WiFi-LED ebenfalls deaktivieren (wird gleich gesetzt) isWiFiConnected = wifi.connect_wifi() # Mit WLAN verbinden logger.debugLog('WiFi Verbindung: '+ ('AN' if isWiFiConnected else 'AUS')) if isWiFiConnected: if util.sync_rtc(): # Systemzeit mit NTP synchronisieren server = restServer.init_rest_server() # REST-API initialisieren while True: restServer.handle_rest_request(server) # HTTP-Anfragen bearbeiten jetzt = time.ticks_ms() # Aktueller Zeitstempel if time.ticks_diff(jetzt, letzter_aufruf) >= FETCH_INTERVAL_MS: fetchData_task() # Nur alle 5 Sekunden Daten abrufen letzter_aufruf = jetzt else: led.setErrorLED(True) # RTC-Sync fehlgeschlagen → Fehler anzeigen else: led.setErrorLED(True) # WLAN-Verbindung fehlgeschlagen → Fehler anzeigen led.setWiFiActive(False) # WLAN-LED am Ende wieder deaktivieren except Exception as e: led.setErrorLED(True) # Fehler-LED einschalten bei unerwartetem Fehler print(e) # Fehlerausgabe zur Analyse
Quellcode – wifi.py
# Importiere Netzwerksteuerung für WLAN import network import time import led from configuration import WIFI_SSID as SSID, WIFI_PASSWORD as PASSWORD # WLAN-Zugangsdaten (→ solltest du später z. B. in eine separate config.py auslagern) SSID = "FRITZBox7590GI24" PASSWORD = "22894580214767401850" # Verbindet das Gerät mit dem WLAN-Netzwerk def connect_wifi(): global SSID, PASSWORD # WLAN-Schnittstelle im Station-Modus (STA_IF = Client-Modus) wlan = network.WLAN(network.STA_IF) wlan.active(True) # Aktiviert WLAN # Wenn noch keine Verbindung besteht, versuche zu verbinden if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(SSID, PASSWORD) # Warte auf Verbindung (max. 10 Sekunden) timeout = 10 # Sekunden while not wlan.isconnected() and timeout > 0: led.blink(0.25) # Status-LED blinkt während Verbindungsversuch timeout -= 0.5 print(".", end="") # Ausgabe eines Punkts pro Versuch (optional) # Ergebnis prüfen if wlan.isconnected(): print("\nWLAN verbunden.") print("IP-Adresse:", wlan.ifconfig()[0]) # Ausgabe der zugewiesenen IP-Adresse led.setWiFiActive(True) # WLAN-LED einschalten return True else: print("\nVerbindung fehlgeschlagen.") led.setWiFiActive(False) # WLAN-LED ausschalten return False
Quellcode – datastore.py
# JSON-Verarbeitung und Logging import ujson import logger import util from configuration import DATASTORE_FILENAME as filename # Globale Datenstruktur zum Zwischenspeichern der Daten im RAM data = {} # Liest die JSON-Datei ein und lädt sie als Dictionary in die globale Variable `data` def read_json_file(): global data, filename try: with open(filename, 'r') as file: data = ujson.load(file) # Inhalt der Datei in `data` laden return data except OSError: logger.debugLog(f"Datei '{filename}' wurde nicht gefunden.", "ERROR") except ValueError: logger.debugLog(f"Fehler beim Einlesen der Datei '{filename}': Ungültiges JSON-Format.","ERROR") # Fügt einen neuen Messwert in die Datenstruktur ein def add_entry(values_dict): global data # Extrahiere Datum im Format DD.MM.YYYY und Zeitstempel (Unixzeit) timestamp_str = values_dict['unixtime'] date_str = util.timestamp_to_date(values_dict['unixtime']) # Wenn der Tag noch nicht im Dictionary ist, anlegen if date_str not in data: data[date_str] = {} # Entferne den Zeitstempel aus dem Dictionary der Messwerte (ist schon im Key enthalten) values_dict.pop('unixtime') # Werte unter dem entsprechenden Datum und Zeitstempel speichern data[date_str][timestamp_str] = values_dict # Speichert die globale `data`-Struktur als JSON-Datei def save_data_to_file(): global data, filename try: with open(filename, "w") as file: ujson.dump(data, file) # Schreibe Daten in Datei logger.debugLog("Daten erfolgreich gespeichert.") except OSError as osError: logger.debugLog("Fehler beim Schreiben der Datei. {osError}", "ERROR") # Löscht alle Datums-Einträge, die älter als X Tage sind (Standard: 5 Tage) def remove_old_entries(): global data # Prüfe, welche Datums-Schlüssel älter als erlaubt sind keys_to_delete = [key for key in data if util.is_older_than_days(key)] # Entferne alte Einträge for key in keys_to_delete: del data[key]
Quellcode – led.py
# Importiere Pin-Steuerung und Zeitfunktionen from machine import Pin import time # Initialisierung der LED-Pins (Anpassen je nach Board / Verkabelung) ledWiFi = Pin(2, Pin.OUT) # LED für WLAN-Status ledError = Pin(3, Pin.OUT) # LED für Fehleranzeige ledData = Pin(4, Pin.OUT) # LED zeigt an, dass neue Daten empfangen wurden # Schaltet die WLAN-LED ein oder aus def setWiFiActive(value): ledWiFi.value(1 if value else 0) # Blinkt die WLAN-LED einmal für das angegebene Zeitintervall (z. B. beim Verbindungsaufbau) def blink(intervall): setWiFiActive(True) time.sleep(intervall) setWiFiActive(False) time.sleep(intervall) # Schaltet die Fehler-LED ein oder aus def setErrorLED(value): ledError.value(1 if value else 0) # Blinkt kurz die Daten-LED, wenn neue Werte gespeichert wurden def receivedDataLED(): ledData.value(1) time.sleep(0.25) ledData.value(0) time.sleep(0.25)
Quellcode – logger.py
import time def debugLog(text, level="INFO"): try: print(text) now = time.localtime() datum = "{:02d}.{:02d}.{:04d}".format(now[2], now[1], now[0]) uhrzeit = "{:02d}:{:02d}:{:02d}".format(now[3], now[4], now[5]) log_line = f"[{datum} {uhrzeit}] [{level.upper()}] {text}\n" with open("debug.log", "a") as log_file: log_file.write(log_line) except Exception as e: print("Fehler beim Schreiben ins Log:", e)
Quellcode – util.py
# Standard- und Projektmodule import json import logger import ntptime import time import machine from configuration import DATA_RETENTION_DAYS # Extrahiert nur die benötigten Messwerte aus der vollständigen Shelly-Antwort def extract_shelly_values(data): try: voltage = data["switch:0"]["voltage"] apower = data["switch:0"]["apower"] current = data["switch:0"]["current"] unixtime = data["sys"]["unixtime"] # Gibt ein flaches Dictionary mit den wichtigsten Werten zurück return { "voltage": voltage, "apower": apower, "current": current, "unixtime": unixtime } except KeyError as e: logger.debugLog("Fehlender Wert im JSON: {e}") return None # Wandelt die aktuelle RTC-Zeit in das Format DD.MM.YYYY um (z. B. für die Datenstruktur im Speicher) def timestamp_to_date(timestamp): try: t = machine.RTC().datetime() return "{:02d}.{:02d}.{:04d}".format(t[2], t[1], t[0]) except Exception as e: logger.debugLog(e) return "-error-" # Gleiche Funktion wie oben, aber klar benannt (könnte ggf. ersetzt oder vereinheitlicht werden) def formatDate(): t = machine.RTC().datetime() return "{tag:02d}.{monat:02d}.{jahr:4d}".format(tag = t[2],monat= t[1],jahr= t[0]) # Synchronisiert die Echtzeituhr (RTC) mit einem NTP-Server über das Internet def sync_rtc(): try: ntptime.settime() logger.debugLog("RTC erfolgreich synchronisiert.") return True except Exception as e: print(e) logger.debugLog("Fehler bei der RTC-Synchronisation. {e}", "ERROR") return False # Prüft, ob ein gegebenes Datum älter ist als eine bestimmte Anzahl an Tagen def is_older_than_days(date_str, days=DATA_RETENTION_DAYS): try: # Aktuelles Datum vom RTC-Modul holen today = machine.RTC().datetime() current_timestamp = time.mktime((today[0], today[1], today[2], 0, 0, 0, 0, 0)) # Umwandlung des gegebenen Datumsstrings in einen Zeitstempel tag, monat, jahr = map(int, date_str.split(".")) entry_timestamp = time.mktime((jahr, monat, tag, 0, 0, 0, 0, 0)) # Vergleich delta = current_timestamp - entry_timestamp return delta > days * 86400 # 86400 Sekunden = 1 Tag except Exception as e: logger.debugLog("Fehler bei Datumsprüfung: {e}", "ERROR") return False # Prüft, ob Internet verfügbar ist (z. B. für NTP oder externe APIs) def internet_available(): try: response = urequests.get("http://www.google.com") if response.status_code == 200: response.close() logger.debugLog("Internetverbindung verfügbar.") return True else: logger.debugLog("Internetverbindung nicht erfolgreich. Status: "+ response.status_code, "ERROR") response.close() return False except Exception as e: logger.debugLog("Kein Internetzugriff: {e}", "ERROR") return False # Konvertiert die verschachtelte Datenstruktur in ein CSV-Format mit Semikolon als Trennzeichen def dict_to_csv(data): # Header-Zeile definieren csv_lines = ["date;timestamp;current;voltage;apower"] for date, timestamps in data.items(): for ts, values in timestamps.items(): # Komma statt Punkt für Dezimaltrennung (deutsches Format) current = str(values.get('current', '')).replace('.', ',') voltage = str(values.get('voltage', '')).replace('.', ',') apower = str(values.get('apower', '')).replace('.', ',') # Zeile zusammenbauen line = f"{date};{ts};{current};{voltage};{apower}" csv_lines.append(line) return '\n'.join(csv_lines)
Quellcode – configuration.py
WIFI_SSID = "abc" WIFI_PASSWORD = "123" # Shelly API-Endpunkt SHELLY_URL = "http://192.168.178.38/rpc/Shelly.GetStatus" # Dateiname für gespeicherte Daten DATASTORE_FILENAME = "data.json" # REST-Server REST_SERVER_PORT = 80 # Datenabrufintervall in Millisekunden FETCH_INTERVAL_MS = 5000 # z. B. alle 5 Sekunden # Maximale Aufbewahrungszeit (z. B. 5 Tage) DATA_RETENTION_DAYS = 5
Fazit
Die Umsetzung einer REST-Schnittstelle mit MicroPython ist erstaunlich unkompliziert und bietet jede Menge Potenzial. Wie in diesem Beitrag gezeigt, lassen sich so die lokal gespeicherten Verbrauchsdaten vom ESP32 ganz einfach im CSV- oder JSON-Format abrufen – direkt im Browser und ohne zusätzliche Tools.
Doch damit ist noch lange nicht Schluss: Die REST-Schnittstelle lässt sich flexibel erweitern – etwa um zusätzliche Parameter zur Filterung der Daten, z. B. nach Datum oder Uhrzeit.
Wenn dich das interessiert, zeige ich dir gerne in einem weiteren Beitrag, wie du deine Ausgaben gezielt eingrenzen und nur die Daten abrufen kannst, die du wirklich brauchst.
Hinterlasse mir einen Kommentar, wenn du dir diesen erweiterten Beitrag wünschst!