đŸ“ș GeekMagic SmartTV-Ultra als ESPHome-Statusmonitor (Glances / Home Assistant)

:television: GeekMagic SmartTV-Ultra als ESPHome-Statusmonitor (Glances / Home Assistant)

Hallo zusammen,

ich möchte euch hier mein ESPHome-Projekt „Small TV Ultra System-Monitor“ vorstellen.
Dabei handelt es sich um einen kompakten Systemmonitor, der wichtige Home-Assistant- und Serverdaten (CPU, RAM, Disk, Netzwerk, Uptime etc.) auf einem GeekMagic SmartTV-Ultra mit 240×240 ST7789 TFT-Display anzeigt – inklusive Alarmfunktion bei kritischen Werten.

Das Projekt ist vollstÀndig mit ESPHome umgesetzt und lÀsst sich ohne zusÀtzliche Programmierung nachbauen.


Statusanzeige -Seite 1-

.

Statusanzeige -Seite 2-

.


Alarmanzeige


:wrench: Hardware

FĂŒr dieses Projekt habe ich folgende Komponenten verwendet:

GeekMagic SmartTV-Ultra
bestehend aus:

  • ESP8266 (ESP-12E / NodeMCU / D1 Mini kompatibel)
  • 240×240 TFT Display mit ST7789 Controller (SPI)

mit geregelter Hintergrundbeleuchtung ĂŒber PWM und
WLAN-Anbindung an Home Assistant

TIP! - aktuell gibt es das Teil fĂŒr unter 10 € auf Aliexpress !

Bzw. dort einfach nach „GeekMagic SmartTV-Ultra“ suchen!


:brain: FunktionsĂŒbersicht

:bar_chart: Angezeigte Werte (aus Home Assistant / Glances)

Seite 1 – SystemĂŒbersicht

  • CPU-Auslastung
  • RAM-Auslastung
  • SWAP-Auslastung
  • Festplattennutzung

Seite 2 – Details / Netzwerk

  • Home-Assistant-Uptime
  • Anzahl aktiver Container
  • Netzwerk RX / TX (MB/s)

:police_car_light: Alarmfunktion

Ein Alarm wird automatisch aktiviert, wenn einer der folgenden Grenzwerte ĂŒberschritten wird:

  • CPU > 90 %
  • RAM > 90 %
  • Disk > 90 %
  • CPU-Temperatur > 80 °C

Im Alarmfall:

  • Roter Rahmen um das Display
  • Anzeige „ALARM“ statt Uhrzeit
  • Automatischer Seitenwechsel wird pausiert

:stopwatch: Seitenwechsel

  • Alle 5 Sekunden wird zwischen den Seiten gewechselt
  • Bei aktivem Alarm bleibt die Alarmseite sichtbar

:antenna_bars: WLAN-Anzeige

  • WLAN-SignalstĂ€rke wird als 3-Balken-Anzeige oben rechts dargestellt

:desktop_computer: Display & Layout

  • Auflösung: 240 × 240 Pixel
  • Farbtiefe: 16 Bit
  • Sehr kompakte und gut lesbare Darstellung
  • Fortschrittsbalken mit farblicher Statusanzeige:
    • :green_circle: GrĂŒn = OK
    • :orange_circle: Orange = Warnung
    • :red_circle: Rot = Kritisch

:three_o_clock: Zeit & Uhr

  • Uhrzeit ĂŒber SNTP
  • Zeitzone: Europe/Berlin
  • Anzeige im 24h-Format mit Sekunden

:house: Home Assistant Integration

Die Daten kommen direkt aus Home Assistant ĂŒber platform: homeassistant.
Ich nutze dafĂŒr u. a. Glances-Sensoren:

Beispiele:

sensor.homeassistant_cpu_auslastung
sensor.homeassistant_arbeitsspeicherauslastung
sensor.homeassistant_swapauslastung
sensor.homeassistant_config_datentragernutzung
sensor.homeassistant_cpu_temperatur
sensor.homeassistant_enp0s18_rx
sensor.homeassistant_enp0s18_tx

:warning: Wichtig:
Die Entity-IDs mĂŒssen ggf. an euer System angepasst werden!


