Skip to content

Technik Blog

Programmieren | Arduino | ESP32 | MicroPython | Python | Raspberry Pi | Raspberry Pi Pico

Menu
  • Smarthome
  • Gartenautomation
  • Mikrocontroller
    • Arduino
    • ESP32 & Co.
    • Calliope Mini
    • Raspberry Pi & Pico
  • Solo Mining
  • Über mich
Menu

Shelly mit Node-RED steuern und MQTT-Status auswerten

Veröffentlicht am 29. Juni 202629. Juni 2026 von Stefan Draeger

Im letzten Beitrag „Shelly MQTT einrichten und erste Befehle nutzen“ habe ich dir gezeigt, wie du MQTT auf einem Shelly aktivierst und den Ausgang mit einfachen MQTT-Kommandos steuerst. Für einen ersten Test eignet sich der MQTT Explorer sehr gut. Möchtest du den Shelly jedoch dauerhaft in eigene Automatisierungen einbinden, ist eine Lösung wie Node-RED deutlich komfortabler.

In diesem Beitrag zeige ich dir, wie du deinen Shelly über MQTT mit Node-RED verbindest. Mithilfe des Low-Code-/No-Code-Ansatzes erstellen wir einen einfachen Flow, mit dem sich der Ausgang des Shellys ein-, ausschalten und umschalten lässt. Außerdem empfangen wir die automatisch veröffentlichten Statusmeldungen und bereiten die enthaltenen JSON-Daten so auf, dass einzelne Werte gezielt weiterverarbeitet werden können.

Damit schaffen wir die Grundlage für spätere Automatisierungen, Visualisierungen und die Speicherung von Messwerten – ganz ohne umfangreiche Programmierung.

Shelly + Node-RED: MQTT-Status auswerten und Dashboard erstellen
Dieses Video auf YouTube ansehen.
Inhaltsverzeichnis
  • Das Ziel des Projekts
    • Messwerte des Shelly 1PM empfangen
  • Voraussetzungen
  • Schaltung des Shelly 1PM
  • MQTT-Broker in Node-RED einrichten
  • Aufbau des MQTT-Topics
    • Payload für den Schaltbefehl
  • Shelly mit Inject-Nodes über MQTT steuern
  • MQTT-Statusmeldungen in Node-RED empfangen
    • Topic der Statusmeldungen
  • Warum mehrere MQTT-Nachrichten zusammengeführt werden müssen
  • MQTT-Werte im Flow-Context zusammenführen
    • Welche Nachrichten werden verarbeitet?
    • Vorhandene Werte laden
    • Werte gezielt aktualisieren
    • Energiewerte übernehmen
    • Zeitstempel der letzten Nachricht speichern
    • Vollständiger Code der Function-Node
    • Ergebnis im Flow-Context
  • Shelly-Dashboard mit Node-RED erstellen
    • Messwerte im Dashboard anzeigen
    • Verbraucher über das Dashboard schalten
    • Energieverbrauch der letzten drei Minuten
    • Zeitpunkt der letzten MQTT-Nachricht
    • Helles und dunkles Theme
  • Fazit und Ausblick

Das Ziel des Projekts

In diesem Projekt zeige ich dir praxisnah, wie du deinen Shelly mit Node-RED über MQTT steuerst und Statusmeldungen auswertest – vollständig lokal und ohne Shelly Cloud.

Mit Node-RED kannst du dir mit etwas Aufwand ein eigenes Smart-Home-System mit Automatisierungen, Messwerten und einer Bedienoberfläche aufbauen. Die Dashboard-Nodes bieten dafür bereits viele Möglichkeiten, die ich in einem separaten Beitrag vorgestellt habe.

Node-RED Dashboard mit ausgeschalteter Lampe am Shelly 1PM
Der Schalter im Node-RED Dashboard befindet sich im Zustand „AUS“. Der Shelly 1PM hat die angeschlossene Lampe ausgeschaltet.
Node-RED Dashboard mit eingeschalteter Lampe am Shelly 1PM
Der Schalter im Node-RED Dashboard befindet sich im Zustand „EIN“. Der Shelly 1PM hat die angeschlossene Lampe eingeschaltet.

Messwerte des Shelly 1PM empfangen

Da ich für dieses Projekt einen Shelly 1PM verwende, kann ich den angeschlossenen Verbraucher nicht nur schalten, sondern auch dessen Energieverbrauch erfassen. Der Shelly misst die Stromaufnahme kontinuierlich und veröffentlicht die aktualisierten Energiewerte automatisch über MQTT.

Bei meinem Gerät wird ungefähr einmal pro Minute eine neue Statusmeldung an den MQTT-Broker gesendet. Diese Nachricht enthält unter anderem den bisherigen Gesamtverbrauch sowie die Verbrauchswerte der letzten Minuten. In Node-RED können wir das entsprechende Topic abonnieren, die empfangene JSON-Nachricht auswerten und die einzelnen Messwerte anschließend weiterverarbeiten.

MQTT Explorer mit empfangener Statusmeldung eines Shelly 1PM Gen3
Der MQTT Explorer zeigt eine automatisch vom Shelly veröffentlichte Statusmeldung im Topic events/rpc.

Voraussetzungen

Wenn du das Projekt nachbauen möchtest, benötigst du folgende Komponenten und Programme:

  • einen Shelly 1PM* oder Shelly 1PM Mini*,
  • eine Lampe als Verbraucher,
  • geeignetes Werkzeug und Material für die Verdrahtung,
  • einen MQTT-Broker, beispielsweise Eclipse Mosquitto,
  • den MQTT Explorer zum Testen und Debuggen,
  • eine installierte Node-RED-Umgebung.
Shelly 1PM Mini Gen3

In meinem Fall laufen Node-RED und der MQTT-Broker auf einem Dell OptiPlex 3050. Du kannst dafür aber ebenso einen Raspberry Pi, Orange Pi oder einen anderen kleinen Server verwenden.

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!

⚠️ Wichtiger Sicherheitshinweis

Der Shelly 1PM wird mit 230 V Netzspannung betrieben. Arbeiten an Netzspannung sind lebensgefährlich und dürfen ausschließlich von entsprechend qualifiziertem Fachpersonal durchgeführt werden.

Beachte dabei immer die fünf Sicherheitsregeln:

  1. Freischalten
  2. Gegen Wiedereinschalten sichern
  3. Spannungsfreiheit feststellen
  4. Erden und kurzschließen
  5. Benachbarte, unter Spannung stehende Teile abdecken oder abschranken

Die Spannungsfreiheit muss mit einem geeigneten zweipoligen Spannungsprüfer, beispielsweise einem Duspol, festgestellt werden. Ein einpoliger Phasenprüfer, umgangssprachlich auch „Lügenstift“ genannt, ist dafür nicht geeignet.

zwei poliger Spannungsprüfer (Duspol), Phasenprüfer und berührungsloser Spannungsprüfer

Im Zweifel solltest du die Installation von einer Elektrofachkraft durchführen lassen.

Schaltung des Shelly 1PM

Die Verdrahtung ist recht einfach aufgebaut:

  • An der Schraubklemme O wird der geschaltete Außenleiter zur Lampe angeschlossen.
  • Der Neutralleiter der Lampe wird direkt mit dem Neutralleiter der Zuleitung verbunden. Dafür eignet sich beispielsweise eine dreifache WAGO-Klemme.
  • Der Außenleiter der Zuleitung wird an der Klemme L des Shelly angeschlossen.
  • Der Neutralleiter der Zuleitung wird mit der Klemme N verbunden.

