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
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!
FunktionsĂŒbersicht
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)
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
Seitenwechsel
- Alle 5 Sekunden wird zwischen den Seiten gewechselt
- Bei aktivem Alarm bleibt die Alarmseite sichtbar
WLAN-Anzeige
- WLAN-SignalstÀrke wird als 3-Balken-Anzeige oben rechts dargestellt
Display & Layout
- Auflösung: 240 à 240 Pixel
- Farbtiefe: 16 Bit
- Sehr kompakte und gut lesbare Darstellung
- Fortschrittsbalken mit farblicher Statusanzeige:
GrĂŒn = OK
Orange = Warnung
Rot = Kritisch
Zeit & Uhr
- Uhrzeit ĂŒber SNTP
- Zeitzone:
Europe/Berlin - Anzeige im 24h-Format mit Sekunden
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
Wichtig:
Die Entity-IDs mĂŒssen ggf. an euer System angepasst werden!
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
VollstÀndiger ESPHome Code
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");
}
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! ![]()
Fragen, VerbesserungsvorschlĂ€ge oder Nachbauten sind ausdrĂŒcklich willkommen ![]()
Viele GrĂŒĂe ![]()
![]()
![]()