:light_bulb: Besonderheiten im Code

  • Hardware-SPI mit 40 MHz fĂŒr flĂŒssige Darstellung
  • Eigenes Farb-Auswahl-Lambda je nach Warnstufe
  • BerĂŒcksichtigung eines bekannten ST7789-Offsets (sichtbarer Bereich = 232px)
  • Hintergrundbeleuchtung per PWM steuerbar
  • Automatischer Fallback-WLAN-AP bei Verbindungsproblemen

:page_facing_up: VollstÀndiger ESPHome Code

:backhand_index_pointing_right: Der komplette Code ist unten angehÀngt
(1:1 ĂŒbernehmbar – WLAN- und Sensor-IDs anpassen!)

ESPHome YAML anzeigen
esphome:
  name: "smarttv-ultra-systemmonitor"
  friendly_name: Smart-TV-Ultra-Systemmonitor
  platformio_options: 
    board_build.f_cpu: 160000000L

esp8266:
  board: esp12e

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "wJjGVc2mGLd/kswjPHMtYkn1KnRspM0/4ar+rT8mMSE="

ota:
  - platform: esphome
    password: "3d0483635128e3377abe302fc22dfe14"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Fallback Hotspot"
    password: "WjXuTG6ta1Nv"

# ───── SPI ─────
spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13
  interface: hardware
  id: spihwd

# ───── BACKLIGHT ─────
output:
  - platform: esp8266_pwm
    pin: GPIO5
    frequency: 1000 Hz
    id: pwm_output
    inverted: true

light:
  - platform: monochromatic
    name: "Display Backlight"
    output: pwm_output
    restore_mode: ALWAYS_ON

font:
  - file: "gfonts://Roboto"
    id: font_huge
    size: 50        # Uhr / Alarm

  - file: "gfonts://Roboto"
    id: font_large
    size: 28        # Hauptwerte (CPU/RAM)

  - file: "gfonts://Roboto"
    id: font_medium
    size: 22        # SekundÀrwerte

  - file: "gfonts://Roboto"
    id: font_small
    size: 18        # Labels

# ───── ZEIT ─────
time:
  - platform: sntp
    id: esptime
    timezone: Europe/Berlin

# ───── GLOBALS ─────
globals:
  - id: page
    type: int
    restore_value: no
    initial_value: '0'

  - id: alarm_active
    type: bool
    restore_value: no
    initial_value: 'false'

# ───── GLANCES SENSOREN (AUS HA) ─────
sensor:
  - platform: homeassistant
    id: gl_cpu
    entity_id: sensor.homeassistant_cpu_auslastung

  - platform: homeassistant
    id: gl_ram
    entity_id: sensor.homeassistant_arbeitsspeicherauslastung

  - platform: homeassistant
    id: gl_swap
    entity_id: sensor.homeassistant_swapauslastung

  - platform: homeassistant
    id: gl_disk
    entity_id: sensor.homeassistant_config_datentragernutzung

  - platform: homeassistant
    id: gl_load
    entity_id: sensor.homeassistant_cpu_last

  - platform: homeassistant
    id: gl_temp
    entity_id: sensor.homeassistant_cpu_temperatur

  # ───── SYSTEM STATUS ─────

  - platform: homeassistant
    id: gl_uptime
    entity_id: sensor.ha_uptime_sekunden


  - platform: homeassistant
    id: ha_containers
    entity_id: sensor.homeassistant_container_aktiv

  # ───── NETZWERK enp0s18 ─────
  - platform: homeassistant
    id: ha_rx
    entity_id: sensor.homeassistant_enp0s18_rx

  - platform: homeassistant
    id: ha_tx
    entity_id: sensor.homeassistant_enp0s18_tx



# ───── SEITENWECHSEL ─────
interval:
  - interval: 5s
    then:
      - lambda: |-
          if (!id(alarm_active)) {
            id(page) = (id(page) + 1) % 2;
          }

  - interval: 10s
    then:
      - lambda: |-
          bool alarm =
            id(gl_cpu).state  > 90 ||
            id(gl_disk).state > 90 ||
            id(gl_ram).state  > 80 ||
            id(gl_temp).state > 80;

          if (alarm && !id(alarm_active)) {
            // Alarm neu erkannt → sofort Seite 1 anzeigen
            id(page) = 0;
          }

          id(alarm_active) = alarm;