Die Klemme O ist der geschaltete Ausgang des Shelly. Wird das Relais aktiviert, liegt dort die Netzspannung für die angeschlossene Lampe an. Die Klemme SW wird in diesem Aufbau nicht benötigt, da die Steuerung ausschließlich über MQTT und Node-RED erfolgt.

Schaltplan eines Shelly 1PM mit Netzanschluss und angeschlossener Lampe

MQTT-Broker in Node-RED einrichten

Damit Node-RED mit dem Shelly kommunizieren kann, benötigen wir eine Verbindung zu einem MQTT-Broker. In meinem Fall läuft Eclipse Mosquitto in einem Docker-Container auf einem Dell OptiPlex 3050.

Wie du einen MQTT-Broker mit Docker einrichtest, zeige ich dir ausführlich in meinem Beitrag „MQTT einfach erklärt: Was ist MQTT und warum ist es perfekt für IoT?“. Die dort gezeigte Einrichtung funktioniert genauso gut auf einem Raspberry Pi oder einem anderen kleinen Linux-Server.

Konfiguration der Verbindung zu einem MQTT-Broker in Node-RED

Ziehe anschließend eine MQTT-Out-Node in den Arbeitsbereich und öffne ihre Einstellungen. Über das Stiftsymbol neben dem Feld „Server“ legst du eine neue Broker-Verbindung an.

Trage dort die IP-Adresse oder den Hostnamen deines MQTT-Brokers sowie den verwendeten Port ein. In meinem Fall verwende ich 192.168.178.186:1883

Falls dein MQTT-Broker durch einen Benutzernamen und ein Passwort geschützt ist, hinterlegst du die Zugangsdaten ebenfalls in der Konfiguration.

Nach dem Speichern und Bereitstellen des Flows sollte unter der MQTT-Node der Status „Verbunden“ erscheinen.

Aufbau des MQTT-Topics

Damit der Shelly weiß, welcher Ausgang geschaltet werden soll, müssen wir in der MQTT-Out-Node das passende Topic eintragen. Dieses setzt sich aus dem Topic-Präfix des Shellys und dem gewünschten Befehl zusammen:

<Topic-Präfix>/command/switch:0

Bei meinem Shelly 1PM Gen3 lautet das Topic beispielsweise: shelly1pmg3-3030f9e95140/command/switch:0

Der erste Teil ist der MQTT-Topic-Präfix des Geräts. Standardmäßig entspricht dieser meist der Geräte-ID des Shellys, kann in den MQTT-Einstellungen jedoch individuell angepasst werden.

Der Abschnitt command/switch:0 legt fest, dass ein Befehl an den ersten Schaltausgang gesendet wird. Die Zahl hinter dem Doppelpunkt steht für den jeweiligen Kanal:

  • switch:0 = erster Ausgang
  • switch:1 = zweiter Ausgang

Beim Shelly 1PM ist nur ein Ausgang vorhanden, weshalb wir switch:0 verwenden. Ein Shelly 2PM besitzt dagegen zwei getrennte Relais. Dort werden die beiden Ausgänge mit switch:0 und switch:1 angesprochen.

Payload für den Schaltbefehl

Neben dem MQTT-Topic wird auch eine passende Payload benötigt. Sie enthält den eigentlichen Befehl für den Shelly-Ausgang.

Für die einfache Steuerung stehen unter anderem folgende Werte zur Verfügung:

PayloadFunktion
onAusgang einschalten
offAusgang ausschalten
toggleAktuellen Zustand umschalten
on,3Ausgang einschalten und nach 3 Sekunden wieder ausschalten
off,3Ausgang ausschalten und nach 3 Sekunden wieder einschalten

Mit der Payload on,3 wird der Ausgang also eingeschaltet und nach drei Sekunden automatisch wieder ausgeschaltet.

Konfiguration einer Inject-Node in Node-RED mit der MQTT-Payload zum Schalten eines Shelly
In der Inject-Node wird die gewünschte Payload wie on, off oder toggle für den Shelly-Ausgang eingetragen.

Für die Steuerung mit Node-RED tragen wir diese Werte später beispielsweise in Inject-Nodes oder direkt in einem Dashboard-Schalter ein.

Shelly mit Inject-Nodes über MQTT steuern

Nachdem die Verbindung zum MQTT-Broker eingerichtet sowie Topic und Payload bekannt sind, können wir den ersten einfachen Flow erstellen.

Dafür benötigen wir mehrere Inject-Nodes und eine MQTT-Out-Node. Jede Inject-Node erhält eine andere Payload, beispielsweise on, off oder toggle. Alle Inject-Nodes werden anschließend mit derselben MQTT-Out-Node verbunden.

In der MQTT-Out-Node tragen wir das Topic des Shelly-Ausgangs ein:

shelly1pmg3-3030f9e95140/command/switch:0

Der vollständige Flow ist damit sehr einfach aufgebaut:

Einfacher Node-RED-Flow mit Inject-Nodes und MQTT-Out-Node zur Steuerung eines Shelly 1PM
Die Inject-Nodes senden die MQTT-Payloads on, off und toggle über eine MQTT-Out-Node an den Shelly 1PM.

Nachdem du den Flow über die Schaltfläche „Übernehmen“ bereitgestellt hast, kannst du die Inject-Nodes einzeln auslösen. Der Shelly sollte den jeweiligen Befehl unmittelbar empfangen und die angeschlossene Lampe entsprechend ein-, ausschalten oder umschalten.

Unterhalb der MQTT-Out-Node sollte außerdem der Status „Verbunden“ angezeigt werden. Reagiert der Shelly nicht, solltest du zunächst die Verbindung zum MQTT-Broker, den Topic-Präfix und die eingetragene Payload überprüfen.

MQTT-Statusmeldungen in Node-RED empfangen

Nachdem wir den Shelly über eine MQTT-Out-Node steuern können, benötigen wir für den Rückweg eine MQTT-In-Node. Diese Node abonniert ein MQTT-Topic und gibt jede dort veröffentlichte Nachricht an den nachfolgenden Flow weiter.

Ziehe dazu eine MQTT-In-Node in den Arbeitsbereich und öffne ihre Einstellungen. Als Broker wählst du dieselbe Verbindung aus, die du bereits für die MQTT-Out-Node eingerichtet hast.

Topic der Statusmeldungen

Die automatischen Statusänderungen veröffentlicht der Shelly über folgendes Topic:

<Topic-Präfix>/events/rpc

Bei meinem Shelly 1PM Gen3 lautet es:

shelly1pmg3-3030f9e95140/events/rpc

Trage dieses Topic in die MQTT-In-Node ein. Die Node abonniert damit alle automatisch veröffentlichten NotifyStatus– und NotifyEvent-Nachrichten dieses Shellys.

Für einen ersten Test verbindest du die MQTT-In-Node direkt mit einer Debug-Node:

Node-RED-Flow mit MQTT-In-Node und Debug-Node zum Empfangen von Shelly-MQTT-Statusmeldungen
Die MQTT-In-Node empfängt die Statusmeldungen des Shelly und gibt sie zur Kontrolle an eine Debug-Node weiter.
Node-RED Flow zum Importieren
[
    {
        "id": "93d7025accf24a2c",
        "type": "tab",
        "label": "einfacher Flow mit MQTT IN Node",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "dc21707a81d02e0e",
        "type": "mqtt in",
        "z": "93d7025accf24a2c",
        "name": "Shelly events/rpc",
        "topic": "shelly1pmg3-3030f9e95140/events/rpc",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "mqtt_broker_shelly",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 120,
        "y": 80,
        "wires": [
            [
                "25b58994c75b18d2"
            ]
        ]
    },
    {
        "id": "25b58994c75b18d2",
        "type": "debug",
        "z": "93d7025accf24a2c",
        "name": "Ausgabe der Nachricht vom Shelly",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 380,
        "y": 80,
        "wires": []
    },
    {
        "id": "mqtt_broker_shelly",
        "type": "mqtt-broker",
        "name": "Lokaler MQTT-Broker",
        "broker": "192.168.178.186",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {}
    }
]

