Der Shelly 1PM kann nicht nur Verbraucher schalten, sondern auch Verbrauchsdaten in der Cloud speichern und visualisieren. Allerdings ist dafür eine aktive WLAN-Verbindung erforderlich. Ein großer Vorteil des Shelly ist, dass lokale Szenen auch ohne Internetverbindung ausgeführt werden und das Gerät weiterhin über das lokale Netzwerk erreichbar bleibt. Doch sobald die Internetverbindung unterbrochen ist, entstehen Lücken in der Datenaufzeichnung – ein Problem, wenn man eine lückenlose Historie benötigt. In diesem Beitrag zeige ich eine einfache und kostengünstige Lösung, um Messdaten mit einem ESP32 zu puffern und sie zeitgesteuert an ThingSpeak zu senden.
Hinweis: Dieser Beitrag entstand aus einer Idee aus meiner YouTube-Community. Ich freue mich immer über Anregungen und Fragen von euch – so kann ich gezielt Lösungen für echte Probleme entwickeln. Wenn auch du ein technisches Problem hast oder eine Idee für einen Beitrag, dann schreib mir gerne eine Mail oder hinterlasse einen Kommentar!
Die hier präsentierte Schaltung und Umsetzung funktioniert mit allen Shelly-Geräten, die eine Leistungsmessung unterstützen. Allerdings kann der JSON-Response je nach Modell variieren, daher solltest du diesen überprüfen und gegebenenfalls an den entsprechenden Stellen anpassen.