# ───── DISPLAY ─────
display:
  - platform: mipi_spi
    id: my_display
    model: st7789v
    spi_id: spihwd

    dimensions:
      width: 240
      height: 240

    dc_pin: GPIO0
    reset_pin: GPIO2
    invert_colors: true
    color_depth: 16
    spi_mode: mode3
    data_rate: 40000000
    buffer_size: 12.5%
    auto_clear_enabled: false

    lambda: |-
      // ============================================================
      // GRUNDLAGE
      // ============================================================
      it.fill(Color(0, 0, 0));   // Bildschirm löschen (schwarz)

      // ============================================================
      // ALARM-RAHMEN (GeekMagic / ST7789 Offset-Fix)
      // ============================================================
      if (id(alarm_active)) {
        int b = 6;
        int w = it.get_width();

        // empirisch ermittelter sichtbarer Bereich
        int visible_h = 232;

        // oben
        it.filled_rectangle(0, 0, w, b, Color(255, 0, 0));
        // unten (nach oben gezogen!)
        it.filled_rectangle(0, visible_h - b, w, b, Color(255, 0, 0));
        // links
        it.filled_rectangle(0, 0, b, visible_h, Color(255, 0, 0));
        // rechts
        it.filled_rectangle(w - b, 0, b, visible_h, Color(255, 0, 0));
      }


      // ============================================================
      // TOP BAR ─ Uhr links / WLAN rechts
      // ============================================================

      if (id(alarm_active)) {
        it.printf(
          10, 2,
          id(font_huge),
          Color(255, 0, 0),
          TextAlign::LEFT,
          "ALARM"
        );
      } else {
        it.strftime(
          10, 2,
          id(font_huge),
          Color(0,150,255),
          TextAlign::LEFT,
          "%H:%M:%S",
          id(esptime).now()
        );
      }


      // WLAN-SignalstÀrke (3 Balken, rechts oben)
      int rssi = WiFi.RSSI();
      int bars =
        (rssi > -55) ? 3 :
        (rssi > -65) ? 2 :
        (rssi > -75) ? 1 : 0;

      int bx = 210;   // rechter Rand
      int by = 40;    // Grundlinie
      int bw = 6;     // Balkenbreite
      int gap = 4;    // Abstand zwischen Balken

      for (int i = 0; i < 3; i++) {
        int h = (i + 1) * 6;
        it.filled_rectangle(
          bx + i * (bw + gap),
          by - h,
          bw,
          h,
          i < bars ? Color(0,200,0) : Color(40,40,40)
        );
      }

      // ============================================================
      // HILFSFUNKTION ─ Farbwahl nach Warn-/Kritischwert
      // ============================================================
      auto color_by_level = [](float v, float warn, float crit) {
        if (isnan(v)) return Color(120,120,120);   // unbekannt
        if (v >= crit) return Color(255, 0, 0);    // kritisch
        if (v >= warn) return Color(255,180,0);    // Warnung
        return Color(0,200,0);                     // OK
      };

      // ============================================================
      // SENSORWERTE (aus Home Assistant / Glances)
      // ============================================================
      float cpu  = id(gl_cpu).state;
      float ram  = id(gl_ram).state;
      float swap = id(gl_swap).state;
      float disk = id(gl_disk).state;

      // ============================================================
      // LAYOUT-KONSTANTEN
      // ============================================================
      int y0 = 75;    // erste Zeile
      int dy = 40;    // Zeilenabstand

      int x_label = 22;   // Labels links

      int x_bar = 80;     // Balkenstart
      int bar_w = 80;     // Balkenbreite
      int bar_h = 14;     // Balkenhöhe

      int x_value = 205;  // Zahlen rechtsbĂŒndig
      int x_pct   = 215;  // Prozentzeichen exakt untereinander

      // ============================================================
      // SEITE 0 ─ SYSTEMÜBERSICHT
      // ============================================================
      if (id(page) == 0) {

        int y = y0;

        // ---------- CPU ----------
        it.printf(x_label, y, id(font_small), Color(180,180,180), "CPU");

        it.filled_rectangle(x_bar, y+6, bar_w, bar_h, Color(40,40,40));
        it.filled_rectangle(
          x_bar, y+6,
          min(bar_w, (int)(bar_w * cpu / 100.0)),
          bar_h,
          color_by_level(cpu, 70, 90)
        );

        it.printf(x_value, y-3, id(font_large),
                  color_by_level(cpu, 70, 90),
                  TextAlign::RIGHT, "%.0f", cpu);
        it.print(x_pct, y-3, id(font_large), "%");

        // ---------- RAM ----------
        y += dy;
        it.printf(x_label, y, id(font_small), Color(180,180,180), "RAM");

        it.filled_rectangle(x_bar, y+6, bar_w, bar_h, Color(40,40,40));
        it.filled_rectangle(
          x_bar, y+6,
          min(bar_w, (int)(bar_w * ram / 100.0)),
          bar_h,
          color_by_level(ram, 75, 90)
        );

        it.printf(x_value, y-3, id(font_large),
                  color_by_level(ram, 75, 90),
                  TextAlign::RIGHT, "%.0f", ram);
        it.print(x_pct, y-3, id(font_large), "%");

        // ---------- SWAP ----------
        y += dy;
        it.printf(x_label, y, id(font_small), Color(180,180,180), "SWAP");

        it.filled_rectangle(x_bar, y+6, bar_w, bar_h, Color(40,40,40));
        it.filled_rectangle(
          x_bar, y+6,
          min(bar_w, (int)(bar_w * swap / 100.0)),
          bar_h,
          color_by_level(swap, 30, 60)
        );

        it.printf(x_value, y-3, id(font_large),
                  color_by_level(swap, 30, 60),
                  TextAlign::RIGHT, "%.0f", swap);
        it.print(x_pct, y-3, id(font_large), "%");

        // ---------- DISK ----------
        y += dy;
        it.printf(x_label, y, id(font_small), Color(180,180,180), "DISK");

        it.filled_rectangle(x_bar, y+6, bar_w, bar_h, Color(40,40,40));
        it.filled_rectangle(
          x_bar, y+6,
          min(bar_w, (int)(bar_w * disk / 100.0)),
          bar_h,
          color_by_level(disk, 70, 85)
        );

        it.printf(x_value, y-3, id(font_large),
                  color_by_level(disk, 70, 85),
                  TextAlign::RIGHT, "%.0f", disk);
        it.print(x_pct, y-3, id(font_large), "%");
      }

      // ─────────────────────────────
      // SEITE 1: DETAILS / NETZWERK
      // ─────────────────────────────
      else if (id(page) == 1) {

        int y  = 70;     // Start-Y
        int dy = 42;     // Zeilenabstand

        // ───── UPTIME ─────
        it.printf(22, y, id(font_small),
                  Color(180,180,180), "UPTIME");

        if (isnan(id(gl_uptime).state)) {
          it.printf(220, y-3, id(font_medium),
                    Color(120,120,120),
                    TextAlign::RIGHT, "--.- h");
        } else {
          it.printf(220, y-3, id(font_large),
                    Color(180,180,180),
                    TextAlign::RIGHT, "%.1f h",
                    id(gl_uptime).state / 3600.0);
        }

        // ───── CONTAINER ─────
        y += dy;
        it.printf(22, y, id(font_small),
                  Color(180,180,180), "CONTAINER");

        it.printf(215, y-3, id(font_large),
                  Color(0,200,200),
                  TextAlign::RIGHT, "%.0f",
                  id(ha_containers).state);

        // ───── RX ─────
        y += dy;
        float rx = id(ha_rx).state;

        it.printf(22, y, id(font_small),
                  Color(180,180,180), "RX");

        if (isnan(rx)) {
          it.printf(150, y-3, id(font_medium),
                    Color(120,120,120),
                    TextAlign::RIGHT, "--.--");
        } else {
          it.printf(150, y-3, id(font_large),
                    Color(0,180,255),
                    TextAlign::RIGHT, "%.2f", rx);
        }
        it.print(170, y, id(font_medium), "MB/s");

        // ───── TX ─────
        y += dy;
        float tx = id(ha_tx).state;

        it.printf(22, y, id(font_small),
                  Color(180,180,180), "TX");

        if (isnan(tx)) {
          it.printf(150, y-3, id(font_medium),
                    Color(120,120,120),
                    TextAlign::RIGHT, "--.--");
        } else {
          it.printf(150, y-3, id(font_large),
                    Color(0,255,180),
                    TextAlign::RIGHT, "%.2f", tx);
        }
        it.print(170, y, id(font_medium), "MB/s");
      }