Nachdem du den Flow deployed hast, sollten die vom Shelly gesendeten Nachrichten im Debug-Fenster erscheinen.

MQTT Nachricht #1 vom Shelly 1PM beim einschalten
MQTT Nachricht #2 vom Shelly 1PM beim einschalten
MQTT Nachricht #3 vom Shelly 1PM beim einschalten

Bei meinem Test folgte rund eine Minute später eine weitere Statusmeldung mit den Energiewerten der letzten drei Minuten. Diese Meldung wurde anschließend regelmäßig aktualisiert.

MQTT Nachricht nach einer Minute vom Shelly 1PM nach dem einschalten

Warum mehrere MQTT-Nachrichten zusammengeführt werden müssen

Beim Ein- oder Ausschalten des Shelly werden die verfügbaren Statuswerte nicht gemeinsam in einer einzigen MQTT-Nachricht übertragen. Stattdessen veröffentlicht das Gerät mehrere NotifyStatus-Meldungen, die jeweils nur die Werte enthalten, die sich gerade geändert haben.

Beim Einschalten meines Shelly 1PM wurde zunächst der neue Schaltzustand übertragen:

{
  "output": true
}

Kurz danach folgte eine weitere Nachricht mit der aktuellen Leistung:

{
  "apower": 5.8
}

Anschließend wurde die gemessene Stromstärke separat veröffentlicht:

{
  "current": 0.055
}

Die Energiewerte unter aenergy wurden erst knapp eine Minute später übertragen. Diese Nachricht enthielt unter anderem den Gesamtverbrauch sowie die Energieverbräuche der letzten drei Minuten.

Eine einzelne MQTT-Nachricht enthält daher nicht alle Werte, die wir später gemeinsam im Dashboard anzeigen möchten. Aus diesem Grund verwenden wir eine Function-Node und den Flow-Context.

Die Function-Node prüft jede eintreffende Nachricht und aktualisiert nur die darin enthaltenen Eigenschaften. Bereits empfangene Werte bleiben im Flow-Context gespeichert, bis sie durch neue Werte ersetzt werden.

Auf diese Weise entsteht nach und nach ein gemeinsamer Datensatz mit:

  • Schaltzustand,
  • aktueller Leistung,
  • Stromstärke,
  • Gesamtverbrauch,
  • Energiewerten der letzten drei Minuten,
  • Zeitstempel der letzten Nachricht.

Die Werte werden nicht innerhalb eines festen Zeitfensters gesammelt. Stattdessen bleiben sie im Flow-Context erhalten und werden bei jeder neuen NotifyStatus-Meldung ergänzt oder aktualisiert.

MQTT-Werte im Flow-Context zusammenführen

Nachdem wir gesehen haben, dass der Shelly seine Werte in mehreren getrennten NotifyStatus-Nachrichten sendet, müssen wir diese Informationen in Node-RED zu einem gemeinsamen Datensatz zusammenführen.

Node-RED Flow zum Importieren
[
    {
        "id": "a2fdbbd67b270e6b",
        "type": "tab",
        "label": "Shelly MQTT-Status zusammenführen",
        "disabled": false,
        "info": "Empfängt NotifyStatus-Meldungen eines Shelly 1PM, führt Teilwerte im Flow-Context zusammen und gibt den gemeinsamen Datensatz aus.",
        "env": []
    },
    {
        "id": "085374c7522be9be",
        "type": "mqtt in",
        "z": "a2fdbbd67b270e6b",
        "name": "Shelly events/rpc",
        "topic": "shelly1pmg3-3030f9e95140/events/rpc",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "mqtt_broker_local",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 510,
        "y": 540,
        "wires": [
            [
                "2e98451d7d17dbc6"
            ]
        ]
    },
    {
        "id": "2e98451d7d17dbc6",
        "type": "function",
        "z": "a2fdbbd67b270e6b",
        "name": "Statuswerte zusammenführen",
        "func": "let data = msg.payload;\n\n// Falls die MQTT-Nachricht noch als Text vorliegt\nif (typeof data === \"string\") {\n    try {\n        data = JSON.parse(data);\n    } catch (error) {\n        node.warn(\"Ungültige JSON-Nachricht empfangen.\");\n        return null;\n    }\n}\n\n// Nur relevante NotifyStatus-Nachrichten verarbeiten\nif (\n    data?.method !== \"NotifyStatus\" ||\n    !data?.params?.[\"switch:0\"]\n) {\n    return null;\n}\n\nconst switchData = data.params[\"switch:0\"];\n\n// Bereits gespeicherte Werte laden\nconst values = flow.get(\"shellyValues\") || {};\n\n// Zeitpunkt der zuletzt empfangenen Statusmeldung\nif (typeof data.params.ts === \"number\") {\n    values.timestamp = data.params.ts;\n}\n\n// Schaltzustand\nif (typeof switchData.output === \"boolean\") {\n    values.output = switchData.output;\n}\n\n// Aktuelle Leistung in Watt\nif (typeof switchData.apower === \"number\") {\n    values.powerW = switchData.apower;\n}\n\n// Stromstärke in Ampere\nif (typeof switchData.current === \"number\") {\n    values.currentA = switchData.current;\n}\n\n// Energiewerte\nif (switchData.aenergy) {\n    const energy = switchData.aenergy;\n    const byMinute = Array.isArray(energy.by_minute)\n        ? energy.by_minute\n        : [];\n\n    values.energyTotalWh = energy.total ?? null;\n    values.energyByMinute = byMinute;\n    values.minuteTimestamp = energy.minute_ts ?? null;\n\n    values.lastMinute = byMinute[0] ?? null;\n    values.twoMinutesAgo = byMinute[1] ?? null;\n    values.threeMinutesAgo = byMinute[2] ?? null;\n}\n\n// Zurückgespeiste Energie\nif (switchData.ret_aenergy) {\n    values.returnedEnergyTotalWh =\n        switchData.ret_aenergy.total ?? null;\n}\n\n// Aktualisierten Datensatz speichern\nflow.set(\"shellyValues\", values);\n\n// Datensatz an nachfolgende Nodes ausgeben\nmsg.payload = values;\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 810,
        "y": 540,
        "wires": [
            [
                "7934b43151d33dee",
                "77e49c5368f0b409"
            ]
        ]
    },
    {
        "id": "7934b43151d33dee",
        "type": "debug",
        "z": "a2fdbbd67b270e6b",
        "name": "Zusammengeführter Datensatz",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1130,
        "y": 510,
        "wires": []
    },
    {
        "id": "77e49c5368f0b409",
        "type": "change",
        "z": "a2fdbbd67b270e6b",
        "name": "Flow-Context anzeigen",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "shellyValues",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1100,
        "y": 570,
        "wires": [
            [
                "73f6e9ed4fb7c939"
            ]
        ]
    },
    {
        "id": "73f6e9ed4fb7c939",
        "type": "debug",
        "z": "a2fdbbd67b270e6b",
        "name": "Flow-Context shellyValues",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1360,
        "y": 570,
        "wires": []
    },
    {
        "id": "0ea119799a56682c",
        "type": "inject",
        "z": "a2fdbbd67b270e6b",
        "name": "Flow-Context zurücksetzen",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 550,
        "y": 640,
        "wires": [
            [
                "38e619d1e40e8815"
            ]
        ]
    },
    {
        "id": "38e619d1e40e8815",
        "type": "function",
        "z": "a2fdbbd67b270e6b",
        "name": "shellyValues löschen",
        "func": "flow.set(\"shellyValues\", {});\nmsg.payload = {};\nnode.status({\n    fill: \"grey\",\n    shape: \"ring\",\n    text: \"Flow-Context geleert\"\n});\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 810,
        "y": 640,
        "wires": [
            [
                "d91b07d2781dc3c8"
            ]
        ]
    },
    {
        "id": "d91b07d2781dc3c8",
        "type": "debug",
        "z": "a2fdbbd67b270e6b",
        "name": "Zurückgesetzter Context",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1100,
        "y": 640,
        "wires": []
    },
    {
        "id": "mqtt_broker_local",
        "type": "mqtt-broker",
        "name": "Lokaler MQTT-Broker",
        "broker": "192.168.178.186",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {}
    }
]