Inhaltsverzeichnis
- REST-Schnittstelle am Shelly
- Ablegen der Daten auf dem Mikrocontroller
- Aufbau der Schaltung mit einem Shelly 1PM Mini Gen3 und ESP32-C3 Mini
- Speichern der Daten von einem Shelly 1PM auf dem ESP32-C3 Mini
- Visualisieren / Verarbeiten der gespeicherten Daten vom ESP32-C3 in ThingSpeak
REST-Schnittstelle am Shelly
Ein großer Vorteil der Shelly-Geräte ist die einfache REST-Schnittstelle, über die sie nicht nur geschaltet, sondern auch ausgelesen werden können.
Mit dem Aufruf http://<IP-Adresse>/rpc/Shelly.GetStatus
lässt sich der aktuelle Status des Shelly als JSON abrufen. Das JSON-Format ist dabei sowohl für Menschen gut lesbar als auch leicht von einem Mikrocontroller zu verarbeiten. Zudem gibt es keine Begrenzung bei der Abrufhäufigkeit, sodass die Daten theoretisch sogar im Sekundentakt abgefragt werden können.
Aus dem JSON-Response wird der Block "switch:0"
ausgewertet, da er die Leistungsdaten des angeschlossenen Verbrauchers enthält. Zusätzlich können wir den aktuellen Zeitstempel "unixtime"
aus dem Block "sys"
entnehmen und ebenfalls speichern.
Ablegen der Daten auf dem Mikrocontroller
Die gesammelten Daten werden auf dem Mikrocontroller in einer JSON-Datei gespeichert und bei bestehen einer Internetverbindung an ThingSpeak gesendet. Der ESP32-C3 verfügt über 4 MB Flash-Speicher, was ausreichend ist, um die Daten eines Tages zu speichern.
Da dieses Projekt auch in Umgebungen mit instabiler Internetverbindung eingesetzt werden kann, wird vor dem Senden zunächst geprüft, ob eine Verbindung besteht. Ist das der Fall, werden die Daten übertragen. Falls keine Verbindung verfügbar ist, wird das Senden zu einem späteren Zeitpunkt erneut versucht.
Aufbau der Schaltung mit einem Shelly 1PM Mini Gen3 und ESP32-C3 Mini
Für den Aufbau der kleinen Schaltung benötigst du:
- einen Shelly 1PM Mini Gen3* oder Shelly Plug S*
- einen ESP32-C3 Mini*
- ein USB-C Datenkabel
Hinweis von mir: Die mit einem Sternchen (*) markierten Links sind Affiliate-Links. Wenn du über diese Links einkaufst, erhalte ich eine kleine Provision, die dazu beiträgt, diesen Blog zu unterstützen. Der Preis für dich bleibt dabei unverändert. Vielen Dank für deine Unterstützung!
Der Shelly Plug S bietet den großen Vorteil, dass keine spezielle Verkabelung erforderlich ist. Du kannst einfach einen Verbraucher direkt über die integrierte Schuko-Steckdose anschließen – ideal für Einsteiger, die schnell und unkompliziert loslegen möchten.
Speichern der Daten von einem Shelly 1PM auf dem ESP32-C3 Mini
Um die Daten des Shelly 1PM auf dem ESP32-C3 speichern zu können, muss zunächst eine Verbindung zum Gerät hergestellt und der JSON-Response über die bereits erwähnte REST-Schnittstelle abgerufen und ausgewertet werden. Die Umsetzung ist sowohl mit der Arduino IDE als auch mit MicroPython möglich. Da das Dateihandling in MicroPython deutlich einfacher und übersichtlicher ist, habe ich mich in diesem Projekt für diese Variante entschieden.
Flashen des ESP32-C3 Mini für die Programmierung in MicroPython
Bevor der kleine Mikrocontroller mit MicroPython programmiert werden kann, muss er zunächst mit der passenden Firmware geflasht werden. Das funktioniert entweder ganz bequem über Thonny, eine leicht bedienbare Entwicklungsumgebung, oder mithilfe des esptool von Espressif Systems über die Kommandozeile. Im folgenden Video zeige ich dir beide Varianten Schritt für Schritt.
Speichern von Daten in einer JSON-Datei auf dem Mikrocontroller ESP32-C3
Der ESP32-C3 verfügt über 4 MB internen Flash-Speicher, der sich vielseitig nutzen lässt – etwa für das Speichern von Bildern, Textdateien oder kleinen Datensätzen. In diesem Projekt verwenden wir den Speicher, um eine JSON-Datei anzulegen und dort die erfassten Messdaten strukturiert abzulegen.
Im ersten Schritt erstellen wir die JSON-Datei auf dem Mikrocontroller und definieren eine Grundstruktur, die später fortlaufend mit neuen Werten ergänzt wird.
{ "10.03.2025": { "1741590664": { "voltage": 230, "ampere": 12, "apower": 23, "current": 1 }, "1741590685": { "voltage": 225, "ampere": 11, "apower": 24, "current": 2 } } }
JSON vs. Dictionary in Python
MicroPython ist eine speziell für Mikrocontroller optimierte Variante der Programmiersprache Python. Viele bekannte Sprachmuster und Konzepte aus Python lassen sich daher direkt wiederverwenden – das macht die Programmierung besonders einfach und effizient.
Da JSON-Datenstrukturen in Python direkt als Dictionary dargestellt werden können, ist der Umgang mit empfangenen oder zu speichernden Daten besonders komfortabel. So lassen sich JSON-Dateien ganz leicht einlesen, bearbeiten und wieder abspeichern – ideal für den Einsatz auf dem ESP32-C3.
import json # Datei einlesen und in ein Dictionary umwandeln def read_json_file(filename): try: with open(filename, 'r') as file: data = json.load(file) return data except FileNotFoundError: print(f"Datei '{filename}' wurde nicht gefunden.") except json.JSONDecodeError: print(f"Fehler beim Einlesen der Datei '{filename}': Ungültiges JSON-Format.") return {} # Dictionary schön ausgeben def print_json_dict(data): for key, value in data.items(): print(f"{key}: {value}") # Hauptprogramm filename = "data.json" json_data = read_json_file(filename) if json_data: print("Inhalt der Datei:") print_json_dict(json_data)
Dieser Code liest die Datei data.json, die sich auf dem Mikrocontroller befindet, ein und gibt deren Inhalt in der Shell von Thonny aus.
Speichern von Daten in der Datei data.json
Das Speichern der Daten ist ebenso unkompliziert wie das Einlesen: Wir befüllen einfach unser Dictionary mit den gewünschten Schlüssel-Wert-Paaren und schreiben es anschließend zurück in die Datei. So lassen sich Messwerte strukturiert und dauerhaft auf dem Mikrocontroller ablegen.
import ujson # Beispiel-Daten (könnten z. B. von einem Sensor stammen) data = { "10.03.2025": { "1741590664": { "voltage": 230, "ampere": 12, "apower": 23, "current": 1 } } } # Datei öffnen und Dictionary als JSON speichern def save_json_file(filename, content): try: with open(filename, 'w') as file: ujson.dump(content, file) print("Daten erfolgreich gespeichert.") except OSError: print("Fehler beim Schreiben der Datei.") # Aufruf der Funktion save_json_file("data.json", data)
Um einen neuen Datensatz dem Dictionary hinzuzufügen, prüfen wir zunächst, ob bereits ein Eintrag für das entsprechende Datum existiert. Falls ja, fügen wir lediglich den neuen Zeitstempel samt Messwerten zum bestehenden Datum hinzu. Existiert das Datum noch nicht, wird ein neuer Schlüssel mit dem Datum angelegt und der Datensatz dort eingefügt.
# Globales Dictionary (muss vorher definiert sein) data = {} def add_entry(date_str, timestamp_str, values_dict): """ Fügt einen neuen Datensatz zum globalen Dictionary 'data' hinzu. :param date_str: Datum als String, z. B. "10.03.2025" :param timestamp_str: Zeitstempel als String, z. B. "1741590664" :param values_dict: Dictionary mit Messwerten """ global data if date_str not in data: data[date_str] = {} data[date_str][timestamp_str] = values_dict
Nachdem die Daten ergänzt wurden, müssen sie nur noch in das Dictionary zurückgeschrieben werden. Damit ist der erste Schritt geschafft: das Speichern und Lesen von JSON-Daten auf dem ESP32-C3 Super Mini mit MicroPython.
fertiger Quellcode zum lesen & speichern von JSON Daten auf dem ESP32-C3
# ----------------------------------------------------------------------------- # Autor: Stefan Draeger # Webseite: https://draeger-it.blog # Beitrag: https://draeger-it.blog/shelly-1pm-esp32-daten-speichern-zeitgesteuert-senden/ # Beschreibung: # Dieses Skript liest eine JSON-Datei vom ESP32-C3 ein, fügt neue Messdaten # hinzu und speichert die aktualisierten Daten zurück in die Datei. # ----------------------------------------------------------------------------- import ujson # Globales Dictionary zur Speicherung der Messdaten data = {} # Liest die JSON-Datei ein und wandelt den Inhalt in ein Dictionary um def read_json_file(filename): global data try: with open(filename, 'r') as file: data = ujson.load(file) except OSError: print(f"Datei '{filename}' wurde nicht gefunden.") except ValueError: print(f"Fehler beim Einlesen der Datei '{filename}': Ungültiges JSON-Format.") # Fügt dem Dictionary einen neuen Eintrag mit Zeitstempel und Messwerten hinzu def add_entry(date_str, timestamp_str, values_dict): """ Fügt einen neuen Datensatz zum globalen Dictionary 'data' hinzu. :param date_str: Datum als String, z. B. "10.03.2025" :param timestamp_str: Zeitstempel als String, z. B. "1741590664" :param values_dict: Dictionary mit Messwerten """ global data # Wenn das Datum noch nicht existiert, wird es angelegt if date_str not in data: data[date_str] = {} # Zeitstempel samt Werten hinzufügen oder überschreiben data[date_str][timestamp_str] = values_dict # Speichert das Dictionary als JSON-Datei auf dem ESP32 def save_data_to_file(): """ Speichert das globale Dictionary 'data' in die Datei 'data.json'. """ global data try: with open("data.json", "w") as file: ujson.dump(data, file) print("Daten erfolgreich gespeichert.") except OSError: print("Fehler beim Schreiben der Datei.") # Gibt das Dictionary zeilenweise in der Shell aus def print_json_dict(): global data for key, value in data.items(): print(f"{key}: {value}") # ---------------------------------------------------------------------- # Hauptprogramm # ---------------------------------------------------------------------- filename = "data.json" # Vorhandene Datei einlesen read_json_file(filename) # Inhalt der Datei anzeigen print("Inhalt der Datei:") print_json_dict() # Neuen Messwert hinzufügen add_entry("10.03.2025", "1741590722", { "voltage": 228, "ampere": 9, "apower": 21, "current": 3 }) # Datei mit neuem Eintrag speichern save_data_to_file() # Datei erneut laden und aktualisierte Daten anzeigen read_json_file(filename) print("Aktualisierter Inhalt der Datei:") print_json_dict()
Auslesen des JSON-Response vom Shelly 1PM oder Shelly Plug S
Aufbau einer WiFi-Verbindung am ESP32
Bevor wir die REST-Schnittstelle des Shellys ansprechen können, müssen wir eine Verbindung zum lokalen WLAN aufbauen. Dieses ist mit wenigen Zeilen Code gelöst.
Quellcode zum aufbauen einer WiFi-Verbindung am ESP32
# ----------------------------------------------------------------------------- # Autor: Stefan Draeger # Webseite: https://draeger-it.blog # Beitrag: https://draeger-it.blog/shelly-1pm-esp32-daten-speichern-zeitgesteuert-senden/ # # Beschreibung: # Dieses Skript stellt mit dem ESP32-C3 Super Mini eine Verbindung zum lokalen # WLAN her und nutzt eine Status-LED an GPIO 2 als einfache Rückmeldung. # # - Während der Verbindungssuche blinkt die LED. # - Nach erfolgreicher Verbindung leuchtet die LED dauerhaft. # - Bei fehlgeschlagener Verbindung bleibt die LED aus. # ----------------------------------------------------------------------------- import network import time from machine import Pin # WLAN-Zugangsdaten SSID = "abc" PASSWORD = "123" # Status-LED an GPIO 2 (z. B. für den ESP32-C3 Super Mini) led = Pin(2, Pin.OUT) # Funktion zum Aufbau der WLAN-Verbindung def connect_wifi(ssid, password): # WLAN-Modul im Station Mode aktivieren wlan = network.WLAN(network.STA_IF) wlan.active(True) # Verbindung starten, falls noch nicht verbunden if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(ssid, password) # LED blinkt während der Verbindungssuche timeout = 10 # Sekunden while not wlan.isconnected() and timeout > 0: led.value(1 - led.value()) # LED-Zustand wechseln (manuelles Blinken) time.sleep(0.5) timeout -= 0.5 print(".", end="") # Verbindung erfolgreich if wlan.isconnected(): print("\nWLAN verbunden.") print("IP-Adresse:", wlan.ifconfig()[0]) led.value(1) # LED dauerhaft an return True else: # Verbindung fehlgeschlagen print("\nVerbindung fehlgeschlagen.") led.value(0) # LED bleibt aus return False # WLAN-Verbindung aufbauen connect_wifi(SSID, PASSWORD)
Wenn die WiFi-Verbindung aufgebaut wurde, dann wird auf der Shell die zugewiesene IP-Adresse ausgegeben sowie wird eine LED zum leuchten gebracht.
Synchronisieren der RTC vom ESP32
Damit wir später einen gültigen Zeitstempel als Schlüssel für unsere Datenspeicherung verwenden können, müssen wir zunächst die RTC (Echtzeituhr) des ESP32 synchronisieren. Sobald die WLAN-Verbindung steht, lässt sich das über einen NTP-Server ganz einfach erledigen.
Quellcode zum synchronisieren der RTC am ESP32-C3
# ----------------------------------------------------------------------------- # Autor: Stefan Draeger # Webseite: https://draeger-it.blog # Beitrag: https://draeger-it.blog/shelly-1pm-esp32-daten-speichern-zeitgesteuert-senden/ # # Beschreibung: # Dieses Skript verbindet den ESP32-C3 mit einem WLAN, synchronisiert die # interne Echtzeituhr (RTC) über einen NTP-Server und gibt das aktuelle Datum # formatiert aus. # # - Während der Verbindungssuche blinkt die LED. # - Nach erfolgreicher Verbindung leuchtet die LED dauerhaft. # - Das Datum wird im Format TT.MM.JJJJ ausgegeben. # ----------------------------------------------------------------------------- import time import network import ntptime from machine import Pin # WLAN-Zugangsdaten SSID = "abc" PASSWORD = "123" # Status-LED an GPIO 2 (z. B. für den ESP32-C3 Super Mini) led = Pin(2, Pin.OUT) # Funktion zum Aufbau der WLAN-Verbindung def connect_wifi(ssid, password): """ Baut eine Verbindung zum WLAN auf und gibt den Status zurück. Während der Verbindungssuche blinkt die LED, nach erfolgreicher Verbindung leuchtet sie dauerhaft. :param ssid: Name des WLAN-Netzwerks :param password: WLAN-Passwort :return: True, wenn erfolgreich verbunden, sonst False """ wlan = network.WLAN(network.STA_IF) # WLAN-Modul im Station Mode wlan.active(True) # WLAN aktivieren if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(ssid, password) # LED blinkt während der Verbindungssuche timeout = 10 # Maximale Wartezeit in Sekunden while not wlan.isconnected() and timeout > 0: led.value(1 - led.value()) # LED-Zustand wechseln (manuelles Blinken) time.sleep(0.5) timeout -= 0.5 print(".", end="") if wlan.isconnected(): print("\nWLAN verbunden.") print("IP-Adresse:", wlan.ifconfig()[0]) led.value(1) # LED dauerhaft an = erfolgreich verbunden return True else: print("\nVerbindung fehlgeschlagen.") led.value(0) # LED bleibt aus return False # Funktion zur Umwandlung des Unix-Timestamps in ein formatiertes Datum def formatDate(): """ Wandelt den aktuellen Unix-Zeitstempel in ein lesbares Datum um. :return: String im Format "TT.MM.JJJJ" """ timestamp = time.localtime() # Aktuelle Zeit abrufen return "{tag:02d}.{monat:02d}.{jahr:4d}".format(tag=timestamp[2], monat=timestamp[1], jahr=timestamp[0]) # WLAN-Verbindung herstellen isWiFiConnected = connect_wifi(SSID, PASSWORD) # Falls WLAN verbunden, Zeit vom NTP-Server holen if isWiFiConnected: try: ntptime.settime() # Zeit vom NTP-Server synchronisieren print("Zeit erfolgreich synchronisiert.") print("Aktuelles Datum:", formatDate()) except: print("Fehler bei der Zeitsynchronisation.") else: print("Keine WLAN-Verbindung – NTP-Sync nicht möglich.")
Der Code stellt zunächst eine WLAN-Verbindung her und synchronisiert anschließend die interne RTC des ESP32 über einen NTP-Server. Sobald die Synchronisation abgeschlossen ist, wird der aktuelle Zeitstempel im Format eines Tupels (time.localtime()
) ausgelesen. Daraus wird das Datum im deutschen Format „Tag.Monat.Jahr“ generiert. Dieses Datum verwenden wir später als Schlüssel (Key) zur strukturierten Ablage der Messwerte in der JSON-Datei.
Abrufen der Daten vom Shelly via Modul urequest
Die Daten vom Shelly wird via GET Request abgerufen. Dazu benötigen wir die IP-Adresse des Shellys welchge wir aus den Geräteinformationen ablesen können.
Dieser IP-Adresse wird der Pfad „/rpc/Shelly.GetStatus“ angehangen und mit der Adresse können nun alle Daten abgerufen werden.
Quellcode zum Abrufen der Daten vom Shelly
# ----------------------------------------------------------------------------- # Autor: Stefan Draeger # Webseite: https://draeger-it.blog # Beitrag: https://draeger-it.blog/shelly-1pm-esp32-daten-speichern-zeitgesteuert-senden/ # # Beschreibung: # Dieses Skript verbindet den ESP32-C3 mit einem WLAN-Netzwerk, ruft aktuelle # Leistungsdaten eines Shelly-Geräts über die REST-API (rpc/Shelly.GetStatus) # ab und extrahiert daraus die benötigten Messwerte. # # - WLAN-Verbindung mit Statusanzeige über LED (GPIO 2) # - Abruf des Shelly-Status über HTTP (lokale IP) # - Extraktion von Spannung, Leistung, Strom und Unix-Zeitstempel # ----------------------------------------------------------------------------- import network import time from machine import Pin import urequests # WLAN-Zugangsdaten (bitte anpassen!) SSID = "abc" PASSWORD = "123" # Shelly-REST-Endpunkt für Statusdaten SHELLY_URL = "http://192.168.178.91/rpc/Shelly.GetStatus" # Status-LED an GPIO 2 (zeigt WLAN-Status an) led = Pin(2, Pin.OUT) # Verbindet den ESP32 mit dem WLAN und zeigt den Status über die LED an def connect_wifi(ssid, password): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(ssid, password) # LED blinkt während der Verbindungssuche timeout = 10 # Sekunden while not wlan.isconnected() and timeout > 0: led.value(1 - led.value()) # LED-Zustand wechseln time.sleep(0.5) timeout -= 0.5 print(".", end="") if wlan.isconnected(): print("\nWLAN verbunden.") print("IP-Adresse:", wlan.ifconfig()[0]) led.value(1) # LED dauerhaft an return True else: print("\nVerbindung fehlgeschlagen.") led.value(0) # LED aus return False # Ruft die JSON-Daten vom Shelly-Device ab def fetch_shelly_data(): try: response = urequests.get(SHELLY_URL) if response.status_code == 200: json_data = response.json() # JSON zu Python-Dictionary umwandeln response.close() return json_data else: print("Fehler beim Abrufen der Daten:", response.status_code) response.close() return None except Exception as e: print("Verbindungsfehler:", e) return None # Extrahiert relevante Werte aus der 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"] # Rückgabe als kompaktes Dictionary return { "voltage": voltage, "apower": apower, "current": current, "unixtime": unixtime } except KeyError as e: print("Fehlender Wert im JSON:", e) return None # Gibt das aktuelle Datum im Format TT.MM.JJJJ zurück def formatDate(): timestamp = time.localtime() return "{:02d}.{:02d}.{:04d}".format(timestamp[2], timestamp[1], timestamp[0]) # Hauptablauf isWiFiConnected = connect_wifi(SSID, PASSWORD) if isWiFiConnected: shelly_data = fetch_shelly_data() if shelly_data: print("Empfangene Daten:") print(shelly_data) values = extract_shelly_values(shelly_data) if values: print("Extrahierte Werte:") print(values)
In der Shell von Thonny kann man die Daten vom Shelly ablesen und ebenso die extrahierten Daten welche wir später auf dem Mikrocontroller speichern möchten.
Sammeln der Daten im Intervall von 5 Minuten
Die Daten sollen nun alle 5 Minuten automatisch abgerufen und gespeichert werden. Dazu starten wir den Hauptteil des Programms in einer Endlosschleife und legen anschließend eine Pause von 300 Sekunden (entspricht 5 Minuten) ein. Theoretisch lassen sich auf diese Weise bis zu 288 Datensätze pro Tag erfassen und auf dem ESP32-C3 speichern.
# 5 Minuten in Sekunden INTERVAL = 300 # Hauptablauf isWiFiConnected = connect_wifi(SSID, PASSWORD) while True: if isWiFiConnected: if sync_rtc(): shelly_data = fetch_shelly_data() if shelly_data: print("Empfangene Daten:") print(shelly_data) values = extract_shelly_values(shelly_data) if values: print("Extrahierte Werte:") print(values) datum = formatDate() print("Verwendbares Datum als Key:", datum) filename = "data.json" read_json_file(filename) print("Inhalt der Datei:") print_json_dict() add_entry(datum, values['unixtime'], values) save_data_to_file() read_json_file(filename) print_json_dict() print("Warte 5 Minuten bis zum nächsten Abruf...") time.sleep(INTERVAL)
Programm – Abrufen der Verbrauchsdaten eines Verbrauchers am Shelly und Speichern der Daten auf dem ESP32-C3
Dieses Programm verbindet den ESP32-C3 mit dem WLAN, ruft aktuelle Leistungsdaten vom Shelly über die REST-API ab und extrahiert die wichtigsten Werte wie Spannung, Leistung und Stromstärke. Du musst lediglich die WLAN-Zugangsdaten sowie die IP-Adresse deines Shelly-Geräts im Code anpassen – danach ist das Skript sofort einsatzbereit.
fertiges Programm zum abrufen der Leistungsdaten eines Verbrauchers am Shelly und speichern dieser auf dem ESP32-C3 als JSON
import network import time from machine import Pin import urequests import ntptime import ujson # Globales Dictionary für alle gesammelten Messdaten data = {} # WLAN-Zugangsdaten (bitte anpassen) SSID = "abc" PASSWORD = "123" # Shelly-Endpunkt für den Abruf der aktuellen Leistungsdaten SHELLY_URL = "http://192.168.178.91/rpc/Shelly.GetStatus" # Abfrageintervall in Sekunden (5 Minuten) INTERVAL = 300 # LED an GPIO 2 zur Anzeige der WLAN-Verbindung led = Pin(2, Pin.OUT) # Verbindet den ESP32 mit dem WLAN und zeigt den Status über die LED an def connect_wifi(ssid, password): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(ssid, password) # LED blinkt während der Verbindungssuche timeout = 10 while not wlan.isconnected() and timeout > 0: led.value(1 - led.value()) time.sleep(0.5) timeout -= 0.5 print(".", end="") if wlan.isconnected(): print("\nWLAN verbunden.") print("IP-Adresse:", wlan.ifconfig()[0]) led.value(1) # LED dauerhaft an bei Verbindung return True else: print("\nVerbindung fehlgeschlagen.") led.value(0) return False # Synchronisiert die interne Uhr über einen NTP-Zeitserver def sync_rtc(): try: ntptime.settime() print("RTC erfolgreich synchronisiert.") return True except: print("Fehler bei der RTC-Synchronisation.") return False # Wandelt Unix-Zeitstempel in ein Datumsformat TT.MM.JJJJ def timestamp_to_date(timestamp): try: t = time.localtime(timestamp) return "{:02d}.{:02d}.{:04d}".format(t[2], t[1], t[0]) except: return "00.00.0000" # Ruft die aktuellen Daten vom Shelly-Gerät ab def fetch_shelly_data(): try: response = urequests.get(SHELLY_URL) if response.status_code == 200: json_data = response.json() response.close() return json_data else: print("Fehler beim Abrufen der Daten:", response.status_code) response.close() return None except Exception as e: print("Verbindungsfehler:", e) return None # Extrahiert relevante Messwerte aus dem Shelly-JSON 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"] return { "voltage": voltage, "apower": apower, "current": current, "unixtime": unixtime } except KeyError as e: print("Fehlender Wert im JSON:", e) return None # Gibt das aktuelle Datum aus der RTC im Format TT.MM.JJJJ zurück def formatDate(): timestamp = time.localtime() return "{:02d}.{:02d}.{:04d}".format(timestamp[2], timestamp[1], timestamp[0]) # Liest die bestehende Datei 'data.json' in das globale Dictionary def read_json_file(filename): global data try: with open(filename, 'r') as file: data = ujson.load(file) except OSError: print(f"Datei '{filename}' wurde nicht gefunden.") except ValueError: print(f"Fehler beim Einlesen der Datei '{filename}': Ungültiges JSON-Format.") # Fügt einen neuen Eintrag zum globalen Dictionary hinzu def add_entry(date_str, timestamp_str, values_dict): """ Fügt die Messwerte unter dem jeweiligen Datum und Zeitstempel ins Dictionary ein. Der Timestamp wird dabei als Key verwendet, die Messwerte als Value. """ global data if date_str not in data: data[date_str] = {} # Entferne 'unixtime' aus den Werten, da er bereits als Key verwendet wird values_dict.pop('unixtime') data[date_str][timestamp_str] = values_dict # Speichert das globale Dictionary in die Datei 'data.json' def save_data_to_file(): global data try: with open("data.json", "w") as file: ujson.dump(data, file) print("Daten erfolgreich gespeichert.") except OSError: print("Fehler beim Schreiben der Datei.") # Gibt das komplette Dictionary formatiert in der Konsole aus def print_json_dict(): global data for key, value in data.items(): print(f"{key}: {value}") # ----------------------------------------- # Hauptprogramm # ----------------------------------------- # WLAN einmalig beim Start verbinden isWiFiConnected = connect_wifi(SSID, PASSWORD) # Hauptschleife: Alle 5 Minuten Daten abfragen und speichern while True: if isWiFiConnected: if sync_rtc(): shelly_data = fetch_shelly_data() if shelly_data: print("Empfangene Daten:") print(shelly_data) values = extract_shelly_values(shelly_data) if values: print("Extrahierte Werte:") print(values) # Aktuelles Datum als Schlüssel datum = formatDate() print("Verwendbares Datum als Key:", datum) filename = "data.json" read_json_file(filename) print("Inhalt der Datei:") print_json_dict() # Daten hinzufügen und speichern add_entry(datum, str(values['unixtime']), values) save_data_to_file() # Inhalt erneut ausgeben read_json_file(filename) print("Aktualisierter Inhalt:") print_json_dict() print("Warte 5 Minuten bis zum nächsten Abruf...") time.sleep(INTERVAL)
Visualisieren / Verarbeiten der gespeicherten Daten vom ESP32-C3 in ThingSpeak
Die Daten wurden nun erfolgreich auf dem ESP32-C3 gespeichert – doch lokal auf dem Mikrocontroller sind sie weder besonders lesbar noch einfach auswertbar. Daher möchte ich die erfassten Werte nun in ThingSpeak visualisieren.