---

:white_check_mark: Fazit

Mit relativ wenig (preiswerter) Hardware entsteht ein sehr ĂŒbersichtlicher Systemmonitor, der:

  • unabhĂ€ngig von Browsern lĂ€uft
  • sofort kritische ZustĂ€nde sichtbar macht
  • optisch gut ins Rack oder auf den Schreibtisch passt

Wie die Software auf das GerÀt kommt zeige ich in wenigen Tagen, wenn mein bestellter Nachschub eingetroffen ist! :innocent:

Fragen, VerbesserungsvorschlĂ€ge oder Nachbauten sind ausdrĂŒcklich willkommen :+1:

Viele GrĂŒĂŸe :waving_hand::waving_hand::waving_hand:

7 „GefĂ€llt mir“

Sehr cool, danke fĂŒr den schönen Beitrag. :slight_smile: :+1:

1 „GefĂ€llt mir“

Das finde ich auch schon mal sehr Interessant, bin gespammt was noch kommt.

Sehr schönes Projekt.
Das werde ich nachbauen, evtl. mit anderen Werten (EntitÀten).
Kommt das in’s Wiki?

Wollen wir schon mal etwas spoilern? :wink:

Es gibt da auch eine „Smalltv PRO“ Version mit ESP32 und einem Touchbutton, da geht dann noch etwas mehr „grafisch“ und „funktional“.

