#Hardware · 11 Min. Lesezeit · Tim Rinkel

Raspberry Pi Live-Dashboard: Aktien, Solar & System-Stats auf dem LCD1602

Raspberry Pi Live-Dashboard: Aktien, Solar & System-Stats auf dem LCD1602

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.

Raspberry Pi mit LCD1602 zeigt SpaceX-Aktienkurs
Das LCD1602 am Raspberry Pi – hier mit dem aktuellen SpaceX-Kurs.

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-configInterface OptionsI2CYes. 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?
Ja. Das Script nutzt nur den I2C-Bus und Standard-Python – vom Pi Zero bis zum Pi 5 läuft es überall, wo das Raspberry Pi OS installiert ist.
Mein Display bleibt leer oder zeigt nur Blöcke. Woran liegt das?
Meist an der I2C-Adresse. Prüfe mit 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?
Nein. Ohne die Datei fox_config.json werden die Solar-Seiten einfach übersprungen. Aktien, System-Werte und Uhr funktionieren unabhängig davon.
Sind die Aktienkurse in Echtzeit?
Sie kommen von Yahoo Finance und sind – je nach Börse – leicht verzögert. Für ein Hobby-Dashboard völlig ausreichend. Das ist keine Anlageberatung.

Quellen & Doku:

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.

Kommentar hinterlassen

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