Smart Home ohne Cloud – lokales Smart Home mit Shelly, Zigbee & Home Assistant

Illustration mit Nerd-Zebra, das Smart-Home-Geräte aus einer Cloud befreit, Symbol für „Smart Home ohne Cloud

Ein cloudfreies Smart Home bedeutet: volle Kontrolle, weniger Abhängigkeiten, mehr Datenschutz – und deine Automationen laufen weiter, auch wenn das Internet gerade beschlossen hat, heute nicht zu arbeiten. Das hier ist der Startpunkt: Überblick, klare Empfehlungen, und ein Mix aus nerdiger Ansage und nüchterner Praxis.


Schnellnavigation


Warum ein Smart Home ohne Cloud?

Ein Smart Home ohne Cloud bedeutet volle Kontrolle über deine Geräte, keine Abhängigkeit von Hersteller-Servern und mehr Datenschutz. Außerdem funktionieren deine Automationen auch dann, wenn das Internet ausfällt. In diesem Artikel bekommst du einen Mix aus passiv-aggressivem Chat-Stil und nüchternen Erklärungen, damit du direkt weißt, wie du am besten startest.

💬 Einsteiger:
Ich will ins Smart Home einsteigen. Also… ich kauf mir erstmal eine Hue Bridge ka—

💬 Nerd-Zebra:
NEIN!

💬 Einsteiger:
Warum denn nicht? Die sieht doch voll… äh… bridge-ig aus?

💬 Nerd-Zebra:
Weil du dir damit direkt eine goldene Cloud-Handschelle anlegst. Klar, die Hue Bridge funktioniert auch halbwegs offline – aber nicht vollständig. Entertainment-Sync? Nur über Philips-Server. Neue Funktionen? Nur, wenn Philips Bock hat. Und wenn die irgendwann sagen „Tschüss, wir machen dicht“, dann sitzt du im Dunkeln – im wahrsten Sinne.

Merksatz: Wenn ein System nur mit Hersteller-Account „richtig“ funktioniert, ist es kein Smart Home – es ist ein Abo mit Lampen.


Gerätewahl: Direkt lokal starten

Wenn du Geräte kaufst, die direkt lokal funktionieren, sparst du dir spätere Umbauten und Ärger mit Cloud-Zwang. Achte beim Kauf auf: LAN-Modus, lokale API oder MQTT. Dann kannst du später entscheiden, wie „nerdig“ du es betreiben willst.

💬 Einsteiger:
Okay, dann halt ’ne smarte Steckdose von… äh… TP-L—

💬 Nerd-Zebra:
NEIN! … also, nicht unbedingt. Die meisten WLAN-Steckdosen wollen erstmal nach Hause telefonieren, bevor sie überhaupt Kaffee kochen lassen. Lieber direkt Geräte kaufen, die LAN-Modus, lokale API oder MQTT können. Zum Beispiel Shelly.

💡 Pro-Tipp: Shelly-Geräte funktionieren zwar auch „mit Cloud“, lassen sich aber sehr gut Schritt für Schritt auf lokal umstellen – perfekt für einen sanften Einstieg.

👉 Beispiel: Shelly Plus 1PM (bezahlter Link) oder Shelly Plus 2PM (bezahlter Link)

📦 Shelly ganz ohne Cloud & ohne Broker
Shellys können auch direkt miteinander kommunizieren – ohne Server oder Cloud.
➡️ Mehr dazu: Shelly Direktverknüpfung – lokale Automationen ohne Cloud

Mini-Checkliste beim Kauf:

  • Kann das Gerät auch ohne Hersteller-App gesteuert werden?
  • Gibt es eine lokale API / MQTT / LAN-Modus?
  • Funktioniert es mit Home Assistant / ioBroker / Zigbee2MQTT?

Zigbee und Alternativen

Zigbee ist ein beliebter Funkstandard fürs Smart Home. Vorteil: Geräte laufen lokal, sind meist sparsam und du kannst Hersteller mischen – wenn du sie über einen eigenen Zigbee-Controller (USB-Stick) ansteuerst.

💬 Einsteiger:
Hm. Dann hol ich mir halt den schicken Aqara Hub—

💬 Nerd-Zebra:
NEIN! … zumindest nicht, wenn er nur in der Cloud lebt. Viele Aqara-Geräte laufen super direkt über Zigbee2MQTT oder ZHA.

💡 Empfehlung: Ein USB-Zigbee-Stick ist die „Hue-Bridge, aber richtig“: Er hängt an deiner eigenen Zentrale und bleibt lokal.

👉 Klassiker: Sonoff Zigbee Dongle Plus (bezahlter Link)

Alternativen (kurz): Matter/Thread ist spannend, Z-Wave ist solide, WLAN-Geräte sind bequem – aber Zigbee ist oft der beste Mix aus Preis, Auswahl und Lokalbetrieb.


Die Schaltzentrale

Deine Schaltzentrale ist das Gehirn deines Smart Homes. Hier laufen Daten zusammen, hier definierst du Automationen, und hier entscheidest du, ob dein Zuhause „smart“ oder nur „fernsteuerbar“ ist.

💬 Einsteiger:
Aber ohne Cloud geht doch gar nix?

💬 Nerd-Zebra:
Doch. Du brauchst nur eine lokale Schaltzentrale – quasi das Gehirn deines Smart Homes:

  • Home Assistant Green (hier bei Amazon (bezahlter Link)) – einfach, solide, wenig Bastelstress
  • Raspberry Pi 4 (Raspi 4 Kit bei Amazon (bezahlter Link)) – flexibel, aber „Pi-Dinge“ passieren
  • Mini-PC / Homelab (HP Mini-PC, meine Empfehlung (bezahlter Link)) – wenn du eh schon Proxmox/Homelab spielst
  • ioBroker – wenn du eher „Blockly & Skripte“ magst (und ja: kann auch lokal)

💡 Nerd-Seitenhieb: Hast du noch ein altes Notebook rumliegen? Auch das kannst du mit Linux aufsetzen und dann Home Assistant oder ioBroker installieren.

Cloud → Lokal umziehen (Prinzip):

  1. Gerät einmal „normal“ einrichten (damit es läuft)
  2. Cloud-Funktionen deaktivieren (wenn möglich)
  3. Lokale Steuerung aktivieren (MQTT / lokale API / Direktverknüpfung)
  4. Automationen in der eigenen Zentrale bauen (nicht in der Hersteller-App)

Kompatibilität mit Hue-Lampen

Viele Hue-Lampen funktionieren auch ohne Hue-Bridge, wenn du sie mit einem Zigbee-Stick direkt an deine Zentrale koppelst. Das spart Geld und verhindert Cloud-Zwang (und du kaufst nicht aus Versehen ein Ökosystem mit Handschellen).

💬 Einsteiger:
Und wenn ich doch mal Hue-Lampen will?

💬 Nerd-Zebra:
Kein Problem – kauf die nackten Lampen, nicht die Bridge. Steck einen Zigbee-USB-Stick an deine Zentrale, fertig.

👉 Empfehlung: Sonoff Zigbee Dongle Plus (bezahlter Link)


Empfohlene Starter-Setups (2026)

Hier sind drei Pakete – je nach Mutlevel. Das Ziel ist: schnell starten, später nerdiger werden, ohne alles neu zu kaufen.

🟢 Minimal – Einstieg ohne Bastelstress

Für: Licht, Steckdosen, erste Automationen. Vorteil: schnell, stabil, wenig Frust.

🟡 Komfort – „Ich will mehr als nur Licht“

Für: Licht + Rollläden + Sensoren + erste „Wenn X dann Y“-Automationen.

🔴 Nerd – „lokal, robust, ausbaufähig“

  • Kleiner (oder großer) 19″ Server
  • USB-Zigbee-Stick (z. B. Sonoff Zigbee Dongle Plus (bezahlter Link))
  • Geräte nach Bedarf (z. B. Shelly Plus 1PM (bezahlter Link), Shelly Plus 2PM (bezahlter Link) oder direkt KNX (bezahlter Link))
  • Optional: MQTT-Broker (lokal), getrennte VLANs, Backup-Konzept

Für: Homelab, viele Geräte, Stabilität, Netzwerk-Segmentierung, „läuft auch ohne Internet“ als echtes Feature.


Weiterführende Guides

Hier geht’s tiefer rein – das ist der Teil, wo aus „Fernbedienung“ echte Automatisierung wird:


Fazit

Ein Smart Home ohne Cloud ist kein Luxusprojekt für Technik-Mönche mit Aluhut, sondern schlicht die vernünftigste Art, sein Zuhause „smart“ zu machen. Du behältst die Kontrolle, deine Automationen funktionieren auch ohne Internet und du bist nicht davon abhängig, ob ein Hersteller morgen noch existiert oder übermorgen sein Abo verdoppelt.

Mit lokalen Geräten wie Shelly, einem eigenen Zigbee-Stick und einer Zentrale wie Home Assistant oder ioBroker baust du dir kein Spielzeug, sondern eine Infrastruktur. Die wächst mit, lässt sich umbauen und gehört am Ende dir – nicht irgendeiner App.