Habe mal einen geordert! :innocent:

Und dann gibt es ja auch noch → dieses Teil ← ,
gefÀllt mir auch ganz gut so mit Touch und auch mit Akku!

:waving_hand:

1 „GefĂ€llt mir“

Über das erste bin ich auch schon gestolpert. :slight_smile:

Ich bin gerade fertig mit dem planen meines Computertisch-Neubaus.

Die Elektronik die ich brauche ist schon da aber hinter den Plexiglas Scheiben ist noch platz
. :zany_face:

Die MP3 Boards sind ĂŒbrigens auch schon vor Weihnachten in meinem Briefkasten gelandet.

Ich werde Dich und Deine Ideen im Auge behalten
 grins

Nur so zur Verwirrung! Das Projekt finde ich auch „toll“ und ausbaufĂ€hig!

Da habe ich mir jetzt auch mal das Display zu bestellt, demnÀchst dann mehr!

Hi, ist ja auch ein geekmagic. Daher dachte ich, poste ich gleich hier. Habe mir heute doch noch den StÀnder gedruckt und mal bei normalen Licht, was das Licht aktuell so her gibt, ein Foto gemacht, da kann man schön den Transparenz Effekt sehen.

Den Code von Dr.Big habe ich auch weiter fĂŒr mich angepasst. Die Zeilen kann ich jetzt bequem ĂŒber das ESP GerĂ€t in HA einstellen und auch einen automatischen Zeilendurchlauf habe ich mit eingebaut.

Wer mag, hier der Code dazu:

esphome:
  name: "giftv2"
  friendly_name: GifTV2
  platformio_options: 
    board_build.f_cpu: 160000000L

esp8266:
  board: esp12e

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "DeinKEY="

ota:
  - platform: esphome
    password: "password"
wifi:
  ssid: "WiFi"
  password: "1234"
  min_auth_mode: WPA2

  ap:
    ssid: "giftv Fallback Hotspot"
    password: "12345678"

substitutions:
  icon_battery: "\U000F058E"
  icon_thermometer: "\U000F050F"

spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13
  interface: hardware
  id: spihwd

output:
  - platform: esp8266_pwm
    pin: GPIO05
    frequency: 1000 Hz
    id: pwm_output
    inverted: true

light:
  - platform: monochromatic
    name: "Backlight"
    output: pwm_output
    restore_mode: RESTORE_AND_ON

font:
  - file: "gfonts://Roboto"
    id: font_large
    size: 55
  - file: "gfonts://Roboto"
    id: font_medium  
    size: 35   
  - file: "gfonts://Roboto"
    id: font_small
    size: 25

time:
  - platform: sntp
    id: esptime
    timezone: Europe/Berlin
    servers:
      - pool.ntp.org


# GLOBALE VARIABLEN 
# ---------------------------------------------------------
globals:
  - id: current_page_index
    type: int
    initial_value: '0'