Dafür verwenden wir eine Function-Node und den Flow-Context. Der Flow-Context dient als Zwischenspeicher innerhalb des aktuellen Node-RED-Flows. Bereits empfangene Werte bleiben dort erhalten und können bei jeder neuen MQTT-Nachricht ergänzt oder aktualisiert werden.

Welche Nachrichten werden verarbeitet?

Zunächst prüfen wir, ob es sich überhaupt um eine Statusmeldung handelt. Die für dieses Projekt relevanten Nachrichten enthalten:

{
  "method": "NotifyStatus"
}

Zusätzlich muss der Bereich switch:0 vorhanden sein, da sich dort die Werte des ersten Schaltausgangs befinden.

Eine eingehende Nachricht kann beispielsweise nur den Schaltzustand enthalten:

{
  "output": true
}

Eine andere Nachricht liefert lediglich die aktuelle Leistung:

{
  "apower": 5.8
}

Weitere Nachrichten können unter anderem current oder aenergy enthalten.

Die Function-Node muss daher immer prüfen, welche Eigenschaften in der aktuellen Nachricht tatsächlich vorhanden sind.

Vorhandene Werte laden

Zu Beginn laden wir die bereits gespeicherten Daten aus dem Flow-Context:

const values = flow.get("shellyValues") || {};

Existiert noch kein Datensatz, wird ein neues leeres Objekt angelegt.

Treffen später weitere Nachrichten ein, enthält values bereits die zuvor gespeicherten Informationen. So bleiben beispielsweise Schaltzustand und Leistung erhalten, auch wenn die aktuelle MQTT-Nachricht nur neue Energiewerte enthält.

Werte gezielt aktualisieren

Für jede mögliche Eigenschaft prüfen wir, ob sie in der aktuellen Nachricht vorhanden ist.

Beim Schaltzustand verwenden wir:

if (typeof switchData.output === "boolean") {
    values.output = switchData.output;
}

Die Prüfung mit typeof ist wichtig, weil auch false ein gültiger Wert ist.

Für Leistung, Spannung und Stromstärke gehen wir ähnlich vor:

if (typeof switchData.apower === "number") {
    values.powerW = switchData.apower;
}

if (typeof switchData.current === "number") {
    values.currentA = switchData.current;
}

Auch der Wert 0 wird dadurch korrekt übernommen.

Energiewerte übernehmen

Enthält die Nachricht den Bereich aenergy, können wir den Gesamtverbrauch und die Werte der letzten drei Minuten speichern:

if (switchData.aenergy) {
    values.energyTotalWh = switchData.aenergy.total;
    values.energyByMinute = switchData.aenergy.by_minute;
    values.minuteTimestamp = switchData.aenergy.minute_ts;
}

Das Array energyByMinute enthält die drei zuletzt gemeldeten Minutenwerte.

Zusätzlich kann der Shelly unter ret_aenergy eine zurückgespeiste Energiemenge liefern:

if (switchData.ret_aenergy) {
    values.returnedEnergyTotalWh =
        switchData.ret_aenergy.total;
}

Für einen normalen Verbraucher bleibt dieser Wert in der Regel bei 0.

Zeitstempel der letzten Nachricht speichern

Jede NotifyStatus-Nachricht enthält unter params.ts einen Zeitstempel. Diesen speichern wir als Zeitpunkt der zuletzt empfangenen Statusmeldung:

values.timestamp = data.params.ts ?? values.timestamp;

Der Wert kann später im Dashboard formatiert angezeigt werden. Dadurch lässt sich erkennen, wann zuletzt eine Meldung vom Shelly eingetroffen ist.

Vollständiger Code der Function-Node

In die Function-Node kannst du folgenden Code einfügen:

let data = msg.payload;

// Falls die MQTT-Nachricht als Text vorliegt
if (typeof data === "string") {
    try {
        data = JSON.parse(data);
    } catch (error) {
        node.warn("Ungültige JSON-Nachricht empfangen.");
        return null;
    }
}

// Nur relevante NotifyStatus-Nachrichten verarbeiten
if (
    data?.method !== "NotifyStatus" ||
    !data?.params?.["switch:0"]
) {
    return null;
}

const switchData = data.params["switch:0"];

// Bereits gespeicherte Werte laden
const values = flow.get("shellyValues") || {};

// Zeitpunkt der zuletzt empfangenen Statusmeldung
if (typeof data.params.ts === "number") {
    values.timestamp = data.params.ts;
}

// Schaltzustand
if (typeof switchData.output === "boolean") {
    values.output = switchData.output;
}

// Aktuelle Leistung in Watt
if (typeof switchData.apower === "number") {
    values.powerW = switchData.apower;
}

// Stromstärke in Ampere
if (typeof switchData.current === "number") {
    values.currentA = switchData.current;
}

// Energiewerte
if (switchData.aenergy) {
    const energy = switchData.aenergy;
    const byMinute = Array.isArray(energy.by_minute)
        ? energy.by_minute
        : [];

    values.energyTotalWh = energy.total ?? null;
    values.energyByMinute = byMinute;
    values.minuteTimestamp = energy.minute_ts ?? null;

    values.lastMinute = byMinute[0] ?? null;
    values.twoMinutesAgo = byMinute[1] ?? null;
    values.threeMinutesAgo = byMinute[2] ?? null;
}

// Zurückgespeiste Energie
if (switchData.ret_aenergy) {
    values.returnedEnergyTotalWh =
        switchData.ret_aenergy.total ?? null;
}

// Aktualisierten Datensatz speichern
flow.set("shellyValues", values);

// Datensatz an nachfolgende Nodes ausgeben
msg.payload = values;

return msg;

Ergebnis im Flow-Context

Nach mehreren empfangenen MQTT-Nachrichten kann der gemeinsame Datensatz beispielsweise so aussehen:

{
  "timestamp": 1782652080,
  "output": true,
  "powerW": 5.8,
  "currentA": 0.055,
  "energyTotalWh": 40.809,
  "energyByMinute": [
    0,
    202.254,
    202.254
  ],
  "lastMinute": 0,
  "twoMinutesAgo": 202.254,
  "threeMinutesAgo": 202.254,
  "returnedEnergyTotalWh": 0
}

Shelly-Dashboard mit Node-RED erstellen