Ja, der Einstieg ist minimal technischer als „App installieren, Account anlegen, fertig“. Aber dafür bekommst du ein System, das nicht bei der ersten Serverstörung dumm aus der Wäsche guckt und bei dem „Smart“ mehr bedeutet als nur „per Handy schalten“.

Kurz gesagt:
Cloud-Smart-Home ist bequem.
Lokales Smart Home ist souverän.

Und ganz ehrlich: Wenn schon Nerd-Hobby, dann bitte mit Kontrolle, Stabilität und der beruhigenden Gewissheit, dass dein Lichtschalter nicht in Kalifornien wohnt.

Nerd-Display – die MQTT LED Matrix für’s Smart Home

Zebra-Avatar hält stolz eine LED-Matrix hoch, auf der in roten Lettern ‚MQTT‘ leuchtet, vor einem Hintergrund mit Technik-Icons.

Du willst Statusmeldungen aus deinem Smart Home nicht nur sehen, sondern stilvoll und individuell animiert anzeigen? Willkommen beim Nerd-Display – unserer eigenen Interpretation einer MQTT LED Matrix, gebaut auf einem ESP8266 mit MD_Parola und einer 4-teiligen MAX7219-Matrix im FC16-Format.

Im Gegensatz zu vielen Bastelprojekten, die nur starr einen Text anzeigen, bietet das Nerd-Display:

  • vollständige MQTT-Anbindung (set/*, state/*, meta/*)
  • konfigurierbare Weboberfläche (LittleFS)
  • Anzeige mehrerer Nachrichten mit eigenen Ein- und Ausblendeffekten
  • individuelle Anzeigedauer (dwell) je Nachricht

Damit kannst du z. B. wichtige Statusmeldungen länger anzeigen und kurze Infos schneller durchlaufen lassen.


tl;dr

Willst du dein eigenes Nerd-Display bauen?
Du brauchst einen D1 Mini* (oder kompatiblen ESP8266-Clone), ein 4-teiliges MAX7219-LED-Matrix (bezahlter Link) (Hardware-SPI) und ein paar Kabel.
Verdrahtung:

Matrix-PinESP8266-PinFunktion
VCC5VStromversorgung
GNDGNDMasse
DIND7 (MOSI)Datenleitung SPI
CSD8Chip Select
CLKD5 (SCK)SPI-Taktleitung

Quellcode & Anleitung: GitHub – Nerd-Display


Hardware und Anschlussbelegung

Das Nerd-Display basiert auf dem ESP8266 D1 mini (z. B. AZDelivery D1 Mini (bezahlter Link)), der direkt an eine MAX7219-LED-Matrix angeschlossen ist. Wir nutzen bewusst die Hardware-SPI-Pins des ESP8266, weil diese deutlich schneller sind als eine Software-Implementierung – wichtig für flüssige Animationen bei mehreren Modulen.

Anschlussplan:

Matrix-PinESP8266-PinFunktion
VCC5VStromversorgung
GNDGMasse
DIND7 (MOSI)Datenleitung SPI
CSD2Chip Select
CLKD5 (SCK)SPI-Taktleitung

Software-Architektur

Der komplette Quelltext liegt auf GitHub – Nerd-Display. Damit der Beitrag lesbar bleibt, beschränken wir uns hier auf Auszüge und eine Funktionsbeschreibung:

1) app.* – Globale Struktur
Zentrale Objekte (Display, MQTT‑Client, Webserver) und Konfigurationen. Änderungen (z. B. Helligkeit) wirken sofort systemweit.

2) config_store.* – Konfiguration speichern & laden
MQTT‑Daten und mDNS‑Namen im LittleFS speichern. Änderungen per WebUI oder MQTT (/set/...).

3) display_service.* – LED‑Matrix‑Steuerung
Steuert alle Parola‑Animationen, Anzeigewechsel und Effekte – jetzt auch pro Nachricht individuell (Effekte + dwell).

4) mqtt_service.* – MQTT‑Handling
Abonniert Befehle, verarbeitet Payloads (String, JSON, Array) und veröffentlicht Status‑Snapshots.

5) web_handlers.* – Weboberfläche
Konfigurationsseite direkt auf dem Gerät.


MQTT LED Matrix des Nerd-Displays mit ESP8266, zeigt den Text ‚Hello‘ in roten LEDs auf vier MAX7219-Modulen.

MQTT-Steuerung – Beispiele


Unterschiede zu anderen Projekten

Viele DIY-MQTT LED Matrix-Projekte:

  • nutzen nur hartcodierte Nachrichten
  • benötigen Neustarts für Änderungen
  • verzichten auf Webinterface

Das Nerd-Display:

  • vollständige MQTT-API (set, state, meta)
  • Konfiguration per WebUI
  • Effekte, Geschwindigkeit, Anzeigedauer live einstellbar
  • Mehrfach-Nachrichten mit automatischer Rotation
  • Hardware-SPI für maximale Performance
  • Benutzername/Passwort für MQTT

MQTT-Steuerung – Beispiele

Einzelner Text

<baseTopic>/set/text → "Tür offen"

Oder als Objekt mit Effekten und individueller Dauer:

{"text":"Hallo Welt","in":"SCROLL_UP","out":"CLOSING","dwell":5000}

Mehrere Texte im Wechsel

[
  {"text":"Willkommen","in":"OPENING","out":"CLOSING","dwell":8000},
  {"text":"Fenster zu?","in":"SCROLL_LEFT","out":"SCROLL_RIGHT","dwell":3000}
]

Oder kurz ohne Effekte (nutzt globale Defaults):

["Willkommen","Gute Fahrt","Fenster zu?"]

Helligkeit, Geschwindigkeit, globale Effekte

<baseTopic>/set/brightness → 10        (0..15)
<baseTopic>/set/speed      → 80        (1..65535)
<baseTopic>/set/dwell      → 2500      (ms, global)
<baseTopic>/set/effect_in  → SCROLL_UP
<baseTopic>/set/effect_out → CLOSING

Tipp: Pro Nachricht gesetzte in/out/dwell überschreiben die globalen Werte nur für diese Nachricht.


Unterstützte MD_Parola-Effekte (Auswahl)

Eingang (in):

  • PRINT
  • SCROLL_DOWN, SCROLL_UP, SCROLL_LEFT, SCROLL_RIGHT
  • OPENING, OPENING_CURSOR
  • (weitere je nach Library-Version verfügbar)

Ausgang (out):

  • NO_EFFECT
  • SCROLL_DOWN, SCROLL_UP, SCROLL_LEFT, SCROLL_RIGHT
  • CLOSING, CLOSING_CURSOR
  • (weitere je nach Library-Version verfügbar)

Unterschiede zu anderen Projekten

Viele DIY‑LED‑Matrix‑Projekte:

  • nutzen nur hartcodierte Nachrichten
  • erfordern Neustarts für Änderungen
  • verzichten auf ein Webinterface

Das Nerd-Display:

  • vollständige MQTT‑API (set, state, meta)
  • Konfiguration per WebUI
  • individuelle Effekte & Dwell je Nachricht
  • Mehrfach‑Nachrichten mit Rotation
  • Hardware‑SPI für maximale Performance
  • MQTT mit Benutzername/Passwort

Integration ins Smart Home

Universell einsetzbar:

  • ioBroker: MQTT-Adapter → einfache Datenpunkte
  • Home Assistant: MQTT-Light oder Sensor
  • Node-RED: Texte aus Automationen einspeisen

Links & Ressourcen


Fazit

Das Nerd-Display bringt deine MQTT LED Matrix-Anzeigen auf das nächste Level: einfache Konfiguration, volle MQTT-Steuerung, schnelle Hardware-SPI-Ansteuerung und jede Menge Animationen. Durch die offene Architektur kannst du es leicht in bestehende Smart-Home-Setups einbinden und jederzeit erweitern.

Hisense per MQTT steuern: So zähmst du deinen Vidaa-Fernseher endlich lokal

Vergleich: Normale Nutzer mit Fernbedienung vs. Zebra mit Terminal – Hisense Fernseher per MQTT steuern

Hast du einen Hisense-TV mit Vidaa-Betriebssystem? Und hast du dir auch schon mal gedacht: „Warum zur Hölle kann ich das Ding nicht einfach per MQTT steuern wie ein anständiges Smart-Home-Gerät?!“ Willkommen im Club. Doch nach Wochen voller Sackgassen, verschlüsselten Geheimnissen und mysteriösen Ports habe ich es geschafft – und du kannst es jetzt auch. Mit „Hisense MQTT“ steuerst du deinen Fernseher wie ein echter Nerd. Lokal. Sicher. Und komplett ohne Cloud.

⚠️ Spoiler: Es ist ein Ritt durch verschlüsselte MQTT-Server, zertifikatsgeschützte Verbindungen und Bash-Magie. Aber hey – das ist Prokrastinerd.de, nicht Mama’s Plug & Play Blog.


Schritt 1: Teste alles mit dem MQTT Explorer

Dein Fernseher muss für die Einrichtung an sein, eine feste IP-Adresse haben und Wake on LAN (WoL) muss aktiviert sein

Bevor du dir die Finger im Terminal wund tippst, teste, ob dein Fernseher überhaupt auf MQTT hört. Verwende dafür MQTT Explorer.

Einstellungen:

  • Broker: IP deines Hisense-TVs
  • Port: 36669
  • Encryption (TLS): Aktivieren ✅
  • Validate certificate: Deaktivieren ❌
  • Client Certificate & Key (unter Advanced):
    • Zertifikat: rcm_certchain_pem.cer
    • Schlüssel: rcm_pem_privkey.pkcs8

Die nötigen Dateien findest du hier: GitHub

Wenn der Explorer sich verbindet, dann sende das folgende Topic (HomeAssistant kannst du austauschen, das ist deine zukünftige Client-ID für den Fernseher):
/remoteapp/tv/ui_service/HomeAssistant/actions/gettvstate

Wenn das klappt, sollte dein Fernseher eine Pin anzeigen. Dann direkt folgendes Topic senden:
/remoteapp/tv/ui_service/HomeAssistant/actions/authenticationcode

Payload:
{"authNum": 1234}

Hat das geklappt? Glückwunsch. Dein TV kann MQTT.


Schritt 2: Debian LXC vorbereiten (alternativ einen Linux-Server oder Raspberry Pi)

Richte dir einen LXC-Container mit Debian 12 ein, z. B. in Proxmox. Gib ihm eine feste IP (Wir verwenden hier 192.168.0.84).

Dann brauchen wir Zugriff um die Zertifikate hochladen zu können.
Also:

nano /etc/ssh/sshd_config

Ergänze oder ändere:

PermitRootLogin yes

(Du kannst das nach dem Hochladen der Zertifikat-Dateien wieder auf „no“ setzen.)

mkdir /etc/mqtt_certs/

Zertifikate per SFTP hochladen, z. B. mit WinSCP.


Infografik zur MQTT-Verbindung zwischen Hisense Fernseher und Linux-Client mit Python, Zertifikaten und Zebra-Avatar

Schritt 3: Python-Umgebung einrichten

mkdir -p /opt/hisense-mqtt
cd /opt/hisense-mqtt
apt update
apt install python3.10-venv -y

🧠 Was ist venv?
venv steht für „virtual environment“. Es erstellt ein isoliertes Python-Umfeld, sodass deine installierten Pakete (wie paho-mqtt) nicht mit dem System-Python oder anderen Projekten kollidieren. Nerds nennen es „sauberes Python“.

python3 -m venv venv
source venv/bin/activate
pip install paho-mqtt

Jetzt erstellen wir eine Python Datei:

nano hisense.py

Inhalt:

import ssl
import argparse
import paho.mqtt.client as mqtt
from paho.mqtt.client import CallbackAPIVersion
import time

# Argumente aus der Kommandozeile parsen
parser = argparse.ArgumentParser(description="Sende MQTT-Befehl an Hisense-TV")
parser.add_argument("--topic", required=True, help="MQTT Topic")
parser.add_argument("--payload", required=True, help="MQTT Payload")
args = parser.parse_args()

# Verbindungskonfiguration
BROKER = "192.168.0.180"
PORT = 36669
USERNAME = "hisenseservice"
PASSWORD = "multimqttservice"
CLIENT_ID = "HomeAssistant"

# Callback bei erfolgreicher Verbindung
def on_connect(client, userdata, flags, reasonCode, properties):
    print(f"[INFO] Verbindungscode: {reasonCode}")
    if reasonCode == mqtt.MQTT_ERR_SUCCESS:
        print(f"[INFO] Verbunden – sende an Topic '{args.topic}': {args.payload}")
        client.publish(args.topic, args.payload)
    else:
        print("[ERROR] Verbindung fehlgeschlagen")

# Callback nach erfolgreicher Veröffentlichung
def on_publish(client, userdata, mid, reasonCode, properties):
    print("[INFO] Nachricht veröffentlicht – beende...")
    client.disconnect()

# MQTT-Client erstellen mit TLS-Optionen
client = mqtt.Client(
    client_id=CLIENT_ID,
    protocol=mqtt.MQTTv311,
    transport="tcp",
    userdata=None,
    clean_session=True,
    reconnect_on_failure=True,
    callback_api_version=CallbackAPIVersion.VERSION2
)

# Login setzen
client.username_pw_set(USERNAME, PASSWORD)

# TLS konfigurieren (Zertifikate nutzen, aber keine Validierung)
client.tls_set(
    ca_certs=None,
    certfile="/etc/mqtt_certs/rcm_certchain_pem.cer",
    keyfile="/etc/mqtt_certs/rcm_pem_privkey.pkcs8",
    tls_version=ssl.PROTOCOL_TLSv1_2,
    cert_reqs=ssl.CERT_NONE
)
client.tls_insecure_set(True)

# Callbacks zuweisen
client.on_connect = on_connect
client.on_publish = on_publish

# Verbindung herstellen und starten
print("[INFO] Stelle Verbindung her...")
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()

# Warten, bis Nachricht gesendet und Verbindung beendet wurde
while client.is_connected():
    time.sleep(0.1)

client.loop_stop()

Test:

python hisense.py --topic "/remoteapp/tv/remote_service/HomeAssistant/actions/sendkey" --payload "KEY_MUTE"

Ist dein Fernseher jetzt lautlos? Super, dann hat alles geklappt!


Schritt 4: Komfortabler Zugriff per Bash

Bevor du dich fragst, warum dein Fernseher auf alle MQTT-Kommandos brav reagiert – aber nicht von allein angeht: Das liegt daran, dass Power On per MQTT nicht vorgesehen ist. Der Fernseher schläft im Deep Sleep – MQTT? Hört er nicht mehr.
Lösung: Du musst ihn via Wake-on-LAN (WoL) aus dem Dornröschenschlaf wecken.

apt install wakeonlan
wakeonlan 00:11:22:33:44:55 # Deine MAC-Adresse anpassen

Damit wir nicht immer erst in die virtuelle Umgebung wechseln müssen, basteln wir uns ein Script, welches das für uns übernimmt.

nano /usr/local/bin/hisense-send

Inhalt:

#!/bin/bash
cd /opt/hisense-mqtt || exit 1
source venv/bin/activate
python hisense.py "$@"

Dann ausführbar machen:

chmod +x /usr/local/bin/hisense-send

Test:

/usr/local/bin/hisense-send --topic "/remoteapp/tv/remote_service/HomeAssistant/actions/sendkey" --payload "KEY_MUTE"

Erneut sollte dein Fernseher lautlos bzw. wieder laut sein.

Optional: PATH erweitern, damit du einfach hisense-send ... tippen kannst:

export PATH="$PATH:/usr/local/bin"


Bonus: Integration mit ioBroker, HomeAssistant, Node-RED, usw.

Damit du von anderen Systemen aus deinen Hisense per MQTT steuern kannst, richte SSH-Zugriff ohne Passwort ein.

Auf dem System welches die Befehle senden soll, führe folgendes aus:

systemctl start ssh
ssh-keygen -t rsa -b 4096
ssh-copy-id root@192.168.0.84

Dann kannst du z. B. in Node-RED einen exec-Node verwenden:

ssh root@192.168.0.84 /usr/local/bin/hisense-send --topic "/remoteapp/tv/remote_service/HomeAssistant/actions/sendkey" --payload "KEY_MUTE"

Alternativ: sudo -u iobroker ssh-keygen & ssh-copy-id für User „iobroker“.
Dies war in meinem Fall nötig, damit die Node-RED, Blockly usw. den Befehl senden konnten.


Alternativen & Empfehlungen

Falls dir das Ganze zu nerdig ist – oder du einfach ein Backup willst, wenn der MQTT-Weg mal wieder zickt – hier zwei Optionen mit Sternchen:

  • 🛒 Hisense Fernseher mit Vidaa gibt’s z. B. hier bei Amazon (bezahlter Link)
  • 🛒 Zigbee-Infrarot-Bridge – die kann klassische IR-Fernbedienungen ersetzen und lässt sich mit ioBroker oder Zigbee2MQTT kombinieren. hier bei Amazon (bezahlter Link)

MQTT-Tastenbefehle für den Hisense TV

Wenn dein Fernseher erfolgreich gekoppelt wurde, kannst du über das folgende Topic Tasteneingaben senden:

Topic: /remoteapp/tv/remote_service/HomeAssistant/actions/sendkey

Steuerung & Navigation:

KEY_POWER    # Gerät ein-/ausschalten (nur wenn aktiv)
KEY_OK           # Bestätigen
KEY_UP           # Pfeil hoch
KEY_DOWN    # Pfeil runter
KEY_LEFT        # Pfeil links
KEY_RIGHT     # Pfeil rechts
KEY_HOME     # Startbildschirm
KEY_MENU     # Menü öffnen
KEY_RETURNS # Zurück
KEY_EXIT         # Beenden / Zurück ins Hauptmenü

Lautstärke & Ton:

KEY_VOLUMEUP          # Lauter
KEY_VOLUMEDOWN   # Leiser
KEY_MUTE                   # Stumm

Mediensteuerung:

KEY_PLAY              # Wiedergabe
KEY_PAUSE            # Pause
KEY_STOP              # Stopp
KEY_FORWARDS    # Vorspulen
KEY_BACK              # Zurückspulen

Zahlenfeld & Untertitel:

KEY_0 bis KEY_9  # Zahlentasten 0–9
KEY_SUBTITLE     # Untertitel ein-/ausblenden

Weitere Befehle findest du hier: GitHub


Fazit

Es ist kein Plug & Play – aber dafür ist es nerdy, offline, lokal und genial. Mit ein bisschen Zertifikatsakrobatik und Bash-Bastelei kannst du deinem Fernseher endlich Manieren beibringen – ganz ohne Cloud, App oder offizielle API. Hisense per MQTT steuern ist nicht nur ein Befehl, sondern eine Lebenseinstellung. 😎


📎 Noch Fragen, Feedback oder eigene Hacks? Schreib mir oder poste auf nerdculture.de/@GrayTheZebra – oder natürlich in den Kommentaren!

Wenn APC „BYE“ sagt und MQTT „Hello“ – Wie ich meine USV smarter machte als nötig

Geöffnete USV mit visualisierter Verbindung von RS232 über einen Netzwerkadapter zu MQTT, inklusive stilisierter Codefragmente

Einleitung

Alte Technik muss nicht verstauben – vor allem nicht, wenn sie noch zuverlässig Strom puffert. In meinem Fall: eine APC Smart-UPS SC420 aus einem ausrangierten Wechselautomat – und die perfekte Gelegenheit, eine APC USV an MQTT anzubinden. Denn nur weil ein Gerät alt ist, heißt das nicht, dass es nicht noch etwas zu sagen hat. Und genau das macht es jetzt – über MQTT. – und die perfekte Gelegenheit, eine APC USV an MQTT anzubinden. Ziel: APC USV an MQTT anbinden, um Daten wie Batteriespannung, Ladezustand und Status in ioBroker nutzbar zu machen.

In diesem Artikel zeige ich dir Schritt für Schritt, wie ich die USV über RS232 und einen Netzwerk-Adapter von Waveshare mit einem Python-Skript auslese und an MQTT übergebe – inklusive Copy&Paste-Code, Nerd-Kommentaren und Stolperfallen.


APC USV an MQTT anbinden: Hardware-Setup

Benötigt:

RS232-Kabel selbst bauen

APC nutzt bei vielen seiner USVs eigene Signalbelegungen und akzeptiert keine Standard-RS232-Kabel. Das bedeutet: Wer einfach ein „normales“ serielles Kabel anschließt, bekommt entweder keine Verbindung – oder die USV schaltet sich sogar ab. Deshalb ist ein selbstgebautes Kabel notwendig.

Adapterkabel zum verbinden einer APC USV an einen RS232 Controller

Die richtige Zuordnung (9-polig):

  • USV TX (Pin 2) an PC RX (Pin 3)
  • USV RX (Pin 3) an PC TX (Pin 2)
  • GND (Pin 5) an GND (Pin 5)

Einrichtung des Netzwerkadapters zur MQTT-Anbindung

Nach dem Anschluss über PoE und dem Start hilft das Tool Vircom, um die IP-Adresse und DHCP einzurichten. Danach kannst du das Gerät bequem über das Webinterface konfigurieren:

  • Betriebsmodus: TCP Server
  • Port: z. B. 5000
  • Ziel-IP brauchst du nicht setzen

LXC-Container vorbereiten

Falls du ebenfalls mit Proxmox arbeitest und dein Setup zukunftssicher gestalten willst, schau dir unbedingt auch meinen Artikel über Proxmox auf IPv6 umstellen – wie, warum und die Probleme an.

Wenn du noch tiefer in das serielle APC-Protokoll einsteigen willst – inklusive aller möglichen Kommandos wie Q1, g oder R – findest du auf networkupstools.org eine exzellente Übersicht.

apt update
apt install python3 python3-pip python3.11-venv -y
python3 -m venv /opt/usv-env
source /opt/usv-env/bin/activate
pip install paho-mqtt


Python-Skript zum Auslesen der APC USV und MQTT-Anbindung

Pfad: /opt/usv_mqtt.py

import socket
import time
import paho.mqtt.client as mqtt

USV_IP = "192.168.0.21"
USV_PORT = 5000
MQTT_SERVER = "192.168.0.102"
MQTT_PORT = 1886
MQTT_USER = "ioBroker"
MQTT_PASSWORD = "digital"
INTERVAL = 60

def clean_value(raw, as_type=float):
    try:
        cleaned = raw.split(":")[0].strip()
        return as_type(cleaned)
    except:
        return None

def send_command(sock, cmd, pause=0.5):
    try:
        sock.sendall(cmd.encode())
        time.sleep(pause)
        data = sock.recv(1024).decode(errors="ignore").strip()
        return data
    except Exception as e:
        print(f"Fehler bei Befehl {cmd}: {e}")
        return "N/A"

def decode_status_flags(hex_str):
    try:
        status = int(hex_str, 16)
        flags = []
        if status & (1 << 3): flags.append("On Line")
        if status & (1 << 4): flags.append("On Battery")
        if status & (1 << 5): flags.append("Overload")
        if status & (1 << 6): flags.append("Battery Low")
        if status & (1 << 7): flags.append("Replace Battery")
        return ", ".join(flags) if flags else "Unknown"
    except:
        return "Invalid"

client = mqtt.Client()
client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
client.connect(MQTT_SERVER, MQTT_PORT, 60)
client.loop_start()

while True:
    try:
        with socket.create_connection((USV_IP, USV_PORT), timeout=5) as sock:
            handshake = send_command(sock, "Y", pause=0.8)
            if "SM" not in handshake:
                print(f"[WARNUNG] Kein Handshake: '{handshake}'")
                raise Exception("USV nicht bereit")

            print("[INFO] Abfrage beginnt")

            raw_status = send_command(sock, "Q1")
            values = {
                "battery_charge": clean_value(send_command(sock, "g")),
                "battery_voltage": clean_value(send_command(sock, "b")),
                "line_voltage": clean_value(send_command(sock, "L")),
                "runtime_left_min": clean_value(send_command(sock, "j"), int),
                "status_raw": raw_status,
                "status_human": decode_status_flags(raw_status),
            }

            for key, value in values.items():
                topic = f"usv/{key}"
                client.publish(topic, value if value is not None else "N/A")
                print(f"{topic}: {value}")

    except Exception as e:
        print(f"[Verbindungsfehler] {e}")

    time.sleep(INTERVAL)


Autostart per systemd

nano /etc/systemd/system/usv-mqtt.service

[Unit]
Description=APC USV → MQTT Bridge (zebra-node)
After=network.target

[Service]
ExecStart=/opt/usv-env/bin/python /opt/usv_mqtt.py
WorkingDirectory=/opt
Restart=on-failure
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target

Dann:

systemctl daemon-reload
systemctl enable usv-mqtt
systemctl start usv-mqtt


Daten in ioBroker nutzen

Die Daten landen z. B. als:

  • usv/battery_charge
  • usv/runtime_left_min
  • usv/status_human

Du kannst nun Visualisierungen bauen, smarte Trigger einrichten oder dich bequem benachrichtigen lassen – beispielsweise per Telegram, Mail oder Sprachassistent.


Fazit: APC USV an MQTT angebunden – und zwar richtig

Die Überraschung: Das alte Gerät konnte mehr, als ich erwartet hatte. Statt „BYE“ sagt es jetzt jeden Tag brav „Hello MQTT“ – und zwar mit überraschend stabilen Werten. Das gibt nicht nur ein gutes Gefühl, sondern bringt auch Transparenz ins Strom-Backup, falls es mal ernst wird. – und warnt mich frühzeitig bei Stromausfällen oder Akkuproblemen.

So wird aus einem staubigen RS232-Port ein smarter Sensor, und der Traum, eine APC USV an MQTT anzubinden, wird Realität – ganz ohne Smart-Slot-Karte oder Spezialsoftware. Außerdem macht es einfach Spaß, Technik zu übertreiben, oder? – und aus einem Blogartikel vielleicht die Inspiration für deinen eigenen Umbau?

Fragen, Ideen oder deinen eigenen Umbau? Ab damit in die Kommentare!

Shelly Gen 3 VS Gen 4 – Lohnt sich das Upgrade?

Einleitung

Shelly hat mit der vierten Generation seiner beliebten Smart-Home-Relais ein großes Upgrade veröffentlicht. Die wichtigsten Neuerungen: Matter-Zertifizierung, Apple HomeKit-Unterstützung und Multi-Protokoll-Konnektivität (WLAN, Bluetooth & Zigbee). Doch wer gewinnt beim Duel Shelly Gen 3 VS Gen 4?

Doch was genau unterscheidet Shelly Gen 3 von Shelly Gen 4? Lohnt sich ein Upgrade, oder kannst du weiterhin auf die bewährten Gen 3-Modelle setzen? In diesem Beitrag findest du alle Unterschiede im Detail!

Shelly Relais

Die wichtigsten Neuerungen von Shelly Gen 4

Multi-Protokoll-Konnektivität: WLAN, Bluetooth & Zigbee

Während Shelly Gen 3 nur WLAN & Bluetooth bot, ist Gen 4 zusätzlich mit Zigbee 3.0 ausgestattet. Dadurch kannst du Shelly Gen 4 nicht nur ins WLAN einbinden, sondern auch in ein Zigbee-Mesh – perfekt für größere Smart-Home-Setups!

💡 Zigbee-Repeater-Funktion: Shelly Gen 4 erweitert dein Zigbee-Netzwerk automatisch, indem er als Mesh-Knoten agiert.


Matter-Zertifizierung – Zukunftssicher für dein Smart Home

Shelly Gen 4 ist offiziell Matter-zertifiziert. Das bedeutet, dass du dein Smart-Home-System herstellerübergreifend steuern kannst – egal ob über Google Home, Amazon Alexa oder Apple HomeKit.

💡 Matter sorgt für bessere Kompatibilität: Du kannst Shelly Gen 4 direkt in Matter-fähige Systeme einbinden, ohne Cloud-Zwang!


Offizielle Apple HomeKit-Unterstützung

Ein großer Schritt nach vorne: Shelly Gen 4 funktioniert jetzt mit Apple HomeKit. Du kannst deine Shelly-Geräte mit der Apple Home-App oder per Siri steuern – ohne Umwege über Drittanbieter-Integrationen.

📌 Shelly Gen 3 funktioniert nicht mit Apple HomeKit, während Gen 4 diese Unterstützung von Haus aus mitbringt.


Verbesserte Energieeffizienz & optimierte Antennen

  • Geringerer Stromverbrauch: Shelly Gen 4 benötigt weniger als 1 W (statt bis zu 1.2 W bei Gen 3).
  • Bessere WLAN- & Bluetooth-Reichweite: Die Antennen wurden für stabilere Verbindungen optimiert.

Technische Detailunterschiede – Shelly Gen 3 vs. Shelly Gen 4

Shelly 1 Gen 3 vs. Shelly 1 Gen 4

Der Shelly 1 Gen 4 bietet Matter, Zigbee und eine bessere Antenne.

FeatureShelly 1 Gen 3Shelly 1 Gen 4
ChipESP-Shelly-C38FESP-Shelly-C68F
Multi-ProtokollWLAN, BluetoothWLAN, Bluetooth, Zigbee
Matter-Zertifiziert❌ Nein✅ Ja
Apple HomeKit❌ Nein✅ Ja
Power Consumption<1.2 W<1 W
KlemmenfarbeSchwarzGrau
Abmessungen (H x B x T)37 x 42 x 16 mm37 x 42 x 16 mm

👉 Jetzt kaufen: Shelly 1 Gen 3 auf Amazon (bezahlter Link)


Shelly 1PM Gen 3 vs. Shelly 1PM Gen 4

📌 Shelly 1PM Gen 4 bietet Matter, Zigbee und eine effizientere Verbrauchsmessung.

FeatureShelly 1PM Gen 3Shelly 1PM Gen 4
ChipESP-Shelly-C38FESP-Shelly-C68F
Multi-ProtokollWLAN, BluetoothWLAN, Bluetooth, Zigbee
Matter-Zertifiziert❌ Nein✅ Ja
Apple HomeKit❌ Nein✅ Ja
Power Consumption<1.2 W<1 W
KlemmenfarbeSchwarzGrau
Abmessungen (H x B x T)37 x 42 x 16 mm37 x 42 x 16 mm

👉 Jetzt kaufen: Shelly 1PM Gen 3 auf Amazon


Shelly 1 Mini Gen 3 vs. Shelly 1 Mini Gen 4

📌 Shelly 1 Mini Gen 4 ist Matter-fähig und bietet eine verbesserte Funkreichweite.

FeatureShelly 1 Mini Gen 3Shelly 1 Mini Gen 4
ChipESP-Shelly-C38FESP-Shelly-C68F
Multi-ProtokollWLAN, BluetoothWLAN, Bluetooth, Zigbee
Matter-Zertifiziert❌ Nein✅ Ja
Apple HomeKit❌ Nein✅ Ja
Power Consumption<1.2 W<1 W
KlemmenfarbeSchwarzGrau
Abmessungen (H x B x T)32 x 35 x 11 mm32 x 35 x 11 mm

👉 Jetzt kaufen: Shelly 1 Mini Gen 3 auf Amazon (bezahlter Link)


Fazit: Shelly Gen 3 oder 4 – Was ist die bessere Wahl?

Hast du bereits Shelly Gen 3?
→ Kein zwingendes Upgrade nötig, es sei denn, du brauchst Zigbee oder Matter.

Willst du Apple HomeKit nutzen?
Dann ist Shelly Gen 4 alternativlos, da Shelly Gen 3 kein HomeKit unterstützt.

Bist du neu im Smart Home?
Greife direkt zu Shelly Gen 4, weil du damit für Matter & Zigbee gerüstet bist.

Brauchst du eine stabile Verbindung mit Zigbee?
→ Shelly Gen 4 agiert als Zigbee-Repeater, während Gen 3 nur WLAN & Bluetooth kann.

Shellys bei Amazon kaufen (bezahlter Link)


Matter benötigt zwingend IPv6. Wie du IPv6 unter Proxmox einrichtest, erfährst du hier.

Einen direkten Vergleich zwischen dem Shelly 1PM Gen3 und dem Shelly 1PM Gen4 findest du in meinem Praxistest.


Was denkst du? Lohnt sich für dich ein Upgrade auf Shelly Gen 4? Schreib es in die Kommentare!

WaterMeV2 – Smarter Feuchtigkeitssensor mit ESP8266 & OLED-Display

Einleitung

Der WaterMeV2 – Feuchtigkeitssensor mit ESP8266 und OLED-Display kann die Bodenfeuchtigkeit in Echtzeit überwachen und die Daten direkt per MQTT an dein Smart Home System senden. Dieses Upgrade unseres vorherigen WaterMe Sensors bietet Verbesserungen in der Hardware und Software, eine optimierte Reset-Funktion sowie ein ansprechendes Gehäuse, das du kostenlos auf meinem Cults3D-Account herunterladen kannst (hier klicken).

WaterMeV2 Gehäuse

Vorteile von WaterMev2

  • Echtzeitüberwachung der Bodenfeuchtigkeit
  • Anbindung an MQTT für Smart Home Integration
  • WiFi-Manager für einfache WLAN-Konfiguration
  • OLED-Display für lokale Anzeige
  • 5-Sekunden-Reset-Taster für eine einfache Neukonfiguration
  • Kostenloses 3D-gedrucktes Gehäuse zum Schutz der Hardware

Benötigte Komponenten

Für dieses Projekt benötigst du folgende Komponenten (mit Affiliate-Links):

Aufbau der Hardware

Der Aufbau ist einfach und erfordert nur wenige Kabelverbindungen:

  1. ESP8266 mit dem OLED-Display verbinden:
    • SDA → D2 (GPIO4)
    • SCL → D1 (GPIO5)
    • VCC → 3.3V
    • GND → GND
  2. Feuchtigkeitssensor anschließen:
    • Signal → A0 (Analog-Pin)
    • VCC → 3.3V
    • GND → GND
  3. Taster für den Reset:
    • Ein Pin an GPIO0 (D3)
    • Der andere Pin an GND

Software – Der Code von WaterMev2

Der folgende Code enthält alle wichtigen Funktionen für den Sensor:

1. Bibliotheken und Variablen

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>
#include <ESP8266WebServer.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <SSD1306Wire.h>
#include <LittleFS.h>
#include <ArduinoJson.h>

#define SENSOR_PIN A0
#define RESET_PIN 0
#define RESET_TIME 5000

Hier laden wir alle wichtigen Bibliotheken und definieren die Pins für den Sensor und den Reset-Taster.

2. Reset-Funktion mit 5-Sekunden-Haltezeit

void checkResetButton() {
    if (digitalRead(RESET_PIN) == LOW) {
        if (!buttonPressed) {
            buttonPressStart = millis();
            buttonPressed = true;
        }
        if (millis() - buttonPressStart >= RESET_TIME) {
            Serial.println("Reset-Taster 5 Sekunden gehalten! Setze WiFiManager-Einstellungen zurück...");
            WiFiManager wifiManager;
            wifiManager.resetSettings();
            delay(1000);
            WiFi.disconnect(true);
            delay(1000);
            ESP.restart();
        }
    } else {
        buttonPressed = false;
    }
}

Diese Funktion sorgt dafür, dass der WiFiManager nur zurückgesetzt wird, wenn der Taster mindestens 5 Sekunden gedrückt wird. Dadurch wird ein versehentliches Zurücksetzen verhindert.

3. Verbindung zum WLAN mit WiFiManager

void setup_wifi() {
    WiFiManager wifiManager;
    wifiManager.setTimeout(180);
    if (!wifiManager.autoConnect("WaterMev2")) {
        ESP.restart();
    }
}

Der WiFiManager ermöglicht eine einfache Einrichtung des WLANs über einen Access Point.

4. Sensor-Daten auslesen und per MQTT senden

void updateSensorData() {
    int sensorValue = analogRead(SENSOR_PIN);
    float moisture = map(sensorValue, 1024, 0, 0, 100);
    client.publish("WaterMev2/feuchtigkeit", String(moisture).c_str());
}

Hier wird die Feuchtigkeit gemessen und an den MQTT-Server gesendet.

5. Loop-Funktion mit Reset-Check

void loop() {
    server.handleClient();
    checkResetButton();
    updateSensorData();
    delay(30000);
}

Die loop()-Funktion überprüft den Taster und aktualisiert die Sensordaten alle 30 Sekunden.

6. Vollständiger Code

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>  // WiFiManager Library für AP-Konfiguration
#include <ESP8266WebServer.h>
#include <time.h>        // Für configTime()
#include <SSD1306Wire.h>        // legacy: #include "SSD1306.h"
#include <LittleFS.h>
#include <ArduinoJson.h>
 
#define SENSOR_PIN A0  // Sensor an A0 angeschlossen
#define RESET_PIN 0  // GPIO0 (D3 beim ESP8266)
#define RESET_TIME 5000  // Zeit in Millisekunden (5 Sekunden)

unsigned long buttonPressStart = 0;
bool buttonPressed = false;

unsigned long previousMillis = 0;  // Speichert den Zeitpunkt des letzten MQTT-Sendevorgangs
const long interval = 30000;       // Zeitintervall für das Senden der MQTT-Daten
 
bool resetActive = false; // verhindert, dass andere Funktionen das Display während Reset stören

char mqtt_server[40] = "";
char mqtt_port[6] = "";
char mqtt_user[40] = "";
char mqtt_pass[40] = "";
char mqtt_topic_prefix[40] = "WaterMe"; // Benutzerdefiniertes Präfix
char ntp_server[40] = "pool.ntp.org";  // Standard NTP Server
 
WiFiClient espClient;
PubSubClient client(espClient);
ESP8266WebServer server(80);
SSD1306Wire display(0x3c, SDA, SCL);
 
void saveConfig() {
    Serial.println("Speichere Konfiguration in LittleFS...");
    File configFile = LittleFS.open("/config.json", "w");
    if (!configFile) {
        Serial.println("Fehler beim Speichern der Konfiguration!");
        return;
    }

    DynamicJsonDocument doc(128);
    doc["mqtt_server"] = mqtt_server;
    doc["mqtt_port"] = mqtt_port;
    doc["mqtt_user"] = mqtt_user;
    doc["mqtt_pass"] = mqtt_pass;
    doc["mqtt_topic_prefix"] = mqtt_topic_prefix;
    doc["ntp_server"] = ntp_server;

    serializeJson(doc, configFile);
    configFile.close();
    Serial.println("Konfiguration gespeichert.");
}

void loadConfig() {
    Serial.println("Lade Konfiguration aus LittleFS...");

    File configFile = LittleFS.open("/config.json", "r");
    if (!configFile) {
        Serial.println("Keine gespeicherte Konfiguration gefunden.");
        return;
    }

    DynamicJsonDocument doc(128);
    DeserializationError error = deserializeJson(doc, configFile);
    if (error) {
        Serial.println("Fehler beim Lesen der Konfigurationsdatei!");
        return;
    }

    strlcpy(mqtt_server, doc["mqtt_server"] | "", sizeof(mqtt_server));
    strlcpy(mqtt_port, doc["mqtt_port"] | "", sizeof(mqtt_port));
    strlcpy(mqtt_user, doc["mqtt_user"] | "", sizeof(mqtt_user));
    strlcpy(mqtt_pass, doc["mqtt_pass"] | "", sizeof(mqtt_pass));
    strlcpy(mqtt_topic_prefix, doc["mqtt_topic_prefix"] | "", sizeof(mqtt_topic_prefix));
    strlcpy(ntp_server, doc["ntp_server"] | "", sizeof(ntp_server));

    Serial.println("Konfiguration geladen.");
}
 
void setup_wifi() {
    Serial.println("Starte WiFiManager...");

    uint8_t mac[6];
    WiFi.macAddress(mac);

    char hostname[32];
    snprintf(hostname, sizeof(hostname), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    WiFiManager wifiManager;
    wifiManager.setTimeout(180); // 3 Minuten warten statt unendlich

    //wifiManager.resetSettings();

    WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", mqtt_server, 40);
    WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 6);
    WiFiManagerParameter custom_mqtt_topic("topic", "MQTT Topic Prefix", mqtt_topic_prefix, 40);
    WiFiManagerParameter custom_ntp_server("ntp_server", "NTP Server", ntp_server, 40);
 
    wifiManager.addParameter(&custom_mqtt_server);
    wifiManager.addParameter(&custom_mqtt_port);
    wifiManager.addParameter(&custom_mqtt_topic);
    wifiManager.addParameter(&custom_ntp_server);

    if (!wifiManager.autoConnect(hostname)) {
        Serial.println("Fehlgeschlagen! Neustart in 5 Sekunden...");
        delay(5000);
        ESP.restart();
    }

    WiFi.hostname(hostname);

    strncpy(mqtt_server, custom_mqtt_server.getValue(), sizeof(mqtt_server) - 1);
    strncpy(mqtt_port, custom_mqtt_port.getValue(), sizeof(mqtt_port) - 1);
    strncpy(mqtt_topic_prefix, custom_mqtt_topic.getValue(), sizeof(mqtt_topic_prefix) - 1);
    strncpy(ntp_server, custom_ntp_server.getValue(), sizeof(ntp_server) - 1);
    saveConfig();
    Serial.println("WLAN verbunden!");
    Serial.print("IP-Adresse: ");
    Serial.println(WiFi.localIP());
}
 
void handleRoot() {
    Serial.println("Web-Oberfläche aufgerufen");
    String htmlContent = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>WaterMe Einstellungen</title>"
                         "<style>"
                         "body { font-family: Arial, sans-serif; margin: 40px; background-color: #333; color: #fff; }"
                         "h1 { color: #fff; }"
                         "form { background-color: #222; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.5); }"
                         "label { display: block; margin-top: 20px; margin-bottom: 5px; color: #ccc; }"
                         "input[type='text'], input[type='submit'] { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #555; border-radius: 4px; }"
                         "input[type='text'] { background-color: #555; color: #ddd; }"
                         "input[type='submit'] { background-color: #008CBA; color: white; cursor: pointer; }"
                         "input[type='submit']:hover { background-color: #005f7a; }"
                         "</style>"
                         "</head><body>"
                         "<h1>WaterMe Einstellungen</h1>"
                         "<form action='/save' method='POST'>"
                         "<label for='server'>MQTT Server:</label> <input type='text' id='server' name='server' value='" + String(mqtt_server) + "'><br>"
                         "<label for='port'>MQTT Port:</label> <input type='text' id='port' name='port' value='" + String(mqtt_port) + "'><br>"
                         "<label for='user'>MQTT Benutzer:</label> <input type='text' id='user' name='user' value='" + String(mqtt_user) + "'><br>"
                         "<label for='pass'>MQTT Passwort:</label> <input type='password' id='pass' name='pass' value='" + String(mqtt_pass) + "'><br>"
                         "<label for='topic'>MQTT Topic Prefix:</label> <input type='text' id='topic' name='topic' value='" + String(mqtt_topic_prefix) + "'><br>"
                         "<label for='ntp_server'>NTP Server:</label> <input type='text' id='ntp_server' name='ntp_server' value='" + String(ntp_server) + "'><br>"
                         "<input type='submit' value='Speichern'>"
                         "</form></body></html>";
    server.send(200, "text/html", htmlContent);
}
 
void handleSave() {
    Serial.println("Speichere neue Einstellungen...");
    if (server.hasArg("server")) strncpy(mqtt_server, server.arg("server").c_str(), sizeof(mqtt_server) - 1);
    if (server.hasArg("port")) strncpy(mqtt_port, server.arg("port").c_str(), sizeof(mqtt_port) - 1);
    if (server.hasArg("user")) strlcpy(mqtt_user, server.arg("user").c_str(), sizeof(mqtt_user));
    if (server.hasArg("pass")) strlcpy(mqtt_pass, server.arg("pass").c_str(), sizeof(mqtt_pass));
    if (server.hasArg("topic")) strncpy(mqtt_topic_prefix, server.arg("topic").c_str(), sizeof(mqtt_topic_prefix) - 1);
    if (server.hasArg("ntp_server")) strncpy(ntp_server, server.arg("ntp_server").c_str(), sizeof(ntp_server) - 1);
    saveConfig();
    server.send(200, "text/html", "<html><body><h1>Gespeichert! Neustart...</h1></body></html>");
    delay(3000);
    ESP.restart();
}
 
void checkResetButton() {
    static int lastSecondsLeft = -1;

    if (digitalRead(RESET_PIN) == LOW) {
        if (!buttonPressed) {
            buttonPressStart = millis();
            buttonPressed = true;
            resetActive = true;
            lastSecondsLeft = -1;
        }

        unsigned long heldTime = millis() - buttonPressStart;
        int secondsLeft = (RESET_TIME - heldTime) / 1000;

        if (secondsLeft != lastSecondsLeft && heldTime < RESET_TIME) {
            lastSecondsLeft = secondsLeft;

            display.clear();
            display.setFont(ArialMT_Plain_16);
            display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
            display.drawString(display.getWidth() / 2, 20, "Reset in");
            display.setFont(ArialMT_Plain_24);
            display.drawString(display.getWidth() / 2, 44, String(secondsLeft));
            display.display();
        }

        if (heldTime >= RESET_TIME) {
            display.clear();
            display.setFont(ArialMT_Plain_16);
            display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
            display.drawString(display.getWidth() / 2, 28, "RESET!");
            display.display();
            delay(1000);

            Serial.println("Reset-Taster 5 Sekunden gehalten! Setze WiFiManager-Einstellungen zurück...");
            WiFiManager wifiManager;
            wifiManager.resetSettings();
            delay(500);
            WiFi.disconnect(true);
            delay(500);
            ESP.restart();
        }

    } else {
        if (buttonPressed) {
            buttonPressed = false;
            resetActive = false;

            // <<< Hier neu ergänzen:
            previousMillis = millis() - interval;  
            // <<< Dadurch wird beim nächsten loop() direkt aktualisiert!
        }
    }
}

void setup() {
    pinMode(RESET_PIN, INPUT_PULLUP);
    Serial.begin(115200);
    Serial.println("WaterMe startet...");

    if (!LittleFS.begin()) {
        Serial.println("LittleFS-Fehler");
    } else {
        loadConfig();
    }

    // *** Display direkt am Anfang initialisieren und "AP" anzeigen ***
    display.init();
    display.flipScreenVertically();
    display.clear();
    display.setFont(ArialMT_Plain_24);
    display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
    display.drawString(display.getWidth() / 2, 28, "AP");
    display.display();
    delay(500);  // Kurze Verzögerung (optional), damit man es sicher sieht

    setup_wifi();
 
    if (strlen(mqtt_server) > 0 && strlen(mqtt_port) > 0) {
        client.setServer(mqtt_server, atoi(mqtt_port));
        Serial.println("MQTT-Konfiguration geladen.");
    } else {
        Serial.println("MQTT-Daten fehlen! Webinterface zur Konfiguration nutzen.");
    }
 
    server.on("/", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.begin();
    Serial.println("Webserver gestartet!");
    Serial.print("Rufe auf: http://");
    Serial.println(WiFi.localIP());

    // Sofortiges erstes Update erzwingen
    previousMillis = millis() - interval;
    handleMQTTAndDisplayUpdate(millis());

    // Sommerzeitregel für Mitteleuropa: letzte So im März + Okt
    configTime("CET-1CEST,M3.5.0/2,M10.5.0/3", ntp_server);

    Serial.print("Warte auf Zeit...");
    time_t now = time(nullptr);
    while (now < 100000) {
        delay(100);
        Serial.print(".");
        now = time(nullptr);
    }
    Serial.println(" Zeit empfangen.");
}
 
void reconnect() {
    static unsigned long lastReconnectAttempt = 0;
    uint8_t mac[6];
    WiFi.macAddress(mac);
    char clientId[32];
    snprintf(clientId, sizeof(clientId), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    if (!client.connected() && millis() - lastReconnectAttempt > 5000) {
        lastReconnectAttempt = millis();
        Serial.print("Verbindungsversuch mit MQTT-Broker als ");
        Serial.println(clientId);

        if (client.connect(clientId, mqtt_user, mqtt_pass)) {
            Serial.println("MQTT-Verbindung hergestellt!");
        } else {
            Serial.print("MQTT-Verbindung fehlgeschlagen, Fehlercode: ");
            Serial.print(client.state());
            Serial.println(" - Neuer Versuch in 5 Sekunden...");
        }
    }
}

void loop() {
    checkResetButton();

    if (resetActive) {
        return;
    }

    server.handleClient();

    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    handleMQTTAndDisplayUpdate(millis());
}

void handleMQTTAndDisplayUpdate(unsigned long currentMillis) {
    if (resetActive) return;

    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        if (!client.connected()) {
            reconnect();
        }
        updateSensorData();
    }
}

void updateSensorData() {
    if (resetActive) return;
    int sensorValue = analogRead(SENSOR_PIN);
    float moisture = map(sensorValue, 1024, 0, 0, 100);

    time_t now = time(nullptr);
    struct tm *timeinfo = localtime(&now);
    char buffer[20];
    strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
    String timestamp = String(buffer);

    updateDisplay(moisture, timestamp);
    sendMQTTData(moisture, timestamp);
}

void updateDisplay(float moisture, String timestamp) {
    Serial.println("Aktualisiere Display...");
    Serial.print("Feuchtigkeit: "); Serial.println(moisture);
    Serial.print("Timestamp: "); Serial.println(timestamp);

    String text = String(moisture, 0) + "%";
    display.clear();
    display.setFont(ArialMT_Plain_24);
    display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
    display.drawString(display.getWidth() / 2, 28, text);
    display.drawLine(0, 42, 128, 42);
    display.setFont(ArialMT_Plain_16);
    display.drawString(display.getWidth() / 2, 54, timestamp);
    display.display();
}

void sendMQTTData(float moisture, const String& timestamp) {
    char topic[50], message[50];

    snprintf(topic, sizeof(topic), "%s/feuchtigkeit", mqtt_topic_prefix);
    snprintf(message, sizeof(message), "%.2f", moisture);
    client.publish(topic, message);

    snprintf(topic, sizeof(topic), "%s/timestamp", mqtt_topic_prefix);
    client.publish(topic, timestamp.c_str());
}

Gehäuse zum 3D-Drucken

Damit die Elektronik geschützt ist, kannst du dir ein passendes Gehäuse für WaterMev2 kostenlos auf meinem Cults3D-Account herunterladen: Hier geht’s zum Gehäuse.

WaterMeV2 Gehäuse mit ESP8266 und OLED Display

Fazit

Mit dem WaterMeV2 – Feuchtigkeitssensor mit ESP8266 und OLED-Display kannst du ganz einfach Werte überwachen und in dein Smart Home System integrieren. Dank der MQTT-Integration kannst du die Daten bequem weiterverarbeiten und mit dem OLED-Display behältst du stets den Überblick. Baue dein eigenes WaterMeV2 jetzt nach und lade dir das passende Gehäuse herunter!


Hast du Fragen oder Verbesserungsvorschläge? Schreib sie in die Kommentare!

WaterMe – DIY Bewässerungssystem mit ESP8266 & 3D-Druck-Gehäuse

Für alle, die gleichzeitig Technikfreaks und Hobbygärtner sind, habe ich hier etwas Spannendes: das WaterMe DIY Bewässerungssystem. Ein DIY-Bewässerungssystem, das auf dem cleveren ESP8266 Mikrocontroller basiert, um deine Pflanzen optimal und smart zu versorgen.

Die Bauteile des WaterMe DIY Bewässerungssystem

1. Der Bodenfeuchtesensor: Kernstück des Systems ist der kapazitive Bodenfeuchtesensor (bezahlter Link), der dank seiner Technologie dauerhaft und zuverlässig die Feuchtigkeit im Boden misst.

2. Der D1 Mini ESP8266 Entwicklungsboard: Der D1 Mini (bezahlter Link) ist ein kleines Kraftpaket mit WLAN-Fähigkeit, ideal für alle IoT-Projekte.

3. Das 3D-gedruckte Gehäuse: Mein selbst entworfenes Gehäuse ist auf Cults3D kostenlos erhältlich und nutzt Gewindeeinsätze (bezahlter Link). Hier findet Ihr passende Mikro-Schrauben (bezahlter Link).

WaterMe – DIY Bewässerungssystem mit 3D gedrucktem Gehäuse

Erweiterte Funktionen und technische Details

Das „WaterMe“ System nutzt einen AP für die WLAN-Verbindung, MQTT und NTP, um die Funktionalität über das reine Messen der Bodenfeuchtigkeit hinaus zu erweitern.

MQTT (Message Queuing Telemetry Transport): Dieses leichte und effiziente Protokoll ermöglicht es dem System, Messdaten über das Internet zu versenden. Im Code werden MQTT-Einstellungen konfiguriert, um Sensorwerte an einen Server zu senden, der diese dann für Monitoring oder automatische Bewässerungsaktionen verwenden kann.

NTP (Network Time Protocol): Die Integration von NTP hilft dabei, die exakte Zeit für das Logging der Sensorwerte zu erhalten. Dies ist besonders nützlich, um zu bestimmen, wann die Pflanzen zuletzt gegossen wurden und wann sie wieder Wasser benötigen.

Anschluss des ESP

Der Anschluss des ESP ist sehr einfach. Es muss lediglich die rote Plus-Leitung an 5V, die schwarze Minus-Leitung an GND und die gelbe Signalleitung an A0 des ESP angeschlossen werden.

WaterMe – DIY Bewässerungssystem mit ESP8266

Arduino Code Erklärung

Der untenstehende Arduino-Sketch ist das Herzstück von des WaterMe DIY Bewässerungssystem. Er verbindet den D1 Mini mit deinem WLAN, misst die Bodenfeuchtigkeit und sendet diese Daten über MQTT. Den kompletten Code findest du nachfolgend:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>  // WiFiManager Library für AP-Konfiguration
#include <ESP8266WebServer.h>
#include <NTPClient.h>  // Bibliothek für NTP-Client
#include <WiFiUdp.h>          // Only needed for Arduino 1.6.5 and earlier
#include <LittleFS.h>
#include <ArduinoJson.h>
 
#define SENSOR_PIN A0  // Sensor an A0 angeschlossen

unsigned long previousMillis = 0;  // Speichert den Zeitpunkt des letzten MQTT-Sendevorgangs
const long interval = 30000;       // Zeitintervall für das Senden der MQTT-Daten
 
char mqtt_server[40] = "";
char mqtt_port[6] = "";
char mqtt_topic_prefix[40] = "WaterMe"; // Benutzerdefiniertes Präfix
char ntp_server[40] = "pool.ntp.org";  // Standard NTP Server
 
WiFiClient espClient;
PubSubClient client(espClient);
ESP8266WebServer server(80);
WiFiUDP udp;
NTPClient timeClient(udp, ntp_server, 3600, 60000);  // Zeitzone +1 für Deutschland
 
void saveConfig() {
    Serial.println("Speichere Konfiguration in LittleFS...");
    File configFile = LittleFS.open("/config.json", "w");
    if (!configFile) {
        Serial.println("Fehler beim Speichern der Konfiguration!");
        return;
    }

    DynamicJsonDocument doc(128);
    doc["mqtt_server"] = mqtt_server;
    doc["mqtt_port"] = mqtt_port;
    doc["mqtt_topic_prefix"] = mqtt_topic_prefix;
    doc["ntp_server"] = ntp_server;

    serializeJson(doc, configFile);
    configFile.close();
    Serial.println("Konfiguration gespeichert.");
}

void loadConfig() {
    Serial.println("Lade Konfiguration aus LittleFS...");

    File configFile = LittleFS.open("/config.json", "r");
    if (!configFile) {
        Serial.println("Keine gespeicherte Konfiguration gefunden.");
        return;
    }

    DynamicJsonDocument doc(128);
    DeserializationError error = deserializeJson(doc, configFile);
    if (error) {
        Serial.println("Fehler beim Lesen der Konfigurationsdatei!");
        return;
    }

    strlcpy(mqtt_server, doc["mqtt_server"] | "", sizeof(mqtt_server));
    strlcpy(mqtt_port, doc["mqtt_port"] | "", sizeof(mqtt_port));
    strlcpy(mqtt_topic_prefix, doc["mqtt_topic_prefix"] | "", sizeof(mqtt_topic_prefix));
    strlcpy(ntp_server, doc["ntp_server"] | "", sizeof(ntp_server));

    Serial.println("Konfiguration geladen.");
}
 
void setup_wifi() {
    Serial.println("Starte WiFiManager...");

    uint8_t mac[6];
    WiFi.macAddress(mac);

    char hostname[32];
    snprintf(hostname, sizeof(hostname), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    WiFiManager wifiManager;
    wifiManager.setTimeout(180); // 3 Minuten warten statt unendlich

    //wifiManager.resetSettings();

    WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", mqtt_server, 40);
    WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 6);
    WiFiManagerParameter custom_mqtt_topic("topic", "MQTT Topic Prefix", mqtt_topic_prefix, 40);
    WiFiManagerParameter custom_ntp_server("ntp_server", "NTP Server", ntp_server, 40);
 
    wifiManager.addParameter(&custom_mqtt_server);
    wifiManager.addParameter(&custom_mqtt_port);
    wifiManager.addParameter(&custom_mqtt_topic);
    wifiManager.addParameter(&custom_ntp_server);

    if (!wifiManager.autoConnect(hostname)) {
        Serial.println("Fehlgeschlagen! Neustart in 5 Sekunden...");
        delay(5000);
        ESP.restart();
    }

    WiFi.hostname(hostname);

    strncpy(mqtt_server, custom_mqtt_server.getValue(), sizeof(mqtt_server) - 1);
    strncpy(mqtt_port, custom_mqtt_port.getValue(), sizeof(mqtt_port) - 1);
    strncpy(mqtt_topic_prefix, custom_mqtt_topic.getValue(), sizeof(mqtt_topic_prefix) - 1);
    strncpy(ntp_server, custom_ntp_server.getValue(), sizeof(ntp_server) - 1);
    saveConfig();
    Serial.println("WLAN verbunden!");
    Serial.print("IP-Adresse: ");
    Serial.println(WiFi.localIP());
}
 
void handleRoot() {
    Serial.println("Web-Oberfläche aufgerufen");
    String htmlContent = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>WaterMe Einstellungen</title>"
                         "<style>"
                         "body { font-family: Arial, sans-serif; margin: 40px; background-color: #333; color: #fff; }"
                         "h1 { color: #fff; }"
                         "form { background-color: #222; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.5); }"
                         "label { display: block; margin-top: 20px; margin-bottom: 5px; color: #ccc; }"
                         "input[type='text'], input[type='submit'] { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #555; border-radius: 4px; }"
                         "input[type='text'] { background-color: #555; color: #ddd; }"
                         "input[type='submit'] { background-color: #008CBA; color: white; cursor: pointer; }"
                         "input[type='submit']:hover { background-color: #005f7a; }"
                         "</style>"
                         "</head><body>"
                         "<h1>WaterMe Einstellungen</h1>"
                         "<form action='/save' method='POST'>"
                         "<label for='server'>MQTT Server:</label> <input type='text' id='server' name='server' value='" + String(mqtt_server) + "'><br>"
                         "<label for='port'>MQTT Port:</label> <input type='text' id='port' name='port' value='" + String(mqtt_port) + "'><br>"
                         "<label for='topic'>MQTT Topic Prefix:</label> <input type='text' id='topic' name='topic' value='" + String(mqtt_topic_prefix) + "'><br>"
                         "<label for='ntp_server'>NTP Server:</label> <input type='text' id='ntp_server' name='ntp_server' value='" + String(ntp_server) + "'><br>"
                         "<input type='submit' value='Speichern'>"
                         "</form></body></html>";
    server.send(200, "text/html", htmlContent);
}
 
void handleSave() {
    Serial.println("Speichere neue Einstellungen...");
    if (server.hasArg("server")) strncpy(mqtt_server, server.arg("server").c_str(), sizeof(mqtt_server) - 1);
    if (server.hasArg("port")) strncpy(mqtt_port, server.arg("port").c_str(), sizeof(mqtt_port) - 1);
    if (server.hasArg("topic")) strncpy(mqtt_topic_prefix, server.arg("topic").c_str(), sizeof(mqtt_topic_prefix) - 1);
    if (server.hasArg("ntp_server")) strncpy(ntp_server, server.arg("ntp_server").c_str(), sizeof(ntp_server) - 1);
    saveConfig();
    server.send(200, "text/html", "<html><body><h1>Gespeichert! Neustart...</h1></body></html>");
    delay(3000);
    ESP.restart();
}
 
void setup() {
    Serial.begin(115200);
    Serial.println("WaterMe startet...");

    if (!LittleFS.begin()) {
        Serial.println("LittleFS-Fehler");
    } else {
        loadConfig();
    }

    setup_wifi();
    timeClient.begin();
 
    if (strlen(mqtt_server) > 0 && strlen(mqtt_port) > 0) {
        client.setServer(mqtt_server, atoi(mqtt_port));
        Serial.println("MQTT-Konfiguration geladen.");
    } else {
        Serial.println("MQTT-Daten fehlen! Webinterface zur Konfiguration nutzen.");
    }
 
    server.on("/", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.begin();
    Serial.println("Webserver gestartet!");
    Serial.print("Rufe auf: http://");
    Serial.println(WiFi.localIP());

    // Sofortiges erstes Update erzwingen
    previousMillis = millis() - interval;  // Simuliert ein vergangenes Intervall
    handleMQTTUpdate(millis());
}
 
void reconnect() {
    static unsigned long lastReconnectAttempt = 0;
    uint8_t mac[6];
    WiFi.macAddress(mac);
    char clientId[32];
    snprintf(clientId, sizeof(clientId), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    if (!client.connected() && millis() - lastReconnectAttempt > 5000) {
        lastReconnectAttempt = millis();
        Serial.print("Verbindungsversuch mit MQTT-Broker als ");
        Serial.println(clientId);

        if (client.connect(clientId)) {
            Serial.println("MQTT-Verbindung hergestellt!");

            // Falls du Topics abonnieren willst, füge sie hier hinzu:
            // client.subscribe("WaterMe/steuerung");  
            // client.subscribe("WaterMe/config");

        } else {
            Serial.print("MQTT-Verbindung fehlgeschlagen, Fehlercode: ");
            Serial.print(client.state());  // Gibt den Fehlercode aus
            Serial.println(" - Neuer Versuch in 5 Sekunden...");
        }
    }
}

void loop() {
    server.handleClient();
    timeClient.update();

    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    handleMQTTUpdate(millis());
}

void handleMQTTUpdate(unsigned long currentMillis) {
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        if (!client.connected()) {
            reconnect();
        }
        updateSensorData();
    }
}

void updateSensorData() {
    int sensorValue = analogRead(SENSOR_PIN);  // Nur einmal aufrufen
    float moisture = map(sensorValue, 1024, 0, 0, 100);
    String timestamp = timeClient.getFormattedTime();

    sendMQTTData(moisture, timestamp);
}

void sendMQTTData(float moisture, const String& timestamp) {
    char topic[50], message[50];

    snprintf(topic, sizeof(topic), "%s/feuchtigkeit", mqtt_topic_prefix);
    snprintf(message, sizeof(message), "%.2f", moisture);
    client.publish(topic, message);

    snprintf(topic, sizeof(topic), "%s/timestamp", mqtt_topic_prefix);
    client.publish(topic, timestamp.c_str());
}