# SENSOREN 
# ---------------------------------------------------------
sensor:
  - platform: homeassistant
    id: aussen_temp
    entity_id: sensor.aussentemperatur_temperatur
    internal: false
    unit_of_measurement: "°C"

  - platform: homeassistant
    id: battery_soc
    entity_id: sensor.victron_system_battery_soc
    internal: false
    unit_of_measurement: "%"

  - platform: homeassistant
    id: daily_pv_energy
    entity_id: sensor.erzeugzung_taglich_kombination_solaranlagen
    internal: false
    unit_of_measurement: "kWh"

  - platform: homeassistant
    id: aktuell_pv_energy
    entity_id: sensor.total_solar_power_kombiniert
    internal: false
    unit_of_measurement: "W"

  - platform: homeassistant
    id: total_grid_power_w
    entity_id: sensor.total_power
    internal: false
    unit_of_measurement: "W"
  
  - platform: homeassistant
    id: daily_house_consumption
    entity_id: sensor.stromverbrauch_taglich
    internal: false
    unit_of_measurement: "kWh"

  - platform: wifi_signal
    id: wifi_strength
    name: "WiFi SignalstÀrke"
    unit_of_measurement: "dBm"
    update_interval: 60s


# SCHALTER (Auto-Rotate)
# ---------------------------------------------------------
switch:
  - platform: template
    name: "Automatisch Wechseln"
    id: switch_auto_rotate
    icon: "mdi:autorenew"
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF


# INTERVAL (Auto-Rotate)
# ---------------------------------------------------------
interval:
  - interval: 5s
    then:
      - if:
          condition:
            switch.is_on: switch_auto_rotate
          then:
            - lambda: |-
                id(current_page_index) += 1;                
                // 5 Optionen (0 bis 4: Solar W, Solar kWh, Netz W, Netz kWh, Temp)
                // Batterie und Wifi sind raus.
                if (id(current_page_index) > 4) {
                  id(current_page_index) = 0;
                }

# SELECT (Konfig Home Assistant)
# ---------------------------------------------------------
select:
  - platform: template
    name: "Anzeige Zeile 1"
    id: disp_select_line1
    optimistic: true
    restore_value: true
    initial_option: "Solar kW"
    options:
      - "Solar kW"
      - "Solar kWh"
      - "Netz kW"
      - "Netz kWh"
      - "Batterie"
      - "Temperatur"
      - "WiFi"
      - "Leer"

  - platform: template
    name: "Anzeige Zeile 2"
    id: disp_select_line2
    optimistic: true
    restore_value: true
    initial_option: "Solar kWh"
    options:
      - "Solar kW"
      - "Solar kWh"
      - "Netz kW"
      - "Netz kWh"
      - "Batterie"
      - "Temperatur"
      - "WiFi"
      - "Leer"

