So hier nun der versprochene Code!
Es war eigentlich mal als „Füllstandsanzeige für eine Zisterne“ gemacht!
Aber macht auch als „SOC Anzeige“ einen guten Job!
Ich habe es nochmal überarbeiten lassen und jetzt schaut es so aus:
ESPHome – SOC LED-Balken (10 × WS2812B)

oder so: (12 LED Ring, habe ich bei der Zisterne verwendet!)

0 – 9 % LED 0 (pulsierend rot) | Warnung
10 – 19 % LED 0 | Rot
20 – 29 % LED 0 – 1 | Rot → Gelb
50 – 59 % LED 0 – 4 | Gelb
80 – 89 % LED 0 – 7 | Gelb → Grün
100 % LED 0 – 9 | Grün
# =============================================================================
# ESPHome – SOC LED-Balken (10 × WS2812B)
#
# Hardware:
# - Standard: ESP32 DevKit (38-Pin, ESP32-D0WDQ6), Datenleitung GPIO5
# - Alternativ: ESP32-C3 SuperMini, Datenleitung GPIO4
# (siehe auskommentierte Blöcke in "substitutions" und "esp32")
#
# LED-Plattform: esp32_rmt_led_strip (empfohlen für ESP32 + ESP-IDF)
# - Es wird AUSSCHLIESSLICH die offizielle light.addressable_set-Action genutzt.
#
# ESPHome >= 2026.1, Framework: ESP-IDF
# =============================================================================
substitutions:
device_name: "esp-led-soc"
friendly_name: "ESP SOC LED Bar"
soc_entity_id: "input_number.pseudo_soc" # ← einzige nötige Anpassung
led_data_pin: "GPIO5" # ESP32 DevKit
# ---------------------------------------------------------------------------
# Alternative: ESP32-C3 SuperMini
# 1) led_data_pin auf GPIO4 ändern
# 2) In "esp32:" board auf "esp-c3-devkitm-1" umstellen
# ---------------------------------------------------------------------------
# led_data_pin: "GPIO4"
# ---------------------------------------------------------------------------
led_count: "10" # Anzahl LEDs im Streifen
update_interval: "60s"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
min_version: 2026.1.0
on_boot:
priority: -100
then:
- light.turn_off: soc_led_bar
- script.execute: update_leds
esp32:
board: esp32dev
framework:
type: esp-idf
# ---------------------------------------------------------------------------
# Alternative: ESP32-C3 SuperMini
# ---------------------------------------------------------------------------
# board: esp-c3-devkitm-1
# framework:
# type: esp-idf
# ---------------------------------------------------------------------------
logger:
level: INFO
api:
encryption:
key: !secret api_encryption_key # hier API Key definieren
reboot_timeout: 15min
ota:
- platform: esphome
password: !secret ota_password # hier OTA Passwort definieren
wifi:
ssid: !secret wifi_ssid # hier WLAN-SSID definieren
password: !secret wifi_password # hier WLAN-Passwort definieren
ap:
password: !secret ap_password # hier AP-Passwort definieren
captive_portal:
# -----------------------------------------------------------------------------
# Globale Variablen für SOC-Wert, aktive LEDs, interpolierte Farbe und Helligkeit.
# -----------------------------------------------------------------------------
globals:
- id: soc_value
type: float
initial_value: "0.0"
restore_value: false
- id: calc_active
type: int
initial_value: "0"
restore_value: false
- id: calc_r
type: float
initial_value: "0.0"
restore_value: false
- id: calc_g
type: float
initial_value: "0.0"
restore_value: false
- id: calc_b
type: float
initial_value: "0.0"
restore_value: false
- id: calc_brightness
type: float
initial_value: "0.5"
restore_value: false
# -----------------------------------------------------------------------------
# Helligkeits-Slider (erscheint automatisch als number-Entität in HA)
# -----------------------------------------------------------------------------
number:
- platform: template
name: "Helligkeit"
id: led_helligkeit
min_value: 1
max_value: 100
step: 1
initial_value: 50
restore_value: false
unit_of_measurement: "%"
icon: "mdi:brightness-6"
mode: slider
# set_action (NICHT on_value) ist entscheidend:
# - on_value feuert AUCH beim internen publish_state(50) waehrend setup()
# -> das ist die Boot-Race-Condition, die den LED-Buffer crasht
# ("StoreProhibited" in ESPRangeView::set_red) und einen OTA-Rollback
# auf die alte Firmware ausloest.
# - set_action feuert NUR, wenn HA den Slider bewegt -> genau das, was
# wir wollen.
optimistic: true
set_action:
- script.execute: update_leds
# -----------------------------------------------------------------------------
# SOC-Sensor von Home Assistant
# -----------------------------------------------------------------------------
sensor:
- platform: homeassistant
id: soc_sensor
entity_id: ${soc_entity_id}
on_value:
then:
- lambda: |-
id(soc_value) = x;
if (isnan(x)) {
id(soc_text).publish_state("--");
} else {
char buf[16];
snprintf(buf, sizeof(buf), "%.0f %%", x);
id(soc_text).publish_state(buf);
}
- script.execute: update_leds
# -----------------------------------------------------------------------------
# Textuelle Anzeige des SOC, z. B. "73 %"
# -----------------------------------------------------------------------------
text_sensor:
- platform: template
id: soc_text
name: "Batterie SOC"
icon: "mdi:battery"
update_interval: never
# -----------------------------------------------------------------------------
# Sicherheits-Intervall: alle 60 s LEDs neu berechnen,
# falls HA keinen neuen Wert sendet.
# Blink-Intervall (Low-Battery-Warnung): toggelt LED 0 alle 500 ms,
# solange SOC < 10 %. Helligkeit folgt dem Slider (calc_brightness).
# -----------------------------------------------------------------------------
interval:
- interval: ${update_interval}
then:
- script.execute: update_leds
- interval: 100ms
then:
- if:
condition:
lambda: 'return id(soc_value) < 10.0f;'
then:
# LED 0 dauerhaft rot, Helligkeit pulsiert sinusfoermig
# ("Atmen") zwischen 10 % und 100 % des Slider-Werts.
# Periode: 2000 ms (1 s hell, 1 s dunkel), keine harte An/Aus-
# Kante -> kein Flackern, kein "global off"-Bug.
- light.addressable_set:
id: soc_led_bar
range_from: 0
range_to: 0
red: 1.0
green: 0.0
blue: 0.0
# t in [0,1) -> sin(2*pi*t) in [-1,1]
# -> 0.5+0.5*sin(...) in [0,1] (sanfter 0%-100%-Verlauf)
# -> (0.1 + 0.9 * ...) in [0.1, 1.0] (geclamped, lebt immer)
# -> * calc_brightness (Slider folgt)
color_brightness: !lambda |-
float t = (millis() % 2000) / 2000.0f;
float pulse = 0.5f + 0.5f * sin(6.2831853f * t);
return id(calc_brightness) * (0.1f + 0.9f * pulse);
# -----------------------------------------------------------------------------
# Script: berechnet aktive LEDs + interpolierte Farbe und schickt alles
# an den LED-Streifen (ausschließlich über light.addressable_set).
# -----------------------------------------------------------------------------
script:
- id: update_leds
# mode: queued statt restart
# - restart: cancelt den laufenden Run sofort, was bei schnellen
# SOC-Aenderungen zu abgeschnittenen addressable_set-Calls und
# damit zu Fehlfarben fuehren kann.
# - queued: jeder Run wird sauber zu Ende gefuehrt, bevor der
# naechste startet. Verhindert Mischfarben durch halbfertige
# RMT-Frames und sorgt fuer konsistente LED-Zustaende.
mode: queued
max_runs: 5
then:
- lambda: |-
// 1) SOC clamping
float soc = id(soc_value);
if (isnan(soc)) soc = 0.0f;
if (soc < 0.0f) soc = 0.0f;
if (soc > 100.0f) soc = 100.0f;
// 2) Anzahl aktiver LEDs (10 % pro LED)
int active = (int)(soc / 10.0f);
if (active > 10) active = 10;
if (active < 0) active = 0;
// 3) Farbinterpolation (linear):
// 0 % → Rot (255, 0, 0)
// 50 % → Gelb (255, 180, 0)
// 100 % → Grün ( 0, 255, 0)
float r = 0.0f, g = 0.0f, b = 0.0f;
if (soc <= 50.0f) {
// 0..50 %: Rot → Gelb (R konstant, G 0 → 180)
r = 1.0f;
g = (180.0f * (soc / 50.0f)) / 255.0f;
} else {
// 50..100 %: Gelb → Grün (R 255 → 0, G 180 → 255)
float t = (soc - 50.0f) / 50.0f; // 0..1
r = 1.0f - t;
g = (180.0f + (255.0f - 180.0f) * t) / 255.0f; // 0.706 → 1.0
}
// 4) Helligkeit aus Slider EINMALIG hier auslesen und cachen.
// (Wuerden wir erst im addressable_set-Lambda lesen, koennte
// der Wert zwischen Berechnung und Setzen kippen -> Flackern
// und gelegentliche Fehlfarben.)
id(calc_brightness) = id(led_helligkeit).state / 100.0f;
// 5) Ergebnisse in Globals ablegen (für addressable_set-Lambdas)
id(calc_active) = active;
id(calc_r) = r;
id(calc_g) = g;
id(calc_b) = b;
- if:
condition:
lambda: 'return id(calc_active) == 0;'
then:
# SOC < 10 %: LEDs 1..9 ausschalten. LED 0 wird vom
# Blink-Intervall (siehe interval:-Block oben) toggelt.
- light.addressable_set:
id: soc_led_bar
range_from: 1
range_to: 9
red: 0
green: 0
blue: 0
color_brightness: 0.001
else:
# SOC >= 10 %: normale Anzeige
# 1) aktive LEDs (0 .. active-1) in interpolierter Farbe setzen
- light.addressable_set:
id: soc_led_bar
range_from: 0
range_to: !lambda 'return id(calc_active) - 1;'
red: !lambda 'return id(calc_r);'
green: !lambda 'return id(calc_g);'
blue: !lambda 'return id(calc_b);'
# Helligkeit aus dem im Haupt-Lambda gecachten Wert
# (Slider darf sich waehrend des Updates nicht auswirken)
color_brightness: !lambda 'return id(calc_brightness);'
# 2) Kurze Pause: RMT-Encoder Zeit geben, den aktiven Range zu
# rendern, bevor der Rest ueberschrieben wird. Bei 10 LEDs
# reichen 5 ms voellig aus, vermeidet aber Mischfarben,
# die bei zwei direkt aufeinander folgenden addressable_set-
# Aufrufen auftreten koennen.
- delay: 5ms
# 3) Verbleibende LEDs (active .. num_leds-1) explizit loeschen,
# damit sie bei fallendem SOC nicht weiter leuchten.
# Wird uebersprungen, wenn active == num_leds (sonst leerer Range).
# color_brightness: 0.001 (statt 0) ist explizit „nahezu aus"
# und loest den „global off"-Bug mancher RMT-LED-Treiber aus.
- if:
condition:
lambda: 'return id(calc_active) < ${led_count};'
then:
- light.addressable_set:
id: soc_led_bar
range_from: !lambda 'return id(calc_active);'
range_to: !lambda 'return ${led_count} - 1;'
red: 0
green: 0
blue: 0
color_brightness: 0.001
# -----------------------------------------------------------------------------
# LED-Streifen (esp32_rmt_led_strip – empfohlen mit ESP-IDF)
# -----------------------------------------------------------------------------
light:
- platform: esp32_rmt_led_strip
id: soc_led_bar
name: "SOC LED Bar"
pin: ${led_data_pin}
num_leds: ${led_count}
chipset: WS2812
rgb_order: GRB
restore_mode: ALWAYS_OFF
Es ist eigentlich alles wichtige entsprechend dokumentiert, falls dir was unklar ist dann einfach fragen!
Du kannst dir einen „Pseudo-SOC“ zum Testen anlegen:
Pseudo SOC Slider in Home Assistant einrichten
-
In Home Assistant in Einstellungen
-
→ Geräte & Dienste → oben rechts Helfer
-
Unten rechts auf + Erstellen klicken → Zahlenwert Eingabe
-
Felder ausfüllen:
| Feld |
Wert |
| Name |
Pseudo SOC |
| Minimalwert |
0 |
| Maximalwert |
100 |
| Schrittweite |
10 |
| Einheit |
% |
| Anzeigemodus |
Schieberegler |
-
Auf Erstellen klicken – fertig!
-
In der ESPHome YAML diese Zeile anpassen:
(ist per default schon gesetzt!)
soc_entity_id: "input_number.pseudo_soc"
Danach neu flashen → Slider in HA bewegen → LEDs reagieren sofort.