Nachdem wir die MQTT-Statusmeldungen des Shelly ausgewertet und die einzelnen Messwerte im Flow-Context zusammengeführt haben, können wir die Daten in einem übersichtlichen Dashboard darstellen. Dafür verwende ich die Nodes aus dem Paket @flowfuse/node-red-dashboard.

Node-RED Flow zum Importieren
[
    {
        "id": "tab_shelly_dashboard_full",
        "type": "tab",
        "label": "Shelly 1PM Dashboard",
        "disabled": false,
        "info": "FlowFuse Dashboard 2.0 für Shelly 1PM: Spannung, Strom, Leistung, Schalten, letzte drei Minuten und Zeitstempel.",
        "env": []
    },
    {
        "id": "mqtt_in_events",
        "type": "mqtt in",
        "z": "tab_shelly_dashboard_full",
        "name": "Shelly events/rpc",
        "topic": "shelly1pmg3-3030f9e95140/events/rpc",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "mqtt_broker_shelly",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 150,
        "y": 100,
        "wires": [
            [
                "fn_collect_shelly"
            ]
        ]
    },
    {
        "id": "mqtt_in_status",
        "type": "mqtt in",
        "z": "tab_shelly_dashboard_full",
        "name": "Shelly status/switch:0",
        "topic": "shelly1pmg3-3030f9e95140/status/switch:0",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "mqtt_broker_shelly",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 170,
        "y": 160,
        "wires": [
            [
                "fn_collect_shelly"
            ]
        ]
    },
    {
        "id": "fn_collect_shelly",
        "type": "function",
        "z": "tab_shelly_dashboard_full",
        "name": "Shelly-Werte zusammenführen",
        "func": "let data = msg.payload;\n\nif (typeof data === \"string\") {\n    try {\n        data = JSON.parse(data);\n    } catch (error) {\n        node.warn(\"Ungültige JSON-Nachricht empfangen\");\n        return null;\n    }\n}\n\nlet switchData;\nlet timestamp;\n\nif (data?.method === \"NotifyStatus\" && data?.params?.[\"switch:0\"]) {\n    switchData = data.params[\"switch:0\"];\n    timestamp = data.params.ts;\n} else if (typeof data === \"object\" && data !== null && (typeof data.output === \"boolean\" || typeof data.voltage === \"number\" || typeof data.current === \"number\" || typeof data.apower === \"number\")) {\n    switchData = data;\n    timestamp = Date.now() / 1000;\n} else {\n    return null;\n}\n\nconst values = flow.get(\"shellyValues\") || {};\nvalues.timestamp = timestamp ?? values.timestamp;\nif (typeof switchData.output === \"boolean\") values.output = switchData.output;\nif (typeof switchData.apower === \"number\") values.powerW = switchData.apower;\nif (typeof switchData.voltage === \"number\") values.voltageV = switchData.voltage;\nif (typeof switchData.current === \"number\") values.currentA = switchData.current;\nif (switchData.aenergy) {\n    values.energyTotalWh = switchData.aenergy.total;\n    values.energyByMinute = switchData.aenergy.by_minute;\n    values.minuteTimestamp = switchData.aenergy.minute_ts;\n}\nif (switchData.ret_aenergy) values.returnedEnergyTotalWh = switchData.ret_aenergy.total;\nflow.set(\"shellyValues\", values);\nmsg.payload = values;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 450,
        "y": 130,
        "wires": [
            [
                "ui_template_shelly_dashboard",
                "debug_combined"
            ]
        ]
    },
    {
        "id": "mqtt_out_command",
        "type": "mqtt out",
        "z": "tab_shelly_dashboard_full",
        "name": "Shelly schalten",
        "topic": "shelly1pmg3-3030f9e95140/command/switch:0",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "mqtt_broker_shelly",
        "x": 1100,
        "y": 240,
        "wires": []
    },
    {
        "id": "inject_status_update",
        "type": "inject",
        "z": "tab_shelly_dashboard_full",
        "name": "Status beim Start anfordern",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 1,
        "topic": "",
        "payload": "status_update",
        "payloadType": "str",
        "x": 190,
        "y": 240,
        "wires": [
            [
                "mqtt_out_command"
            ]
        ]
    },
    {
        "id": "debug_combined",
        "type": "debug",
        "z": "tab_shelly_dashboard_full",
        "name": "Zusammengeführte Shelly-Werte",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 760,
        "y": 210,
        "wires": []
    },
    {
        "id": "ui_template_shelly_dashboard",
        "type": "ui-template",
        "z": "tab_shelly_dashboard_full",
        "group": "ui_group_custom_dashboard",
        "page": "",
        "ui": "",
        "name": "Styled Shelly Dashboard",
        "order": 1,
        "width": 12,
        "height": 0,
        "head": "",
        "format": "\n<template>\n  <div class=\"shelly-dashboard\" :class=\"{ dark: darkMode }\">\n    <div class=\"dashboard-header\">\n      <div>\n        <p class=\"eyebrow\">SMART HOME · MQTT</p>\n        <h1>Shelly 1PM Dashboard</h1>\n        <p class=\"subtitle\">Lokale Steuerung und Live-Messwerte</p>\n      </div>\n\n      <button class=\"theme-toggle\" type=\"button\" @click=\"toggleTheme\">\n        <span class=\"theme-icon\">{{ darkMode ? '☾' : '☀' }}</span>\n        <span>{{ darkMode ? 'Dunkel' : 'Hell' }}</span>\n        <span class=\"toggle-track\" :class=\"{ active: darkMode }\">\n          <span class=\"toggle-thumb\"></span>\n        </span>\n      </button>\n    </div>\n\n    <section class=\"metric-grid\">\n      <article class=\"metric-card voltage\">\n        <div class=\"metric-icon\">V</div>\n        <div>\n          <span class=\"metric-label\">Spannung</span>\n          <strong>{{ numberValue(values.voltageV, 1) }}</strong>\n          <small>Volt</small>\n        </div>\n      </article>\n\n      <article class=\"metric-card current\">\n        <div class=\"metric-icon\">A</div>\n        <div>\n          <span class=\"metric-label\">Strom</span>\n          <strong>{{ numberValue(values.currentA, 3) }}</strong>\n          <small>Ampere</small>\n        </div>\n      </article>\n\n      <article class=\"metric-card power\">\n        <div class=\"metric-icon\">W</div>\n        <div>\n          <span class=\"metric-label\">Leistung</span>\n          <strong>{{ numberValue(values.powerW, 1) }}</strong>\n          <small>Watt</small>\n        </div>\n      </article>\n    </section>\n\n    <section class=\"panel control-panel\">\n      <div>\n        <span class=\"panel-kicker\">STEUERUNG</span>\n        <h2>Verbraucher</h2>\n        <p>Schaltet den Ausgang des Shelly 1PM über MQTT.</p>\n      </div>\n\n      <button\n        class=\"power-switch\"\n        :class=\"{ on: isOn }\"\n        type=\"button\"\n        @click=\"toggleConsumer\"\n        :aria-pressed=\"isOn\"\n      >\n        <span class=\"power-dot\"></span>\n        <span>{{ isOn ? 'EIN' : 'AUS' }}</span>\n      </button>\n    </section>\n\n    <section class=\"panel\">\n      <div class=\"section-heading\">\n        <div>\n          <span class=\"panel-kicker\">ENERGIEVERBRAUCH</span>\n          <h2>Letzte drei Minuten</h2>\n        </div>\n        <span class=\"unit-badge\">mWh</span>\n      </div>\n\n      <div class=\"minute-grid\">\n        <article class=\"minute-card minute-1\">\n          <span>Letzte Minute</span>\n          <strong>{{ minuteValue(0) }}</strong>\n        </article>\n        <article class=\"minute-card minute-2\">\n          <span>Vor zwei Minuten</span>\n          <strong>{{ minuteValue(1) }}</strong>\n        </article>\n        <article class=\"minute-card minute-3\">\n          <span>Vor drei Minuten</span>\n          <strong>{{ minuteValue(2) }}</strong>\n        </article>\n      </div>\n\n      <div class=\"last-message\">\n        <span class=\"status-dot\"></span>\n        <span>Letzte Nachricht</span>\n        <strong>{{ formattedTimestamp }}</strong>\n      </div>\n    </section>\n  </div>\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      values: {},\n      darkMode: false\n    }\n  },\n  computed: {\n    isOn() {\n      return this.values?.output === true\n    },\n    formattedTimestamp() {\n      const timestamp = this.values?.timestamp\n      if (typeof timestamp !== 'number') {\n        return 'Noch keine Nachricht empfangen'\n      }\n\n      const date = new Date(timestamp * 1000)\n      const parts = new Intl.DateTimeFormat('de-DE', {\n        timeZone: 'Europe/Berlin',\n        hour: '2-digit',\n        minute: '2-digit',\n        second: '2-digit',\n        day: '2-digit',\n        month: '2-digit',\n        year: 'numeric',\n        hour12: false\n      }).formatToParts(date)\n\n      const get = type => parts.find(part => part.type === type)?.value ?? ''\n      return `${get('hour')}:${get('minute')}:${get('second')} ${get('day')}.${get('month')}.${get('year')}`\n    }\n  },\n  watch: {\n    msg: {\n      handler() {\n        if (this.msg?.payload && typeof this.msg.payload === 'object') {\n          this.values = { ...this.msg.payload }\n        }\n      },\n      deep: true,\n      immediate: true\n    }\n  },\n  mounted() {\n    this.darkMode = localStorage.getItem('shelly-dashboard-theme') === 'dark'\n  },\n  methods: {\n    toggleTheme() {\n      this.darkMode = !this.darkMode\n      localStorage.setItem(\n        'shelly-dashboard-theme',\n        this.darkMode ? 'dark' : 'light'\n      )\n    },\n    toggleConsumer() {\n      const command = this.isOn ? 'off' : 'on'\n      this.send({ payload: command })\n    },\n    numberValue(value, digits) {\n      if (typeof value !== 'number' || Number.isNaN(value)) {\n        return '–'\n      }\n      return value.toLocaleString('de-DE', {\n        minimumFractionDigits: digits,\n        maximumFractionDigits: digits\n      })\n    },\n    minuteValue(index) {\n      const values = this.values?.energyByMinute\n      if (!Array.isArray(values) || typeof values[index] !== 'number') {\n        return '–'\n      }\n      return values[index].toLocaleString('de-DE', {\n        minimumFractionDigits: 3,\n        maximumFractionDigits: 3\n      })\n    }\n  }\n}\n</script>\n\n<style>\n.shelly-dashboard {\n  --bg: #eef4ff;\n  --surface: rgba(255,255,255,0.88);\n  --surface-solid: #ffffff;\n  --text: #172033;\n  --muted: #64748b;\n  --border: rgba(148,163,184,0.22);\n  --shadow: 0 18px 42px rgba(15,23,42,0.13);\n  --shadow-soft: 0 10px 26px rgba(15,23,42,0.10);\n  min-height: 100%;\n  padding: 24px;\n  border-radius: 26px;\n  background:\n    radial-gradient(circle at 0% 0%, rgba(59,130,246,0.18), transparent 34%),\n    radial-gradient(circle at 100% 0%, rgba(236,72,153,0.12), transparent 30%),\n    linear-gradient(135deg, #eef4ff, #f8fafc 48%, #fff7ed);\n  color: var(--text);\n  transition: all .25s ease;\n}\n\n.shelly-dashboard.dark {\n  --bg: #07101f;\n  --surface: rgba(15,23,42,0.88);\n  --surface-solid: #111827;\n  --text: #f8fafc;\n  --muted: #cbd5e1;\n  --border: rgba(148,163,184,0.18);\n  --shadow: 0 22px 54px rgba(0,0,0,0.44);\n  --shadow-soft: 0 14px 34px rgba(0,0,0,0.38);\n  background:\n    radial-gradient(circle at 0% 0%, rgba(37,99,235,0.28), transparent 34%),\n    radial-gradient(circle at 100% 0%, rgba(168,85,247,0.18), transparent 30%),\n    linear-gradient(135deg, #07101f, #0f172a 52%, #111827);\n}\n\n.dashboard-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 18px;\n  margin-bottom: 22px;\n}\n\n.eyebrow,\n.panel-kicker {\n  margin: 0 0 5px;\n  font-size: .74rem;\n  letter-spacing: .16em;\n  font-weight: 800;\n  color: #3b82f6;\n}\n\n.dashboard-header h1,\n.panel h2 {\n  margin: 0;\n  color: var(--text);\n}\n\n.dashboard-header h1 {\n  font-size: clamp(1.7rem, 3vw, 2.5rem);\n  line-height: 1.05;\n}\n\n.subtitle,\n.panel p {\n  margin: 7px 0 0;\n  color: var(--muted);\n}\n\n.theme-toggle {\n  display: flex;\n  align-items: center;\n  gap: 9px;\n  border: 1px solid var(--border);\n  color: var(--text);\n  background: var(--surface);\n  box-shadow: var(--shadow-soft);\n  padding: 9px 12px;\n  border-radius: 999px;\n  cursor: pointer;\n  font-weight: 700;\n}\n\n.theme-icon {\n  font-size: 1.15rem;\n}\n\n.toggle-track {\n  position: relative;\n  width: 38px;\n  height: 22px;\n  border-radius: 999px;\n  background: #cbd5e1;\n  transition: .2s ease;\n}\n\n.toggle-track.active {\n  background: #8b5cf6;\n}\n\n.toggle-thumb {\n  position: absolute;\n  top: 3px;\n  left: 3px;\n  width: 16px;\n  height: 16px;\n  border-radius: 50%;\n  background: #fff;\n  box-shadow: 0 2px 6px rgba(0,0,0,.22);\n  transition: transform .2s ease;\n}\n\n.toggle-track.active .toggle-thumb {\n  transform: translateX(16px);\n}\n\n.metric-grid,\n.minute-grid {\n  display: grid;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  gap: 16px;\n}\n\n.metric-card {\n  display: flex;\n  align-items: center;\n  gap: 16px;\n  min-height: 132px;\n  padding: 20px;\n  color: #fff;\n  border-radius: 22px;\n  box-shadow: var(--shadow);\n  transform: translateY(0);\n  transition: transform .2s ease, box-shadow .2s ease;\n}\n\n.metric-card:hover,\n.minute-card:hover {\n  transform: translateY(-4px);\n}\n\n.metric-card.voltage {\n  background: linear-gradient(135deg, #2563eb, #06b6d4);\n}\n\n.metric-card.current {\n  background: linear-gradient(135deg, #7c3aed, #d946ef);\n}\n\n.metric-card.power {\n  background: linear-gradient(135deg, #ea580c, #f59e0b);\n}\n\n.metric-icon {\n  display: grid;\n  place-items: center;\n  flex: 0 0 52px;\n  width: 52px;\n  height: 52px;\n  border-radius: 17px;\n  background: rgba(255,255,255,.18);\n  border: 1px solid rgba(255,255,255,.25);\n  font-size: 1.45rem;\n  font-weight: 900;\n}\n\n.metric-label {\n  display: block;\n  opacity: .88;\n  font-weight: 700;\n}\n\n.metric-card strong {\n  display: block;\n  margin-top: 3px;\n  font-size: clamp(1.65rem, 4vw, 2.35rem);\n  line-height: 1;\n}\n\n.metric-card small {\n  display: block;\n  margin-top: 5px;\n  opacity: .78;\n}\n\n.panel {\n  margin-top: 18px;\n  padding: 20px;\n  border-radius: 22px;\n  border: 1px solid var(--border);\n  background: var(--surface);\n  box-shadow: var(--shadow);\n  backdrop-filter: blur(12px);\n}\n\n.control-panel {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 18px;\n}\n\n.power-switch {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 11px;\n  min-width: 130px;\n  padding: 14px 22px;\n  border: 0;\n  border-radius: 16px;\n  cursor: pointer;\n  color: #fff;\n  background: linear-gradient(135deg, #64748b, #334155);\n  box-shadow: 0 10px 24px rgba(51,65,85,.28);\n  font-size: 1rem;\n  font-weight: 900;\n  letter-spacing: .06em;\n  transition: all .2s ease;\n}\n\n.power-switch.on {\n  background: linear-gradient(135deg, #059669, #22c55e);\n  box-shadow: 0 12px 28px rgba(34,197,94,.28);\n}\n\n.power-dot {\n  width: 11px;\n  height: 11px;\n  border-radius: 50%;\n  background: #cbd5e1;\n  box-shadow: 0 0 0 4px rgba(255,255,255,.12);\n}\n\n.power-switch.on .power-dot {\n  background: #dcfce7;\n  box-shadow: 0 0 14px rgba(220,252,231,.85);\n}\n\n.section-heading,\n.last-message {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 14px;\n}\n\n.unit-badge {\n  padding: 6px 10px;\n  border-radius: 999px;\n  color: #2563eb;\n  background: rgba(59,130,246,.11);\n  font-weight: 800;\n}\n\n.minute-grid {\n  margin-top: 16px;\n}\n\n.minute-card {\n  min-height: 105px;\n  padding: 17px;\n  border-radius: 18px;\n  color: var(--text);\n  box-shadow: var(--shadow-soft);\n  transition: transform .2s ease;\n}\n\n.minute-card span {\n  display: block;\n  color: var(--muted);\n  font-weight: 700;\n}\n\n.minute-card strong {\n  display: block;\n  margin-top: 10px;\n  font-size: 1.55rem;\n}\n\n.minute-1 {\n  background: linear-gradient(135deg, rgba(14,165,233,.24), rgba(59,130,246,.10));\n}\n\n.minute-2 {\n  background: linear-gradient(135deg, rgba(139,92,246,.24), rgba(217,70,239,.10));\n}\n\n.minute-3 {\n  background: linear-gradient(135deg, rgba(244,63,94,.22), rgba(249,115,22,.10));\n}\n\n.dark .minute-1,\n.dark .minute-2,\n.dark .minute-3 {\n  background-color: rgba(15,23,42,.82);\n}\n\n.last-message {\n  margin-top: 16px;\n  padding: 13px 15px;\n  border-radius: 15px;\n  background: rgba(59,130,246,.08);\n  color: var(--muted);\n}\n\n.last-message strong {\n  color: var(--text);\n}\n\n.status-dot {\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background: #22c55e;\n  box-shadow: 0 0 12px rgba(34,197,94,.65);\n}\n\n@media (max-width: 760px) {\n  .dashboard-header,\n  .control-panel,\n  .section-heading,\n  .last-message {\n    align-items: stretch;\n    flex-direction: column;\n  }\n\n  .metric-grid,\n  .minute-grid {\n    grid-template-columns: 1fr;\n  }\n\n  .theme-toggle,\n  .power-switch {\n    width: 100%;\n  }\n}\n</style>\n",
        "storeOutMessages": true,
        "passthru": false,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 790,
        "y": 120,
        "wires": [
            [
                "mqtt_out_command"
            ]
        ]
    },
    {
        "id": "mqtt_broker_shelly",
        "type": "mqtt-broker",
        "name": "Lokaler MQTT-Broker",
        "broker": "192.168.178.186",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {}
    },
    {
        "id": "ui_group_custom_dashboard",
        "type": "ui-group",
        "name": "Shelly Dashboard",
        "page": "ui_page_shelly_full",
        "width": "12",
        "height": "1",
        "order": 1,
        "showTitle": false,
        "className": "",
        "visible": "true",
        "disabled": "false",
        "groupType": "default"
    },
    {
        "id": "ui_page_shelly_full",
        "type": "ui-page",
        "name": "Shelly 1PM",
        "ui": "6ab8ca9187b0f4ba",
        "path": "/shelly-1pm",
        "icon": "lightbulb",
        "layout": "grid",
        "theme": "ui_theme_shelly_full",
        "breakpoints": [
            {
                "name": "Default",
                "px": "0",
                "cols": "3"
            },
            {
                "name": "Tablet",
                "px": "576",
                "cols": "6"
            },
            {
                "name": "Small Desktop",
                "px": "768",
                "cols": "9"
            },
            {
                "name": "Desktop",
                "px": "1024",
                "cols": "12"
            }
        ],
        "order": 1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "6ab8ca9187b0f4ba",
        "type": "ui-base",
        "name": "Mein Dashboard",
        "path": "/dashboard",
        "appIcon": "",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "headerContent": "page",
        "navigationStyle": "default",
        "titleBarStyle": "default",
        "showReconnectNotification": true,
        "notificationDisplayTime": 1,
        "showDisconnectNotification": true,
        "allowInstall": false
    },
    {
        "id": "ui_theme_shelly_full",
        "type": "ui-theme",
        "name": "Shelly Theme",
        "colors": {
            "surface": "#ffffff",
            "primary": "#2563eb",
            "bgPage": "#eaf0fb",
            "groupBg": "#ffffff",
            "groupOutline": "#dbeafe"
        },
        "sizes": {
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "20px",
            "widgetGap": "12px",
            "density": "default"
        }
    },
    {
        "id": "92eb6594b361568b",
        "type": "global-config",
        "env": [],
        "modules": {
            "@flowfuse/node-red-dashboard": "1.30.2"
        }
    }
]

