ESPHome Smartmeter zum Auslesen von zwei kaskadierten Stromzählern inkl. Shelly3em Emulation

Hi,

Wir haben zuhause 2 Stromzähler mit IR Schnittstelle verbaut. Der eine zählt das gesamte Haus (Wärmepumpe und Haushalt), der andere nur den Haushalt. Warum? Damit zwei verschiedene Stromtarife für die Wärmepumpe und Haushalt möglich sind, jedoch beide Geräte von der PV Anlage profitieren können. Die Daten hätte ich natürlich gern in HomeAssistant. Zudem sollen die Daten an einen AC gekoppelten Speicher von Marstek weitergegeben werden.

Mein Projekt, Watt Wächter getauft, basiert auf einem ESP32 DevKitC, mit USB C zur Stromversorgung und externer Antenne. Notwendig, da das Gerät im Schaltschrank verbaut ist. Kann im Gehäuse nach rechts oder links montiert werden, wie Platz vorhanden ist. Wir haben eine Steckdose auf der Hut-Schiene zur Stromversorgung installiert. Es sind zwei IR Lese-Schreib-Köpfe an dem Gerät angeschlossen. GPIO 4/5 und 26/27. Zum Anschließen habe ich eine Platine mit Schraubterminals designt, dort ist nichts weiter drauf außer die 2 Terminals, mit den GPIO, GND und 3,3V Verbindungen. Damit die Stromzähler (bei uns Digimeto) über die IR Schnittstelle die Daten ausgeben, musst zunächst ein Code vom Stromversorger beantragt werden. Die Obis Codes können in der ESPHome Config so eingepflegt werden, wie sie im Handbuch der Stromzähler stehen. Hier ist keine Umrechnung wie bei Tasmota im Script (zumindest war es vor 2 Jahren so) notwendig. Ausgelesen wird bei jedem Zähler die aktuelle Leistung, sowie der eingespeiste und bezogene Gesamtwert.

Die Werte subtrahiere ich dann direkt noch, um die Wärmepumpe einzelnd als Entität an HomeAssistant übergeben zu können (Bezug und Leistung).

Ganz cool ist auch die Weboberfläche von ESPHome, dadurch könnte das Gerät auch ohne HomeAssistant arbeiten.

Damit war ich soweit dann erst einmal ganz zufrieden. Ich habe die Leistung des gesamten Haus von HomeAssistant mithilfe des BT2500 (Shelly3EM Emulator) Addon an den Marstek Venus E weitergeleitet. War jedoch dadurch von HomeAssistant abhängig. Bis ich zufällig gefunden habe, das jemand (danke an Jibbo) die Shelly Emulation direkt mit ESPHome umgesetzt hat. Das habe ich übernommen. Heißt der ESPHome nimmt die Anfragen vom Marstek entgegen und schickt eine Antwort, wie sie auch vom Shelly3em kommen würde. Und das ganze funktioniert seit mehreren Wochen einwandfrei! Einziger Nachteil, die IP des Marstek muss in der Config angegeben werden.

Ich habe es so noch nie umgesetzt gesehen und hoffe, ich kann dem einen oder anderen damit weiterhelfen.

Hier noch die Config für kaskadierte Zähler. Kann natürlich auch für nur einen oder zwei Zähler, welche einzeln angeschlossen sind angepasst werden.