Wie man Daten an ThingSpeak sendet, habe ich bereits in mehreren Beiträgen auf diesem Blog gezeigt, zum Beispiel:
- IoT-Service ThingSpeak einrichten und betreiben
- ThingSpeak & ESP Easy
- MAKER Pi Pico #1 – Senden von Sensordaten an ThingSpeak
- Calliope Mini & ThingSpeak
In diesem Abschnitt zeige ich dir, wie du die Messdaten deines Shelly-Geräts auslesen und anschließend – sofern eine Internetverbindung besteht – an ThingSpeak senden kannst. Sollte keine Verbindung verfügbar sein, werden die Daten zunächst lokal auf dem Mikrocontroller gespeichert und beim nächsten erfolgreichen Durchlauf automatisch übertragen.
Dank des internen Flash-Speichers von 4 MB beim ESP32-C3 Super Mini lassen sich auf diese Weise problemlos mehrere Tage an Daten puffern – ideal für Projekte mit unzuverlässiger Internetverbindung.
Damit die Daten bei der Übertragung zu ThingSpeak auch dem korrekten Zeitpunkt zugeordnet werden können, fügen wir der URL den zusätzlichen Parameter created_at
hinzu. So können wir auch nachträglich Daten mit dem tatsächlichen Zeitstempel hochladen – perfekt für eine lückenlose Historie.
Die URL zum Übertragen eines Datensatzes in meinem Fall sieht folgendermaßen aus:
https://api.thingspeak.com/update.json?
api_key=<WRITE_KEY>&
field1=<VOLTAGE>&
field2=<AMPERE>&
field3=<CURRENT>&
created_at=<TIMESTAMP>
Den Parameter created_at
können wir entweder im ISO 8601 Format (z. B. 2025-03-10T10:30:00Z
) oder als Unix-Timestamp übergeben. Beide Varianten sind von ThingSpeak akzeptiert und ermöglichen eine rückwirkende Zuordnung der Messdaten.
Quellcode
# ----------------------------------------------------------------------------- # Autor: Stefan Draeger # Webseite: https://draeger-it.blog # Beitrag: https://draeger-it.blog/shelly-1pm-esp32-daten-speichern-zeitgesteuert-senden/ # # Beschreibung: # Dieses MicroPython-Skript für den ESP32-C3 liest alle 5 Minuten Leistungsdaten # eines Shelly-Geräts per REST-Schnittstelle aus und speichert diese lokal in # einer JSON-Datei. Falls eine Internetverbindung besteht, werden die Daten an # ThingSpeak gesendet und anschließend vom Mikrocontroller gelöscht. # Zusätzlich wird ein Logfile "debug.log" mit Zeitstempeln geführt. # ----------------------------------------------------------------------------- import network import time from machine import Pin import urequests import ntptime import ujson # Globales Dictionary zur Datenspeicherung data = {} # WLAN-Zugangsdaten (bitte anpassen) SSID = "abc" PASSWORD = "123" # REST-Endpunkt des Shelly-Geräts SHELLY_URL = "http://192.168.178.91/rpc/Shelly.GetStatus" # Messintervall in Sekunden (5 Minuten) INTERVAL = 300 # LED an GPIO 2 dient als Statusanzeige (verbunden/nicht verbunden) led = Pin(2, Pin.OUT) # ----------------------------------------------------------------------------- # Funktion: debugLog # Schreibt Zeitstempel + Meldung + Schweregrad in die Datei "debug.log" # ----------------------------------------------------------------------------- def debugLog(text, level="INFO"): try: 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) # ----------------------------------------------------------------------------- # Funktion: connect_wifi # Baut eine WLAN-Verbindung auf und blinkt dabei die LED als Statusanzeige. # ----------------------------------------------------------------------------- def connect_wifi(ssid, password): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print("Verbinde mit WLAN...") wlan.connect(ssid, password) timeout = 10 # Sekunden while not wlan.isconnected() and timeout > 0: led.value(1 - led.value()) # LED blinkt time.sleep(0.5) timeout -= 0.5 print(".", end="") if wlan.isconnected(): led.value(1) debugLog("WLAN verbunden") debugLog("IP-Adresse: " + wlan.ifconfig()[0]) return True else: debugLog("Verbindung fehlgeschlagen.", "ERROR") led.value(0) return False # ----------------------------------------------------------------------------- # Funktion: sync_rtc # Synchronisiert die interne Uhr des ESP32 mit einem NTP-Server. # ----------------------------------------------------------------------------- def sync_rtc(): try: ntptime.settime() debugLog("RTC erfolgreich synchronisiert.") return True except: debugLog("Fehler bei der RTC-Synchronisation.", "ERROR") return False # ----------------------------------------------------------------------------- # Hilfsfunktion: Wandelt Unix-Timestamp in Datum (z. B. für JSON-Key) # ----------------------------------------------------------------------------- def timestamp_to_date(timestamp): try: t = time.localtime(timestamp) return "{:02d}.{:02d}.{:04d}".format(t[2], t[1], t[0]) except: return "00.00.0000" # ----------------------------------------------------------------------------- # Funktion: fetch_shelly_data # Holt aktuellen JSON-Status vom Shelly über die REST-Schnittstelle. # ----------------------------------------------------------------------------- def fetch_shelly_data(): try: response = urequests.get(SHELLY_URL) if response.status_code == 200: json_data = response.json() response.close() return json_data else: debugLog("Fehler beim Abrufen der Daten: " + str(response.status_code), "ERROR") response.close() return None except Exception as e: debugLog(f"Verbindungsfehler: {e}", "ERROR") return None # ----------------------------------------------------------------------------- # Funktion: extract_shelly_values # Extrahiert relevante Felder aus dem JSON-Response. # ----------------------------------------------------------------------------- def extract_shelly_values(data): try: return { "voltage": data["switch:0"]["voltage"], "apower": data["switch:0"]["apower"], "current": data["switch:0"]["current"], "unixtime": data["sys"]["unixtime"] } except KeyError as e: debugLog(f"Fehlender Wert im JSON: {e}", "ERROR") return None # Gibt das aktuelle Datum im Format TT.MM.JJJJ zurück (für JSON-Struktur) def formatDate(): timestamp = time.localtime() return "{:02d}.{:02d}.{:04d}".format(timestamp[2], timestamp[1], timestamp[0]) # ----------------------------------------------------------------------------- # Funktion: read_json_file # Liest bestehende JSON-Datei auf dem Flash-Speicher in das globale Dictionary. # ----------------------------------------------------------------------------- def read_json_file(filename): global data try: with open(filename, 'r') as file: data = ujson.load(file) except OSError: debugLog(f"Datei '{filename}' wurde nicht gefunden.", "ERROR") except ValueError: debugLog(f"Fehler beim Einlesen der Datei '{filename}': Ungültiges JSON-Format.", "ERROR") # ----------------------------------------------------------------------------- # Funktion: add_entry # Fügt einen Datensatz mit Timestamp in das Dictionary ein (Key = Datum) # ----------------------------------------------------------------------------- def add_entry(date_str, timestamp_str, values_dict): global data if date_str not in data: data[date_str] = {} values_dict.pop('unixtime') data[date_str][timestamp_str] = values_dict # ----------------------------------------------------------------------------- # Funktion: save_data_to_file # Speichert das aktualisierte Dictionary in die Datei 'data.json' # ----------------------------------------------------------------------------- def save_data_to_file(): global data try: with open("data.json", "w") as file: ujson.dump(data, file) debugLog("Daten erfolgreich gespeichert.") except OSError: debugLog("Fehler beim Schreiben der Datei.", "ERROR") # ----------------------------------------------------------------------------- # Funktion: internet_available # Prüft, ob der ESP32 eine Verbindung zum Internet (nicht nur WLAN) hat. # ----------------------------------------------------------------------------- def internet_available(): try: response = urequests.get("http://www.google.com") if response.status_code == 200: response.close() debugLog("Internetverbindung verfügbar.") return True else: debugLog("Internetverbindung fehlgeschlagen. Status: " + str(response.status_code), "ERROR") response.close() return False except Exception as e: debugLog(f"Kein Internetzugriff: {e}", "ERROR") return False # ----------------------------------------------------------------------------- # Funktion: send_data_to_thingspeak # Überträgt die Daten an ThingSpeak und entfernt erfolgreich gesendete Einträge. # ----------------------------------------------------------------------------- def send_data_to_thingspeak(): global data THINGSPEAK_API_KEY = "9ZMMDML77HTWPMRR" THINGSPEAK_URL = "https://api.thingspeak.com/update.json" daten_zu_entfernen = [] for datum in list(data.keys()): eintraege = data[datum] gesendete_timestamps = [] for timestamp, werte in eintraege.items(): try: payload = { "api_key": THINGSPEAK_API_KEY, "field1": werte.get("voltage"), "field2": werte.get("apower"), "field3": werte.get("current"), "created_at": int(timestamp) } response = urequests.post(THINGSPEAK_URL, json=payload) if response.status_code == 200: debugLog(f"Datensatz {timestamp} gesendet.") gesendete_timestamps.append(timestamp) else: debugLog(f"Fehler beim Senden von {timestamp}: Status {response.status_code}", "ERROR") response.close() except Exception as e: debugLog(f"Fehler beim Senden von {timestamp}: {e}", "ERROR") for ts in gesendete_timestamps: del data[datum][ts] if not data[datum]: daten_zu_entfernen.append(datum) for d in daten_zu_entfernen: del data[d] save_data_to_file() print("Alle gesendeten Daten wurden aus der Datei entfernt.") # ----------------------------------------------------------------------------- # Hauptprogramm: 5-Minuten-Loop zum Messen, Speichern & ggf. Senden # ----------------------------------------------------------------------------- isWiFiConnected = connect_wifi(SSID, PASSWORD) while True: if isWiFiConnected: if sync_rtc(): shelly_data = fetch_shelly_data() if shelly_data: values = extract_shelly_values(shelly_data) if values: datum = formatDate() read_json_file("data.json") add_entry(datum, str(values['unixtime']), values) save_data_to_file() # Wenn Internet vorhanden → versuche Upload if internet_available(): send_data_to_thingspeak() print("Warte 5 Minuten bis zum nächsten Abruf...") time.sleep(INTERVAL)