# DISPLAY
# ---------------------------------------------------------
display:
  - id: my_display
    platform: mipi_spi
    model: st7789v
    spi_id: spihwd
    dimensions:
      height: 240
      width: 240
    update_interval: 1s 
    buffer_size: 12.5%
    invert_colors: true
    dc_pin: GPIO00
    reset_pin: GPIO02
    color_depth: 16
    spi_mode: mode3
    data_rate: 40000000
    auto_clear_enabled: false

    transform:
      swap_xy: false
      mirror_x: true
      mirror_y: false

    lambda: |-
        it.fill(Color(0, 0, 0));

        // --- DEFINITIONEN ---
        int box_x = 30;
        int box_y = 105;
        int box_w = 90;
        int box_h = 30;

        int r1_lbl_y = box_y + box_h + 25; 
        int r1_val_y = box_y + box_h + 20; 
        int r1_lbl_x = box_x - 20;
        int r1_val_x = box_x + 135;

        int r2_lbl_y = box_y + box_h + 68; 
        int r2_val_y = box_y + box_h + 63; 
        int r2_lbl_x = box_x - 20;
        int r2_val_x = box_x + 135;

        
        // LOGIK AUTO-ROTATE
        // -----------------------------------------------------------
        // Batterie entfernt
        std::string options[] = {"Solar kW", "Solar kWh", "Netz kW", "Netz kWh", "Temperatur"};
        int options_count = 5;

        // *** FIX: Klammern () hinzugefĂŒgt ***
        std::string val_s1 = id(disp_select_line1).current_option();
        std::string val_s2 = id(disp_select_line2).current_option();
        
        std::string s1 = val_s1;
        std::string s2 = val_s2;

        if (id(switch_auto_rotate).state) {
            int idx1 = id(current_page_index) % options_count;
            int idx2 = (id(current_page_index) + 1) % options_count;
            
            s1 = options[idx1];
            s2 = options[idx2];
        }

        
        // 1. HEADER BEREICH 
        // -----------------------------------------------------------
        float temp = id(aussen_temp).state;
        Color temp_color = Color(200, 220, 255);
        if (!isnan(temp)) {
            it.printf(5, 5, id(font_small), temp_color, "%.1f°C", temp);
        } else {
            it.print(5, 5, id(font_small), Color(100, 100, 100), "--°C");
        }

        it.strftime(120, 65, id(font_large), Color(0, 0, 255), TextAlign::CENTER, "%H:%M", id(esptime).now());

        if (!global_api_server->is_connected()) {
             it.filled_circle(230, 230, 3, Color(255, 0, 0));
        }

        int rssi = WiFi.RSSI();
        int bars = 0;
        if (rssi > -50) bars = 3;
        else if (rssi > -60) bars = 2;
        else if (rssi > -70) bars = 1;
        
        int bx = 200; int by = 15; int bar_w = 6; int bar_gap = 5;
        for (int i = 0; i < 3; i++) {
            int h = (i + 1) * 3;
            Color col = (i < bars) ? Color(0, 255, 0) : Color(40, 40, 40);
            it.filled_rectangle(bx + i * (bar_w + bar_gap), by - h, bar_w, h, col);
        }

        
        // 2. BATTERIE
        // -----------------------------------------------------------
        float soc = id(battery_soc).state;
        if (isnan(soc)) { soc = 0; }

        int red = (int)(255 * (100 - soc) / 100);
        int green = (int)(255 * soc / 100);
        
        Color soc_color;
        if (soc == 0 && isnan(id(battery_soc).state)) {
            soc_color = Color(100, 100, 100);
        } else {
            soc_color = Color(red, green, 0);
        }

        it.rectangle(box_x, box_y, box_w, box_h, Color(255, 255, 255));
        it.rectangle(box_x + box_w, box_y + 7, 6, box_h - 14, Color(255, 255, 255));

        int fill_width = (int)(box_w * soc / 100.0);
        if (fill_width > 0) {
            it.filled_rectangle(box_x + 1, box_y + 1, fill_width - 2, box_h - 2, soc_color);
        }

        if (!isnan(id(battery_soc).state)) {
            it.printf(box_x + box_w + 25, box_y - 2, id(font_medium), soc_color, "%.0f%%", soc);
        } else {
            it.print(box_x + box_w + 25, box_y - 2, id(font_medium), Color(100,100,100), "--%");
        }

        
        // 3. VARIABLE ZEILEN
        // -----------------------------------------------------------

        // --- ZEILE 1 RENDER ---
        if (s1 == "Solar kW") {
             float val = id(aktuell_pv_energy).state / 1000.0;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Solar kW:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), Color(211, 117, 17), "%.1f", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        } 
        else if (s1 == "Solar kWh") {
             float val = id(daily_pv_energy).state;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Solar kWh:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), Color(72, 211, 17), "%.1f", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s1 == "Netz kW") {
             float val = id(total_grid_power_w).state / 1000.0;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Netz kW:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), Color(0, 255, 255), "%.1f", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s1 == "Netz kWh") {
             float val = id(daily_house_consumption).state;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Haus kWh:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), Color(255, 0, 255), "%.1f", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s1 == "Batterie") {
             float val = id(battery_soc).state;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Akku SoC:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), soc_color, "%.0f%%", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s1 == "Temperatur") {
             float val = id(aussen_temp).state;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "Aussen:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), temp_color, "%.1f", val);
             else it.print(r1_val_x, r1_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s1 == "WiFi") {
             float val = id(wifi_strength).state;
             it.print(r1_lbl_x, r1_lbl_y, id(font_small), Color::WHITE, "WiFi Signal:");
             if (!isnan(val)) it.printf(r1_val_x, r1_val_y, id(font_medium), Color::WHITE, "%.0f", val);
        }

        // --- ZEILE 2 RENDER ---
        if (s2 == "Solar kW") {
             float val = id(aktuell_pv_energy).state / 1000.0;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Solar kW:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), Color(211, 117, 17), "%.1f", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        } 
        else if (s2 == "Solar kWh") {
             float val = id(daily_pv_energy).state;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Solar kWh:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), Color(72, 211, 17), "%.1f", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s2 == "Netz kW") {
             float val = id(total_grid_power_w).state / 1000.0;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Netz kW:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), Color(0, 255, 255), "%.1f", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s2 == "Netz kWh") {
             float val = id(daily_house_consumption).state;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Haus kWh:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), Color(255, 0, 255), "%.1f", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s2 == "Batterie") {
             float val = id(battery_soc).state;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Akku SoC:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), soc_color, "%.0f%%", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s2 == "Temperatur") {
             float val = id(aussen_temp).state;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "Aussen:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), temp_color, "%.1f", val);
             else it.print(r2_val_x, r2_val_y, id(font_medium), Color(100,100,100), "--");
        }
        else if (s2 == "WiFi") {
             float val = id(wifi_strength).state;
             it.print(r2_lbl_x, r2_lbl_y, id(font_small), Color::WHITE, "WiFi Signal:");
             if (!isnan(val)) it.printf(r2_val_x, r2_val_y, id(font_medium), Color::WHITE, "%.0f", val);
        }