> \# ===================================================================
>
> \#                      Watt Wächter - Zwei Zähler (Kaskade)
>
> \# ===================================================================
>
> \# Diese Konfiguration ist für den Kaskadenbetrieb mit zwei Zählern
>
> \# vorgesehen. Dabei misst ein Hauptzähler (Meter 2) den gesamten
>
> \# Netzbezug, während ein Unterzähler (Meter 1) einen Teil davon
>
> \# erfasst (z.B. eine Wallbox, Hausstrom oder Einliegerwohnung).
>
> \#
>
> \# Es wird die Differenz berechnet (Hauptzähler - Unterzähler),
>
> \# um den Verbrauch des restlichen Haushalts zu ermitteln.
>
> \#
>
> \# WICHTIG:
>
> \# - Meter 2 (GPIO5/4) = Hauptzähler am Netzanschlusspunkt
>
> \# - Meter 1 (GPIO26/27) = Nachgelagerter Unterzähler
>
> \#
>
> \# --- Benutzerkonfiguration ---
>
> \# Passe die folgenden Werte unter "substitutions" an deine
>
> \# Gegebenheiten an:
>
> \#
>
> \# - meter1_name: Name für den Unterzähler (z.B. "Haushalt").
>
> \# - meter2_name: Name für den Hauptzähler (z.B. "Gesamt").
>
> \# - calculated_name: Name für die berechnete Differenz (z.B. "Wärmepumpe").
>
> \# - ...\_factor: Die Umrechnungsfaktoren für beide Zähler.
>
> \# - obis_meter1\_... / obis_meter2\_...: Die jeweiligen OBIS-Codes
>
> \#   für beide Zähler.
>
> \#
>
> \# Die WLAN-Zugangsdaten müssen in einer separaten \`secrets.yaml\`-Datei
>
> \# hinterlegt sein.
>
> \# ===================================================================
>
> \# ===================================================================
>
> \#                      BENUTZERKONSTANTEN & VARIABLEN
>
> \# ===================================================================
>
> substitutions:
>
>   device_name: watt-waechter-cascade
>
>   friendly_device_name: "Watt Wächter"
>
>   \# Meter 2 ist der Hauptzähler, Meter 1 der Unterzähler
>
>   meter1_name: "Haushalt"
>
>   meter2_name: "Gesamt"
>
>   calculated_name: "Wärmepumpe"
>
>   \# Umwandlung in kWh
>
>   meter1_consumption_factor: "0.0001"
>
>   meter1_feedin_factor:     "0.0001"
>
>   meter2_consumption_factor: "0.0001"
>
>   meter2_feedin_factor:     "0.0001"
>
>   \# OBIS-Codes
>
>   obis_meter1_total_consumption: "1-0:1.8.0"
>
>   obis_meter1_total_feed_in:     "1-0:2.8.0"
>
>   obis_meter1_current_power:     "1-0:16.7.0"
>
>   obis_meter2_total_consumption: "1-0:1.8.0"
>
>   obis_meter2_total_feed_in:     "1-0:2.8.0"
>
>   obis_meter2_current_power:     "1-0:16.7.0"
>
>   \# --- Einstellungen für Shelly Emulation ---
>
>   \# Die Ziel-IP-Adresse des Batteriespeichers (z.B. Marstek)
>
>   udp_target_ip: "192.168.20.120"
>
>   \# Welche Leistungsdaten sollen gesendet werden?
>
>   \# Mögliche Werte: "meter1", "meter2", "calculated"
>
>   shelly_emulation_source: "meter2"
>
> \# ===================================================================
>
> \#                      BASIS-SYSTEMKONFIGURATION
>
> \# ===================================================================
>
> esphome:
>
>   name: ${device_name}
>
>   friendly_name: ${friendly_device_name}
>
> esp32:
>
>   board: esp32dev
>
>   framework:
>
>     type: esp-idf
>
>     sdkconfig_options:
>
>       CONFIG_COMPILER_OPTIMIZATION_PERF: "y"
>
>       CONFIG_ESPTOOLPY_FLASHSIZE_4MB: "y"
>
>       CONFIG_SPIRAM_SUPPORT: "n"
>
>       CONFIG_BT_ENABLED: "n"
>
> wifi:
>
>   ssid: !secret wifi_ssid
>
>   password: !secret wifi_password
>
>   ap:
>
>     ssid: "${friendly_device_name} AP"
>
>     password: "CHANGE_ME_PLS"
>
> captive_portal:
>
> api:
>
>   encryption:
>
>     key: "f6pbA8ZHCiL95X0gJiNa2oDMO3MyTOKp4E/1DmfEEE4="
>
> ota:
>
>   - platform: esphome
>
>     password: "162b8eca5fdc9f947541672558f3b708"
>
> logger:
>
>   baud_rate: 0
>
>   level: DEBUG
>
> \# ===================================================================
>
> \#                      WEB-SERVER (v3 mit Gruppen)
>
> \# ===================================================================
>
> web_server:
>
>   port: 80
>
>   version: 3
>
>   local: true
>
>   sorting_groups:
>
>     - { id: calculated_values, name: "${calculated_name}",     sorting_weight: -50 }
>
>     - { id: meter_2,          name: "Zähler 2: ${meter2_name}", sorting_weight: -30 } # Hauptzähler nach oben
>
>     - { id: meter_1,          name: "Zähler 1: ${meter1_name}", sorting_weight: -40 }
>
>     - { id: device_info,      name: "Geräteinformationen",      sorting_weight: -20 }
>
> \# ===================================================================
>
> \#                   SERIELLE SCHNITTSTELLEN & SML
>
> \# ===================================================================
>
> uart:
>
>   - id: uart_bus_meter1
>
>     tx_pin: GPIO26
>
>     rx_pin: GPIO27
>
>     baud_rate: 9600
>
>     data_bits: 8
>
>     parity: NONE
>
>     stop_bits: 1
>
>   - id: uart_bus_meter2
>
>     tx_pin: GPIO5
>
>     rx_pin: GPIO4
>
>     baud_rate: 9600
>
>     data_bits: 8
>
>     parity: NONE
>
>     stop_bits: 1
>
> sml:
>
>   - id: sml_meter1
>
>     uart_id: uart_bus_meter1
>
>   - id: sml_meter2
>
>     uart_id: uart_bus_meter2
>
> \# ===================================================================
>
> \#                         SENSOREN
>
> \# ===================================================================
>
> sensor:
>
>   \# ---------- System ----------
>
>   - platform: wifi_signal
>
>     name: "${friendly_device_name} WiFi Signal"
>
>     update_interval: 60s
>
>     entity_category: diagnostic
>
>     web_server: { sorting_group_id: device_info, sorting_weight: 40 }
>
>   - platform: uptime
>
>     name: "${friendly_device_name} Uptime"
>
>     update_interval: 60s
>
>     entity_category: diagnostic
>
>     web_server: { sorting_group_id: device_info, sorting_weight: 10 }
>
>   \# ---------- Zähler 1 (Unterzähler) ----------
>
>   - platform: sml
>
>     id: meter1_bezug
>
>     sml_id: sml_meter1
>
>     name: "${meter1_name} Zählerstand Bezug"
>
>     obis_code: "${obis_meter1_total_consumption}"
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     filters:
>
>       - multiply: ${meter1_consumption_factor}
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     web_server: { sorting_group_id: meter_1 }
>
>   - platform: sml
>
>     id: meter1_einspeisung
>
>     sml_id: sml_meter1
>
>     name: "${meter1_name} Zählerstand Einspeisung"
>
>     obis_code: "${obis_meter1_total_feed_in}"
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     filters:
>
>       - multiply: ${meter1_feedin_factor}
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     web_server: { sorting_group_id: meter_1 }
>
>   - platform: sml
>
>     id: meter1_leistung
>
>     sml_id: sml_meter1
>
>     name: "${meter1_name} Aktuelle Leistung"
>
>     obis_code: "${obis_meter1_current_power}"
>
>     unit_of_measurement: "W"
>
>     accuracy_decimals: 0
>
>     device_class: power
>
>     state_class: measurement
>
>     web_server: { sorting_group_id: meter_1 }
>
>   \# ---------- Zähler 2 (Hauptzähler) ----------
>
>   - platform: sml
>
>     id: meter2_bezug
>
>     sml_id: sml_meter2
>
>     name: "${meter2_name} Zählerstand Bezug"
>
>     obis_code: "${obis_meter2_total_consumption}"
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     filters:
>
>       - multiply: ${meter2_consumption_factor}
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     web_server: { sorting_group_id: meter_2 }
>
>   - platform: sml
>
>     id: meter2_einspeisung
>
>     sml_id: sml_meter2
>
>     name: "${meter2_name} Zählerstand Einspeisung"
>
>     obis_code: "${obis_meter2_total_feed_in}"
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     filters:
>
>       - multiply: ${meter2_feedin_factor}
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     web_server: { sorting_group_id: meter_2 }
>
>   - platform: sml
>
>     id: meter2_leistung
>
>     sml_id: sml_meter2
>
>     name: "${meter2_name} Aktuelle Leistung"
>
>     obis_code: "${obis_meter2_current_power}"
>
>     unit_of_measurement: "W"
>
>     accuracy_decimals: 0
>
>     device_class: power
>
>     state_class: measurement
>
>     web_server: { sorting_group_id: meter_2 }
>
>   \# ---------- Berechnete Werte (Differenz) ----------
>
>   - platform: template
>
>     name: "${calculated_name} Aktuelle Leistung"
>
>     id: calc_current_power
>
>     unit_of_measurement: "W"
>
>     accuracy_decimals: 0
>
>     device_class: power
>
>     state_class: measurement
>
>     update_interval: 1s
>
>     lambda: |-
>
>       if (!id(meter1_leistung).has_state() || !id(meter2_leistung).has_state()) return NAN;
>
>       float diff = id(meter2_leistung).state - id(meter1_leistung).state;
>
>       return diff < 0.0f ? 0.0f : diff;
>
>     web_server: { sorting_group_id: calculated_values }
>
>   - platform: template
>
>     name: "${calculated_name} Zählerstand Bezug"
>
>     id: calc_consumption_total
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     update_interval: 15s
>
>     lambda: |-
>
>       if (!id(meter1_bezug).has_state() || !id(meter2_bezug).has_state()) return NAN;
>
>       float diff = id(meter2_bezug).state - id(meter1_bezug).state;
>
>       return diff < 0.0f ? 0.0f : diff;
>
>     web_server: { sorting_group_id: calculated_values }
>
>   - platform: template
>
>     name: "${calculated_name} Zählerstand Einspeisung"
>
>     id: calc_feedin_total
>
>     unit_of_measurement: "kWh"
>
>     accuracy_decimals: 3
>
>     device_class: energy
>
>     state_class: total_increasing
>
>     update_interval: 15s
>
>     lambda: |-
>
>       if (!id(meter1_einspeisung).has_state() || !id(meter2_einspeisung).has_state()) return NAN;
>
>       float diff = id(meter2_einspeisung).state - id(meter1_einspeisung).state;
>
>       return diff < 0.0f ? 0.0f : diff;
>
>     web_server: { sorting_group_id: calculated_values }
>
> \# ===================================================================
>
> \#         UDP-Shelly Emulation (für Marstek Akku etc.)
>
> \# ===================================================================
>
> udp:
>
>   \# 1) Sender: sendet an den Batteriespeicher
>
>   - id: udp_shelly_sender
>
>     port:
>
>       listen_port: 18001      # Beliebiger lokaler Sendeport
>
>       broadcast_port: 22222   # Zielport des Akkus
>
>     addresses:
>
>       - "${udp_target_ip}"
>
>   \# 2) Server: lauscht auf Anfragen vom Akku (z.B. EM.GetStatus)
>
>   - id: udp_server
>
>     port:
>
>       listen_port: 1010
>
>       broadcast_port: 1010
>
>     on_receive:
>
>       then:
>
>       - lambda: |-
>
>           std::string msg(data.begin(), data.end());
>
>           if (msg.find("\\"method\\":\\"EM.GetStatus\\"") == std::string::npos) return;
>
>           ESP_LOGD("udp_server", "EM.GetStatus Anfrage erhalten. Bereite Antwort vor...");
>
>       - udp.write:
>
>           id: udp_shelly_sender
>
>           data: !lambda |-
>
>             float total_act = 0.0f;
>
>             std::string source = "${shelly_emulation_source}";
>
>             // Wähle die Datenquelle basierend auf der Substitution
>
>             if (source == "meter1") {
>
>               if (id(meter1_leistung).has_state()) {
>
>                 total_act = id(meter1_leistung).state;
>
>               }
>
>             } else if (source == "meter2") {
>
>               if (id(meter2_leistung).has_state()) {
>
>                 total_act = id(meter2_leistung).state;
>
>               }
>
>             } else if (source == "calculated") {
>
>               if (id(calc_current_power).has_state()) {
>
>                 total_act = id(calc_current_power).state;
>
>               }
>
>             }
>
>             // Annahme einphasig
>
>             float a_voltage = 230.0f;
>
>             float a_current = (a_voltage > 0.0f) ? (total_act / a_voltage) : 0.0f;
>
>             float a_aprt_power = fabs(total_act);
>
>             float a_pf = 1.00f;
>
>             float a_freq = 50.0f;
>
>             // Phasen B / C nicht vorhanden
>
>             float b_current = 0.0f, b_voltage = 230.0f, b_act = 0.0f, b_aprt_power = 0.0f, b_pf = 1.0f, b_freq = a_freq;
>
>             float c_current = 0.0f, c_voltage = 230.0f, c_act = 0.0f, c_aprt_power = 0.0f, c_pf = 1.0f, c_freq = a_freq;
>
>             float total_current = fabs(a_current);
>
>             float total_aprt_power = a_aprt_power;
>
>             char buf\[1024\];
>
>             int len = snprintf(buf, sizeof(buf),
>
>               "{"
>
>                 "\\"id\\":0,"
>
>                 "\\"src\\":\\"shellypro3em-watt-waechter\\","
>
>                 "\\"result\\":{"
>
>                   "\\"id\\":0,"
>
>                   "\\"a_current\\":%.2f,\\"a_voltage\\":%.1f,\\"a_act_power\\":%.2f,"
>
>                   "\\"a_aprt_power\\":%.2f,\\"a_pf\\":%.2f,\\"a_freq\\":%.1f,"
>
>                   "\\"b_current\\":%.2f,\\"b_voltage\\":%.1f,\\"b_act_power\\":%.2f,"
>
>                   "\\"b_aprt_power\\":%.2f,\\"b_pf\\":%.2f,\\"b_freq\\":%.1f,"
>
>                   "\\"c_current\\":%.2f,\\"c_voltage\\":%.1f,\\"c_act_power\\":%.2f,"
>
>                   "\\"c_aprt_power\\":%.2f,\\"c_pf\\":%.2f,\\"c_freq\\":%.1f,"
>
>                   "\\"total_current\\":%.2f,"
>
>                   "\\"total_act_power\\":%.2f,"
>
>                   "\\"total_aprt_power\\":%.2f"
>
>                 "}"
>
>               "}",
>
>               // Phase A
>
>               a_current, a_voltage, total_act,
>
>               a_aprt_power, a_pf, a_freq,
>
>               // Phase B
>
>               b_current, b_voltage, b_act,
>
>               b_aprt_power, b_pf, b_freq,
>
>               // Phase C
>
>               c_current, c_voltage, c_act,
>
>               c_aprt_power, c_pf, c_freq,
>
>               // Totals
>
>               total_current,
>
>               total_act,
>
>               total_aprt_power
>
>             );
>
>             ESP_LOGD("udp_sender", "Sende Daten von '%s': Leistung=%.2f W", source.c_str(), total_act);
>
>             return std::vector<uint8_t>(buf, buf + len);
>
> \# ===================================================================
>
> \#                   TEXTSENSOREN (inkl. IP + SSID)
>
> \# ===================================================================
>
> text_sensor:
>
>   - platform: version
>
>     name: "${friendly_device_name} ESPHome Version"
>
>     hide_timestamp: true
>
>     entity_category: diagnostic
>
>     web_server: { sorting_group_id: device_info, sorting_weight: 20 }
>
>   - platform: wifi_info
>
>     ip_address:
>
>       name: "${friendly_device_name} IP"
>
>       entity_category: diagnostic
>
>       web_server: { sorting_group_id: device_info, sorting_weight: 30 }
>
>     ssid:
>
>       name: "${friendly_device_name} SSID"
>
>       entity_category: diagnostic
>
>       web_server: { sorting_group_id: device_info, sorting_weight: 35 }
>
> \# ===================================================================
>
> \#                   BUTTONS & BINÄRSENSOREN
>
> \# ===================================================================
>
> button:
>
>   - platform: restart
>
>     name: "${friendly_device_name} Neustart"
>
>     entity_category: diagnostic
>
>     web_server: { sorting_group_id: device_info, sorting_weight: 100 }
>
> binary_sensor:
>
>   - platform: status
>
>     name: "${friendly_device_name} Status"
>
>     device_class: connectivity
>
>     entity_category: diagnostic
>
>     web_server: { sorting_group_id: device_info, sorting_weight: 50 }

:crayon:by HarryP: Code-/Logzeilen formatiert (bitte immer in </> einbinden)
s.a.: (Neues Update & Features - Hier in der Community 🫶)

2 „Gefällt mir“