Das Dashboard zeigt oben drei farbige Kacheln mit den aktuell erfassten Messwerten:

  • Spannung in Volt,
  • Stromstärke in Ampere,
  • aktuelle Leistung in Watt.

Darunter befindet sich ein Schalter, über den der angeschlossene Verbraucher ein- und ausgeschaltet werden kann. Beim Betätigen des Schalters sendet Node-RED die Payload on beziehungsweise off über die bereits eingerichtete MQTT-Out-Node an den Shelly.

Messwerte im Dashboard anzeigen

Die zuvor im Flow-Context gespeicherten Werte werden an das Dashboard übergeben. Dabei werden unter anderem folgende Eigenschaften verwendet:

values.voltageV
values.currentA
values.powerW
values.output
values.energyByMinute
values.timestamp

Die Werte für Spannung, Strom und Leistung werden in den drei oberen Kacheln dargestellt. Ändert sich einer der Werte durch eine neue MQTT-Statusmeldung, wird die Anzeige automatisch aktualisiert.

Verbraucher über das Dashboard schalten

Der Schaltzustand des Verbrauchers wird über einen eigenen Schalter dargestellt. Ist der Ausgang des Shelly eingeschaltet, zeigt das Dashboard den Zustand EIN. Beim Ausschalten wechselt die Anzeige entsprechend auf AUS.