4 „GefĂ€llt mir“

Du hast doch echt zuviel Freizeit? :zany_face:
Bist halt ein „Guter“! :folded_hands:

Gerade entdeckt! Etwas anderer Ansatz, klingt aber interessant! :wink:

1 „GefĂ€llt mir“

schon eher die richtige GrĂ¶ĂŸe! :smiley:

1 „GefĂ€llt mir“

Wehe du bestellst den.. :wink: Aber sieht schon nett aus.

1 „GefĂ€llt mir“

Hier habe ich gerade bei ali entdeckt: Der kostet aktuell nur 7,99€ und hat sogar einen ESP32 statt wie bei unseren einen ESP8266 verbaut. Da könnte man dann sogar Kamera-Snapshots zeigen, wĂŒrde von der Leistung gehen.

mit integriertem 8 MB PSRAM

auch noch! Gleich mal zuschlagen! :wink:

Aber als KomplettSet kostet das Teil auch

18,59€

ist aber immer noch ok

Wie sieht das in der Nacht aus, wird das das ganze auch so angezeigt?

Ja, genauso wie am Tag! Ich baue gerade noch eine Helligkeitsautomatik rein, da kann man zwischen Sonnenuntergang → Aufgang eine reduzierte Helligkeit setzen!

3 „GefĂ€llt mir“

OK dann muss ich mir so ein Teil auch besorgen! :smiley:

1 „GefĂ€llt mir“

wisst ihr zufÀllig, ob man das GeekMagic SmartTV-Ultra schlag mich tot auch mit dem ESPHome Designer editieren kann? das wÀre ziemlich nice

im Video von @Smartrev Jörg bin ich ĂŒber den Designer gestolpert und dachte direkt an meine Geekmagic Displays

1 „GefĂ€llt mir“

Interessantes Projekt, kannte ich auch noch nicht und scheint auch noch recht neu zu sein. Sollte eigentlich damit funktionieren, ist ja schließlich ein ESP GerĂ€t und kann ĂŒber ESPHome in HA eingebunden werden.

Werde ich mir auch mal genauer anschauen. :slight_smile:

Ich sehe gerade da steht aktuell, dann wird es ohne weiteres nicht mit dem geekmagic klappen.

Hardware Support

Currently Supported:

Aber schaue mal hier, auch noch nicht so alt und habe ich mir auch gemerkt:

und das Thema dazu:
https://community.home-assistant.io/t/esphome-lvgl-ui-designer-editor-online/959428

1 „GefĂ€llt mir“

Bei LVGL sollte es dann aber zwingend ein ESP32 sein!

So ein Zufall aber auch, habe ich doch gerade einen SmartTV-”Pro” ĂŒberredet mit mir zu spielen!:laughing: Das ist die Variante mit ESP32 und Touch Button.

ich werde berichten 
.. :waving_hand:

2 „GefĂ€llt mir“