Werbung / Anzeige: Dieser Beitrag enthält Affiliate-Links (mit * markiert). Kaufst du über einen solchen Link, erhalte ich eine kleine Provision – für dich bleibt der Preis gleich.
Ein winziges 16×2-Display, das gerade mal 32 Zeichen anzeigen kann – und trotzdem ein komplettes Live-Dashboard? Genau das habe ich mit einem Raspberry Pi aus dem GeeekPi/Freenove Starter Kit * gebaut. Das LCD wechselt im Sekundentakt durch mehrere „Seiten“ und zeigt mir Aktienkurse, den Ertrag meiner Solaranlage und die wichtigsten System-Werte des Pi. In diesem Beitrag zeige ich dir Schritt für Schritt, wie du das nachbaust – inklusive des kompletten Scripts.

Was du dafür brauchst
- Raspberry Pi (hier: Pi 4) mit Raspberry Pi OS
- I2C-LCD1602 mit PCF8574-Backpack (im Kit enthalten)
- 4 Jumperkabel – mehr nicht, der I2C-Bus braucht nur SDA, SCL, VCC und GND
- Optional: ein 3D-gedruckter Display-Halter
🛒 Komplett-Set: Pi-Board, Breadboard, LCD, Sensoren & Kabel – das GeeekPi/Freenove Raspberry Pi Starter Kit * enthält alles, was du für dieses Projekt (und viele weitere) brauchst.
Schritt 1: I2C aktivieren
Das LCD wird über den I2C-Bus angesteuert. Aktiviere ihn einmalig mit sudo raspi-config → Interface Options → I2C → Yes. Danach hängst du deinen Benutzer noch in die passende Gruppe, damit du ohne sudo aufs Display zugreifen darfst:
sudo usermod -aG i2c $USER
pip3 install smbus2 gpiozero # falls noch nicht vorhanden
Schritt 2: Verkabelung (nur 4 Kabel)
| LCD-Pin | Raspberry-Pi-Pin |
|---|---|
| GND | Pin 6 (GND) |
| VCC | Pin 2 (5V) |
| SDA | Pin 3 (GPIO2 / SDA) |
| SCL | Pin 5 (GPIO3 / SCL) |
Die meisten dieser Backpacks sitzen auf der Adresse 0x27 (manche auf 0x3F). Falls dein Display dunkel bleibt, ist das der erste Wert, den du im Code anpasst.
Schritt 3: Der LCD-Treiber
Damit wir keine schweren Bibliotheken installieren müssen, steuern wir den HD44780-Controller mit einem kleinen eigenen Treiber direkt an. Speichere das als lcd_demo.py:
#!/usr/bin/env python3
# Programm 4 - LCD1602 (I2C) Text anzeigen
# I2C-Backpack PCF8574 an Adresse 0x27, HD44780-LCD im 4-Bit-Modus
import smbus2
from time import sleep
ADDR = 0x27
BUS = 1
EN = 0x04 # Enable-Bit
BACKLIGHT = 0x08 # Hintergrundbeleuchtung an
class LCD:
def __init__(self, addr=ADDR, bus=BUS):
self.addr = addr
self.bus = smbus2.SMBus(bus)
for v in (0x33, 0x32, 0x28, 0x0C, 0x06, 0x01):
self._cmd(v)
sleep(0.005)
def _write_byte(self, data):
self.bus.write_byte(self.addr, data | BACKLIGHT)
# Enable-Puls
self.bus.write_byte(self.addr, data | EN | BACKLIGHT)
sleep(0.0005)
self.bus.write_byte(self.addr, (data & ~EN) | BACKLIGHT)
sleep(0.0001)
def _send(self, value, mode):
self._write_byte(mode | (value & 0xF0))
self._write_byte(mode | ((value << 4) & 0xF0))
def _cmd(self, value):
self._send(value, 0)
def text(self, s, line=1):
self._cmd(0x80 if line == 1 else 0xC0)
for ch in s.ljust(16)[:16]:
self._send(ord(ch), 1)
def clear(self):
self._cmd(0x01)
sleep(0.002)
if __name__ == "__main__":
lcd = LCD()
lcd.clear()
lcd.text("Freenove Kit :)", 1)
lcd.text("Hallo Tim!", 2)
print("Text steht jetzt auf dem LCD (bleibt nach Programmende stehen).")
Schritt 4: Das Dashboard-Script
Jetzt kommt das Herzstück. Das Script lcd_stocks.py holt sich die Aktienkurse von Yahoo Finance (mit Wochen-Performance, Montag = 0 %), die Solarwerte über die FoxESS-OpenAPI und die System-Daten direkt aus /proc. Jede „Seite“ ist eine kleine Funktion, die zwei Display-Zeilen zurückgibt – die Hauptschleife blättert dann der Reihe nach durch:
#!/usr/bin/env python3
# Programm 7 - LCD-Info-Rotation: Aktien (Yahoo) + Solaranlage (FoxESS)
# Zeigt abwechselnd mehrere "Panels" mit Live-Werten. Sauberer Stop leert das LCD.
# Hinweis: SpaceX = Yahoo-Ticker SPCX (ISIN US84615Q1031).
from lcd_demo import LCD
import urllib.request, json, time, signal, sys, os, hashlib, datetime, socket, shutil, threading
# ---- Aktien (Yahoo Finance) ----
# Prozentwert = Performance seit Wochenstart (Montag = 0 %).
STOCKS = [
("SPCX", "SpaceX (SPCX) $"),
("VWCE.DE", "FTSE All-World"), # Vanguard FTSE All-World Acc, ISIN IE00BK5BQT80
("SXRV.DE", "NASDAQ 100"), # iShares NASDAQ 100 Acc, ISIN IE00B53SZB19
]
SHOW = 4 # Sekunden pro Panel
STOCK_REFRESH = 60
# WICHTIG: Alle Netzwerk-Abrufe laufen in einem Hintergrund-Thread (updater()).
# Die Render-Funktionen lesen NUR aus dem Cache -> die Anzeige (und die Uhr)
# blockiert nie, auch wenn Yahoo/FoxESS langsam sind.
_scache = {} # sym -> (preis, wochen_chg, timestamp)
def _weekly_change(res, p, m):
# Referenz = erster Handelstag der laufenden Woche (Montag) -> Montag startet bei 0 %
try:
ts = res["timestamp"]; q = res["indicators"]["quote"][0]
opens = q["open"]; closes = q["close"]
today = datetime.date.today()
monday = today - datetime.timedelta(days=today.weekday())
ref = None
for t, o, c in zip(ts, opens, closes):
if datetime.date.fromtimestamp(t) >= monday:
ref = o if o is not None else c
if ref is not None:
break
if ref is None:
ref = m.get("chartPreviousClose")
return 100 * (p - ref) / ref if ref else 0.0
except Exception:
pc = m.get("chartPreviousClose") or m.get("previousClose")
return 100 * (p - pc) / pc if pc else 0.0
def _fetch_stock(sym):
# Netzwerk-Abruf, NUR vom Hintergrund-Thread aufgerufen. Aktualisiert _scache.
url = "https://query1.finance.yahoo.com/v8/finance/chart/" + sym + "?range=1mo&interval=1d"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
d = json.load(urllib.request.urlopen(req, timeout=10))
res = d["chart"]["result"][0]
m = res["meta"]
p = m["regularMarketPrice"]
chg = _weekly_change(res, p, m)
_scache[sym] = (p, chg, time.time())
def stock_render(sym, label):
# Liest nur aus dem Cache -> blockiert nie.
if sym in _scache:
return label, "%.2f %+.2f%%" % (_scache[sym][0], _scache[sym][1])
return label, "laedt..."
# ---- Solaranlage (FoxESS OpenAPI) ----
FOX_CONFIG = os.path.expanduser("~/led_demo/fox_config.json")
FOX_REFRESH = 60
_fcache = {}
def _fox_cfg():
try:
with open(FOX_CONFIG) as f:
return json.load(f)
except Exception:
return None
def _fetch_fox():
# Netzwerk-Abruf, NUR vom Hintergrund-Thread aufgerufen. Aktualisiert _fcache.
now = time.time()
cfg = _fox_cfg()
if not cfg:
return
token = cfg["apiKey"]; sn = cfg["deviceSN"]
path = "/op/v0/device/real/query"
ts = str(int(now * 1000))
sig = hashlib.md5((path + "\\r\\n" + token + "\\r\\n" + ts).encode()).hexdigest()
headers = {"token": token, "timestamp": ts, "signature": sig,
"lang": "en", "User-Agent": "Mozilla/5.0",
"Content-Type": "application/json"}
body = json.dumps({"sn": sn,
"variables": ["pvPower", "SoC", "feedinPower",
"loadsPower", "gridConsumptionPower"]}).encode()
req = urllib.request.Request("https://www.foxesscloud.com" + path,
data=body, headers=headers, method="POST")
r = json.load(urllib.request.urlopen(req, timeout=15))
if r.get("errno") != 0:
return
vals = {x["variable"]: x["value"] for x in r["result"][0]["datas"]}
def pick(*names):
for n in names:
if n in vals:
return vals[n]
return None
d = {"pv": pick("pvPower"), "soc": pick("SoC", "SoC_1"),
"feed": pick("feedinPower"), "load": pick("loadsPower"),
"grid": pick("gridConsumptionPower")}
_fcache["d"] = d; _fcache["ts"] = now
def solar_render():
# Liest nur aus dem Cache -> blockiert nie.
d = _fcache.get("d")
if not d or d.get("pv") is None:
return "Solar", "laedt..."
l1 = "Solar:%6.2fkW" % d["pv"]
soc = d.get("soc")
l2 = ("Akku: %.0f%%" % soc) if soc is not None else "Akku: n/a"
return l1, l2
def house_render():
d = _fcache.get("d")
if not d or d.get("load") is None:
return "Haus", "laedt..."
l1 = "Haus:%7.2fkW" % d["load"]
feed = d.get("feed") or 0.0
grid = d.get("grid") or 0.0
if feed > 0.01:
l2 = "Einsp:%6.2fkW" % feed
else:
l2 = "Bezug:%6.2fkW" % grid
return l1, l2
# ---- Raspberry-Pi-Systemdaten (IP, SD-Karte, CPU, RAM) ----
def get_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80)) # nichts gesendet - nur Route ermitteln
ip = s.getsockname()[0]
except Exception:
ip = "keine IP"
finally:
s.close()
return ip
def ip_render():
return "Raspberry Pi IP:", get_ip()
def sd_render():
try:
t, u, f = shutil.disk_usage("/")
gb = 1024 ** 3
l1 = "SD: %.0f%% belegt" % (100.0 * u / t)
l2 = "%.1f/%.0f GB frei" % (f / gb, t / gb)
return l1, l2
except Exception:
return "SD-Karte", "n/a"
_cpu_prev = [None, None] # total, idle (fuer Delta-Berechnung)
def cpu_render():
try:
with open("/proc/stat") as fh:
vals = list(map(int, fh.readline().split()[1:]))
total = sum(vals)
idle = vals[3] + vals[4] # idle + iowait
pt, pi = _cpu_prev
_cpu_prev[0], _cpu_prev[1] = total, idle
if pt is None:
usage = 0.0
else:
dt = total - pt; di = idle - pi
usage = 100.0 * (dt - di) / dt if dt else 0.0
return "CPU-Auslastung:", "%5.1f %%" % usage
except Exception:
return "CPU-Last", "n/a"
def mem_render():
try:
info = {}
with open("/proc/meminfo") as fh:
for line in fh:
k, v = line.split(":")
info[k] = int(v.split()[0]) # kB
total = info["MemTotal"]
avail = info.get("MemAvailable", info["MemFree"])
used = total - avail
mb = 1024.0
l1 = "RAM: %.0f%% voll" % (100.0 * used / total)
l2 = "%.1f/%.1f GB" % (used / mb / 1024, total / mb / 1024)
return l1, l2
except Exception:
return "Arbeitsspeicher", "n/a"
# ---- Uhr (Wochentag, Datum, Uhrzeit) ----
WEEKDAYS = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
def clock_render():
now = datetime.datetime.now()
l1 = "%s %s" % (WEEKDAYS[now.weekday()], now.strftime("%d.%m.%Y"))
l2 = now.strftime("%H:%M:%S Uhr")
return l1, l2
# ---- Panel-Liste zusammenstellen ----
def build_panels():
panels = [(lambda s=s, l=l: stock_render(s, l)) for s, l in STOCKS]
if _fox_cfg():
panels.append(solar_render)
panels.append(house_render)
# Raspberry-Pi-Systemdaten
panels.append(ip_render)
panels.append(sd_render)
panels.append(cpu_render)
panels.append(mem_render)
panels.append(clock_render)
return panels
def updater():
# Hintergrund-Thread: holt Aktien-/Solarwerte, ohne die Anzeige zu blockieren.
have_fox = _fox_cfg() is not None
while True:
now = time.time()
for sym, _label in STOCKS:
if sym not in _scache or now - _scache[sym][2] >= STOCK_REFRESH:
try:
_fetch_stock(sym)
except Exception:
pass
if have_fox and ("ts" not in _fcache or time.time() - _fcache["ts"] >= FOX_REFRESH):
try:
_fetch_fox()
except Exception:
pass
time.sleep(3)
def main():
lcd = LCD()
def stop(*args):
lcd.clear()
print("Info-Anzeige gestoppt, LCD geleert.")
sys.exit(0)
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
threading.Thread(target=updater, daemon=True).start()
panels = build_panels()
print("Info-Anzeige laeuft (%d Panels). Stoppen mit: bash ~/led_demo/off-lcd-stocks.sh" % len(panels))
i = 0
while True:
panel = panels[i % len(panels)]
for _ in range(SHOW): # pro Sekunde neu zeichnen -> Uhr tickt live
l1, l2 = panel()
lcd.text(l1.ljust(16)[:16], 1)
lcd.text(l2.ljust(16)[:16], 2)
time.sleep(1)
i += 1
if __name__ == "__main__":
main()
💡 Solaranlage optional: Die FoxESS-Panels erscheinen nur, wenn eine Datei ~/led_demo/fox_config.json mit deinem apiKey und der deviceSN existiert. Ohne diese Datei läuft das Script ganz normal – nur ohne die beiden Solar-Seiten. Deinen API-Key bekommst du in der FoxESS-Cloud unter User Profile → API Management.
Schritt 5: Starten
Beide Dateien liegen im selben Ordner. Dann einfach starten:
python3 lcd_stocks.py
Für den Dauerbetrieb startest du es im Hintergrund (setsid python3 lcd_stocks.py &) oder legst dir einen systemd-Service an, damit das Dashboard nach jedem Boot automatisch läuft.
Das Ergebnis im Video
▶ Direkt auf YouTube ansehen: youtu.be/yCtORBgBHNk
Häufige Fragen
Funktioniert das mit jedem Raspberry Pi?
Mein Display bleibt leer oder zeigt nur Blöcke. Woran liegt das?
i2cdetect -y 1, ob das Modul als 0x27 oder 0x3F auftaucht, und trage den Wert in lcd_demo.py bei ADDR ein. Zweithäufigste Ursache: VCC am 3,3-V- statt am 5-V-Pin.Brauche ich zwingend eine Solaranlage?
fox_config.json werden die Solar-Seiten einfach übersprungen. Aktien, System-Werte und Uhr funktionieren unabhängig davon.Sind die Aktienkurse in Echtzeit?
Quellen & Doku:
- GeeekPi/Freenove Raspberry Pi Starter Kit (Amazon) *
- Raspberry Pi – I2C aktivieren (offizielle Doku)
- FoxESS OpenAPI (FoxESS Cloud)
Werbung: Dieser Beitrag enthält Affiliate-Links (mit * markiert). Kaufst du über einen solchen Link, erhalte ich eine kleine Provision – für dich ändert sich am Preis nichts. Kurs- und Finanzdaten dienen nur zur Demonstration und sind keine Anlageberatung.