Der Schalter sendet abhängig vom gewählten Zustand eine der beiden Payloads: on oder: off

Die MQTT-Out-Node veröffentlicht den Befehl anschließend unter dem bekannten Topic:

shelly1pmg3-3030f9e95140/command/switch:0

Damit der angezeigte Zustand mit dem tatsächlichen Ausgang des Shelly übereinstimmt, wird auch der Wert output aus den empfangenen Statusmeldungen berücksichtigt.

Energieverbrauch der letzten drei Minuten

Unterhalb der Steuerung werden die drei Werte aus dem Array aenergy.by_minute angezeigt. Der Shelly liefert damit die Energieverbräuche der letzten drei vollständig abgeschlossenen Minuten.

Die Werte werden in Milliwattstunden dargestellt:

Letzte Minute
Vor zwei Minuten
Vor drei Minuten

Dadurch lässt sich bereits ohne zusätzliche Datenbank erkennen, wie sich der Verbrauch in den letzten Minuten entwickelt hat.

Zeitpunkt der letzten MQTT-Nachricht

Zusätzlich zeigt das Dashboard an, wann zuletzt eine Statusmeldung des Shelly empfangen wurde. Dafür wird der Unix-Zeitstempel aus der MQTT-Nachricht in ein lesbares Datum umgewandelt.