Zusätzlich leite ich alle Ausgaben welche auf der Shell ausgegeben werden in eine Datei schreiben, somit kann man zu einem späteren Zeitpunkt nachvollziehen warum ggf. keine Daten gesendet oder gespeichert werden konnten.
[10.03.2025 16:04:38] [INFO] WLAN verbunden
[10.03.2025 16:04:38] [INFO] IP-Adresse: 192.168.178.32
[10.03.2025 15:04:38] [INFO] RTC erfolgreich synchronisiert.
[10.03.2025 15:04:38] [INFO] Daten erfolgreich gespeichert.
[10.03.2025 15:04:39] [INFO] Internetverbindung verfügbar.
[10.03.2025 15:04:40] [INFO] Datensatz 1741619079 gesendet.
[10.03.2025 15:04:40] [INFO] Daten erfolgreich gespeichert.
Hallo Stefan,
tolle Sache, den Vorteil darin sehe ich zu „Shelly Cloud“, das die Werte „lokal“ zwischengespeichert werden, und gehen somit nicht verloren, wenn keine Internetverbindung besteht. (Sh plus1PM,Gen2, usw.. haben keinen Zwischenspeicher ?).
Wäre es als alternative auch machbar, anstelle Daten senden an ThingSpeak, die Daten als CSV.Datei am Desktop zu hinterlegen. ( Es gibt von dir einBlog, Daten in Excel importieren)
Na, ja, mal schaun, ich wag mich mal dran.
– ich verwende den „tasmotizer-1.2.exe“ zum flashen von ESP8266. Könnte dieser anstelle Tonny verwendet werden?
Gruß Matthias
Hi,
die Idee der Erweitung zum export der Daten als CSV Datei klingt gut. Und ist recht einfach machbar als kleine Schnittstelle auf dem ESP32.
Gruß, Stefan