Das verwendete Format lautet: Stunde:Minute:Sekunde Tag.Monat.Jahr

Eine mögliche Ausgabe sieht beispielsweise so aus: 18:42:15 27.06.2026

Der Zeitstempel ist besonders hilfreich, um zu erkennen, ob weiterhin aktuelle Daten vom Shelly eintreffen. Bleibt die Anzeige über einen längeren Zeitraum unverändert, kann dies auf eine unterbrochene MQTT-Verbindung oder ein nicht erreichbares Gerät hinweisen.

Helles und dunkles Theme

Das Dashboard verfügt außerdem über einen Schalter für ein helles und ein dunkles Theme. Im hellen Modus werden die Messwerte auf einem freundlichen, kontrastreichen Hintergrund angezeigt. Das Dark Theme eignet sich besonders für dauerhaft geöffnete Anzeigen oder Wanddisplays.

Node-RED Dashboard für einen Shelly 1PM mit eingeschaltetem Verbraucher, Messwerten und hellem Theme
Das Node-RED Dashboard zeigt den eingeschalteten Verbraucher sowie aktuelle Messwerte für Spannung, Strom und Leistung im hellen Theme.
Node-RED Dashboard für einen Shelly 1PM mit eingeschaltetem Verbraucher, Messwerten und aktivem Dark Theme
Das Shelly-Dashboard mit eingeschaltetem Verbraucher und aktuellen Messwerten im aktivierten Dark Theme.

Die ausgewählte Darstellung wird im Browser gespeichert. Dadurch bleibt das zuletzt verwendete Theme auch nach dem erneuten Laden des Dashboards erhalten.

Die farbigen Kacheln, Schatten und unterschiedlichen Hervorhebungen sorgen dafür, dass sich die Messwerte schnell voneinander unterscheiden lassen. Spannung, Strom und Leistung erhalten jeweils eine eigene Farbe, während der Schaltzustand und die Minutenwerte in separaten Bereichen dargestellt werden.

Das vollständige Dashboard vereint damit Steuerung und Überwachung auf einer Oberfläche: Der Verbraucher lässt sich lokal über MQTT schalten, während Node-RED gleichzeitig die aktuellen Mess- und Energiewerte des Shelly anzeigt.

Fazit und Ausblick

Mit Node-RED und MQTT lässt sich der Shelly vollständig lokal steuern und überwachen. Die einzelnen NotifyStatus-Nachrichten werden dabei im Flow-Context zu einem gemeinsamen Datensatz zusammengeführt und anschließend im Dashboard dargestellt.

Im nächsten Beitrag zeige ich dir, wie du mit einem eigenen Shelly Script individuelle MQTT-Nachrichten an deinen Broker senden kannst. Der folgende Screenshot gibt bereits einen kleinen Vorgeschmack darauf.

Vorschau auf ein Shelly Script zum Senden eigener MQTT-Nachrichten an einen MQTT-Broker
Im nächsten Beitrag zeige ich, wie sich mit einem Shelly Script eigene MQTT-Nachrichten erzeugen und an einen Broker senden lassen.

Letzte Aktualisierung am: 29. Juni 2026

Foto von Stefan Draeger
Über den Autor

Stefan Draeger — Entwickler & Tech-Blogger

Ich zeige praxisnah, wie du Projekte mit Arduino, ESP32 und Smarthome-Komponenten umsetzt – Schritt für Schritt, mit Code und Schaltplänen.

Mehr Artikel von Stefan →

Schreibe einen Kommentar Antwort abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Fragen oder Feedback?

Du hast eine Idee, brauchst Hilfe oder möchtest Feedback loswerden?
Support-Ticket erstellen

Newsletter abonnieren

Bleib auf dem Laufenden: Erhalte regelmäßig Updates zu neuen Projekten, Tutorials und Tipps rund um Arduino, ESP32 und mehr – direkt in dein Postfach.

Jetzt Newsletter abonnieren

Unterstütze meinen Blog

Wenn dir meine Inhalte gefallen, freue ich mich über deine Unterstützung auf Tipeee.
So hilfst du mit, den Blog am Leben zu halten und neue Beiträge zu ermöglichen.

draeger-it.blog auf Tipeee unterstützen

Vielen Dank für deinen Support!
– Stefan Draeger

Kategorien

Tools

  • QR-Code Generator
  • Passwort Generator: Sichere Passwörter & Passphrasen erstellen
  • PNG zu WebP konvertieren – kostenlos, schnell & ohne Upload
  • Bilder online komprimieren – PNG, JPG & JPEG ohne Upload
  • Code online formatieren: JSON, HTML, XML, CSS & JavaScript
  • Unix-Zeitstempel-Rechner
  • ASCII Tabelle
  • Spannung, Strom, Widerstand und Leistung berechnen
  • Widerstandsrechner
  • 8×8 LED Matrix Tool
  • 8×16 LED Matrix Modul von Keyestudio
  • 16×16 LED Matrix – Generator

Links

Blogverzeichnis Bloggerei.de TopBlogs.de das Original - Blogverzeichnis | Blog Top Liste Blogverzeichnis trusted-blogs.com

Stefan Draeger
Königsberger Str. 13
38364 Schöningen
Tel.: 015565432686
E-Mail: info@draeger-it.blog

Folge mir auf

link zu Fabook
link zu LinkedIn
link zu YouTube
link zu TikTok
link zu Pinterest
link zu Instagram
  • Impressum
  • Datenschutzerklärung
  • Disclaimer
  • Cookie-Richtlinie (EU)
©2026 Technik Blog | Built using WordPress and Responsive Blogily theme by Superb
Cookie-Zustimmung verwalten
Wir verwenden Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wir tun dies, um das Surferlebnis zu verbessern und um personalisierte Werbung anzuzeigen. Wenn Sie diesen Technologien zustimmen, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn Sie Ihre Zustimmung nicht erteilen oder zurückziehen, können bestimmte Funktionen beeinträchtigt werden.
Funktional Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt. Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
  • Optionen verwalten
  • Dienste verwalten
  • Verwalten von {vendor_count}-Lieferanten
  • Lese mehr über diese Zwecke
Einstellungen anzeigen
  • {title}
  • {title}
  • {title}