SML meter reader on ESP32 with I2C display to control openWB wallbox

Due to the big rise of power prices in germany, I now changed my meter setup to self-use solar energy (which is extra funded by german government), and got here 2 brand new DZG DWSB10.2 bidirectional powermeters with IR based SML interface from our power supplier, hope my config helps someone else.

Though these meters have a pulsed LED, I discovered it’s worthless, because it cannot tell if the power is entering or leaving the house, so my old pulsecounter is not usable anymore.

To get the meters talking more frequently, they can be set to report all measurements every second, I needed a pin to unlock the meters menu, my very friendly power company gave them to me together with the meters manual.

To get the serial data, I use 2 IR heads from Ebay

3D printed cases found at Thingiverse, modified a bit to match the meters and my magnets, and a ESP32 dev board.

Had an unused I2C display from AZdelivery, which looks pretty good in the cabinet of the meters.


Used an ESP32, it has 2 usable hardware serial ports, here’s the esphome config to read out 2 meters, TX and RX are incorrectly printed on the IR heads, so had to swap the pins in software:

substitutions:
  name: smlreader

esphome:
  name: ${name}
  platform: ESP32
  board: nodemcu-32s

<<: !include _common/global.yaml

time:
  - platform: sntp
    id: sntp_time
    servers: timeserver.in.edevau.net

external_components:
  - source: github://alengwenus/esphome_components@main
    refresh: 0s
    components: [sml]

i2c:
  sda: GPIO21
  scl: GPIO22

font:
  - file: "fonts/hd44780.ttf"
    id: font1
    size: 8

display:
  - platform: ssd1306_i2c
    model: "SH1106 128x64"
    # no model: "SH1107 128x64"
    # no model: "SH1106 128x32"
    # no model: "SSD1306 128x64"
    #reset_pin: D0
    address: 0x3C
    lambda: |-
      auto red = Color(255, 0, 0);
      it.printf( 64,  0, id(font1), TextAlign::TOP_CENTER, "${name}");
      it.line(0, 9, 128, 9, red);
      it.printf( 5, 12, id(font1), "Import  %8.2f kWh", id(evu_import).state);
      it.printf( 5, 21, id(font1), "Export  %8.2f kWh", id(evu_export).state);
      it.line(0, 30, 128, 30, red);

      it.printf( 2, 33, id(font1), "PV:");
      it.printf( 5, 42, id(font1), "Import  %8.2f kWh", id(pv_import).state);

      it.line(0, 52, 128, 52, red);
      it.line(42, 9, 42, 52, red);

      it.strftime(64, 56, id(font1), TextAlign::TOP_CENTER, "%Y-%m-%d %H:%M", id(sntp_time).now());

uart:
  - id: uart_bus0
    # TX: 1, RX: 3 (reversed)
    tx_pin: GPIO3
    rx_pin: GPIO1
    baud_rate: 9600
    rx_buffer_size: 2048
    data_bits: 8
    stop_bits: 1
    parity: NONE
  - id: uart_bus2
    # TX:17, RX: 16 (reversed)
    tx_pin: GPIO16
    rx_pin: GPIO17
    baud_rate: 9600
    rx_buffer_size: 2048
    data_bits: 8
    stop_bits: 1
    parity: NONE

sml:
 - id: sml_pv
   uart_id: uart_bus0
 - id: sml_evu
   uart_id: uart_bus2
  
sensor:

  - platform: sml
    name: "evu_import"
    id: "evu_import"
    sml_id: sml_evu
    obis_code: "1-0:1.8.0"
    unit_of_measurement: kWh
    state_class: total_increasing
    device_class: energy
    filters:
     - multiply: 0.0001
    accuracy_decimals: 4
    force_update: true
    
  - platform: sml
    name: "evu_WhImported"
    id: "evu_WhImported"
    sml_id: sml_evu
    obis_code: "1-0:1.8.0"
    unit_of_measurement: Wh
    state_class: total_increasing
    device_class: energy
    filters:
     - multiply: 0.1
    accuracy_decimals: 1
    force_update: true
    
  - platform: sml
    name: "evu_export"
    id: "evu_export"
    sml_id: sml_evu
    obis_code: "1-0:2.8.0"
    unit_of_measurement: kWh
    state_class: total_increasing
    device_class: energy
    filters:
     - multiply: 0.0001
    accuracy_decimals: 4
    force_update: true

  - platform: sml
    name: "evu_WhExported"
    id: "evu_WhExported"
    sml_id: sml_evu
    obis_code: "1-0:2.8.0"
    unit_of_measurement: Wh
    state_class: total_increasing
    device_class: energy
    filters:
     - multiply: 0.1
    accuracy_decimals: 1
    force_update: true

  - platform: sml
    name: "pv_import"
    id: "pv_import"
    sml_id: sml_pv
    obis_code: "1-0:2.8.0"
    unit_of_measurement: kWh
    state_class: total_increasing
    device_class: energy
    accuracy_decimals: 4
    filters:
     - multiply: 0.0001
    force_update: true
    
  - platform: sml
    name: "pv_WhImported"
    id: "pv_WhImported"
    sml_id: sml_pv
    obis_code: "1-0:2.8.0"
    unit_of_measurement: Wh
    state_class: total_increasing
    device_class: energy
    filters:
     - multiply: 0.1
    accuracy_decimals: 1
    force_update: true

#  - platform: sml
#    name: "evu_leistung"
#    id: "evu_leistung"
#    sml_id: sml_evu
#    obis_code: "1-0:16.7.0"
#    filters:
#     - multiply: 0.001
#    unit_of_measurement: W
#    state_class: measurement
#    device_class: power

#  - platform: sml
#    name: "pv_leistung"
#    id: "pv_leistung"
#    sml_id: sml_pv
#    obis_code: "1-0:16.7.0"
#    filters:
#     - multiply: 0.001
#    unit_of_measurement: W
#    state_class: measurement
#    device_class: power

#  - platform: sml
#    name: "pv_p_eff"
#    id: "pv_p_eff"
#    sml_id: sml_pv
#    obis_code: "1-0:16.7.0"
#    filters:
#     - multiply: 0.01
#    unit_of_measurement: W
#    state_class: measurement
#    device_class: power

  - platform: sml
    name: "evu_p_eff"
    id: "evu_p_eff"
    sml_id: sml_evu
    obis_code: "1-0:16.7.0"
    filters:
     - multiply: 0.01
    unit_of_measurement: W
    state_class: measurement
    device_class: power

One meter counts import solar energy, the second meter measures import and export from the power company. state_class and device_class are required to make the entities show up in Home Assistants Energy Dashboard.

The values are also published on MQTT (from Home Assistant) to tell the Wallbox if we have unused energy (to charge the car). These values for the MQTT bridge to Openwb are mostly calculated by template sensors, at this point, force_update: true is very important, to avoid the template sensors get stuck at the last old measurement if one entity is not updated for longer, e.g. the evu import counter during sunshine.

Getting the power from the absolute counters is easy then using derivative platform:

sensor:
#openWB/set/evu/W Bezugsleistung in Watt, int, positiv Bezug, negativ Einspeisung
- platform: derivative
  name: bezugsleistung
  source: sensor.evu_whimported
  round: 0
  unit: W
  time_window: 00:00:10
  unit_time: h
- platform: derivative
  name: einspeiseleistung
  source: sensor.evu_whexported
  round: 0
  unit: W
  time_window: 00:00:10
  unit_time: h
- platform: template
  sensors:
    #openWB/set/evu/W Bezugsleistung in Watt, int, positiv Bezug, negativ Einspeisung
    bezugsleistung4openwb:
      friendly_name: 'W'
      value_template: "{{ (states('sensor.bezugsleistung')) | int(0) - (states('sensor.einspeiseleistung')) | int(0) }}"

Here I have the one automation to update the MQTT from my template sensors, some values from a SDM630 modbus meter:

automation:
- alias: 'openWB Publish calculated sensor values'
  mode: parallel
  trigger:
    platform: state
    entity_id: sensor.aphase1, sensor.aphase2, sensor.aphase3, sensor.bezugsleistung4openwb, sensor.evu_whimported, sensor.smlwhexported4openwb, sensor.smll1volts4openwb, sensor.smll2volts4openwb, sensor.smll3volts4openwb, sensor.smlfrequency4openwb
  action:
    service: mqtt.publish
    data_template:
      payload: "{{trigger.to_state.state}}"
      topic: "openWB/openWB/set/evu/{{trigger.to_state.name}}"

I (mis-)use the ‘name’ attribute to address the correct target MQTT topic, because it is case sensitive, and the wallbox expects the values exactly there.
As soon as the sun comes out, the cars starts charging with the unused current, unfortunately in this example the dishwasher is running in parallel, the blue line (car) tries to keep the EVU current in balance:

For better long term analysis, I also store all measurements in InfluxDB:

1 Like

Some things have changed in HA 2023.3 or 2023.4, always one of the derivatives was not updated, either the calculated export power or the calculated import power. These then were stuck at the last value, the force_update: true in the esp’s config did not help anymore, so the calculation of the current power W (published to openWB/set/evu/W) had to be changed. Now the change of one of the counters triggers the recalclulation of the current EVU power.

#Per MQTT zu schreiben, für OpenWB:

sensor:
#openWB/set/evu/W Bezugsleistung in Watt, int, positiv Bezug, negativ Einspeisung
### Take care of unchanged derivative
### https://github.com/home-assistant/core/issues/67627#issuecomment-1451255785
- platform: derivative
  name: bezugsleistung
  source: sensor.evu_whimported
  round: 0
  unit: W
  time_window: 00:00:10
  unit_time: h
- platform: derivative
  name: einspeiseleistung
  source: sensor.evu_whexported
  round: 0
  unit: W
  time_window: 00:00:10
  unit_time: h
- platform: derivative
  name: evu_leistung
  source: sensor.evu_cumulated
  round: 0
  unit: W
  time_window: 00:00:10
  unit_time: h

#openWB/set/evu/W Bezugsleistung in Watt, int, positiv Bezug, negativ Einspeisung

- platform: template
  sensors:
    #openWB/set/evu/W Bezugsleistung in Watt, int, positiv Bezug, negativ Einspeisung
    bezugsleistung4openwb:
      friendly_name: 'W'
      value_template: "{{ (states('sensor.bezugsleistung')) | int(0) - (states('sensor.einspeiseleistung')) | int(0) }}"
    evu_cumulated:
      value_template: "{{ (states('sensor.evu_whimported')) | int(0) - (states('sensor.evu_whexported')) | int(0) }}"
    #openWB/set/evu/WhExported Eingespeiste Energie in Wh, float, Punkt als Trenner, nur positiv

    #openWB/set/evu/VPhase1 Spannung in Volt für Phase 1-3, float, Punkt als Trenner
    # sensor.sdm630_phase_1_line_to_neutral_volts
    smll1volts4openwb:
      friendly_name: 'VPhase1'
      value_template: "{{ (states('sensor.sdm630_phase_1_line_to_neutral_volts')|float(0)) }}"
      unit_of_measurement: V
      device_class: voltage
    # sensor.sdm630_phase_2_line_to_neutral_volts
    smll2volts4openwb:
      friendly_name: 'VPhase2'
      value_template: "{{ (states('sensor.sdm630_phase_2_line_to_neutral_volts')|float(0)) }}"
      unit_of_measurement: V
      device_class: voltage
    # sensor.sdm630_phase_3_line_to_neutral_volts
    smll3volts4openwb:
      friendly_name: 'VPhase3'
      value_template: "{{ (states('sensor.sdm630_phase_3_line_to_neutral_volts')|float(0)) }}"
      unit_of_measurement: V
      device_class: voltage

    #openWB/set/evu/HzFrequenz Netzfrequenz in Hz, float, Punkt als Trenner
    # sensor.sdm630_frequency_of_supply_voltages
    smlfrequency4openwb:
      friendly_name: 'HzFrequenz'
      value_template: "{{ (states('sensor.sdm630_frequency_of_supply_voltages')|float(0)) }}"
      unit_of_measurement: Hz
      device_class: frequency
      

#openWB/set/evu/APhase1 Strom in Ampere für Phase 1-3, float, Punkt als Trenner, positiv Bezug, negativ Einspeisung
template:
  - sensor:
    - name: 'APhase1'
      device_class: current
      unit_of_measurement: A
      state: >
        {% if is_number(states('sensor.sdm630_phase_1_current')) and is_number(states('sensor.current_ac_fronius_inverter_1_172_16_2_169')) %}
          {{ states('sensor.sdm630_phase_1_current') | float(0) - states('sensor.current_ac_fronius_inverter_1_172_16_2_169') | float(0) / 3 }}
        {% else %}
          {{ states('sensor.sdm630_phase_1_current') | float(0) }}
        {% endif %}
    - name: 'APhase2'
      device_class: current
      unit_of_measurement: A
      state: >
        {% if is_number(states('sensor.sdm630_phase_2_current')) and is_number(states('sensor.current_ac_fronius_inverter_1_172_16_2_169')) %}
          {{ states('sensor.sdm630_phase_2_current') | float(0) - states('sensor.current_ac_fronius_inverter_1_172_16_2_169') | float(0) / 3 }}
        {% else %}
          {{ states('sensor.sdm630_phase_2_current') | float(0) }}
        {% endif %}
    - name: 'APhase3'
      device_class: current
      unit_of_measurement: A
      state: >
        {% if is_number(states('sensor.sdm630_phase_3_current')) and is_number(states('sensor.current_ac_fronius_inverter_1_172_16_2_169')) %}
          {{ states('sensor.sdm630_phase_3_current') | float(0) - states('sensor.current_ac_fronius_inverter_1_172_16_2_169') | float(0) / 3 }}
        {% else %}
          {{ states('sensor.sdm630_phase_3_current') | float(0) }}
        {% endif %}

automation:
- alias: 'openWB Publish calculated sensor values'
  mode: parallel
  trigger:
    platform: state
    entity_id: sensor.aphase1, sensor.aphase2, sensor.aphase3, sensor.evu_whimported, sensor.evu_whexported, sensor.smll1volts4openwb, sensor.smll2volts4openwb, sensor.smll3volts4openwb, sensor.smlfrequency4openwb
  action:
    - service: mqtt.publish
      data_template:
        payload: "{{trigger.to_state.state}}"
        topic: "openWB/openWB/set/evu/{{trigger.to_state.name}}"
- alias: 'openWB Publish calculated power'
  trigger:
    platform: state
    entity_id: sensor.evu_leistung
  action:
    - service: mqtt.publish
      data_template:
        payload: "{{trigger.to_state.state}}"
        topic: "openWB/openWB/set/evu/W"

Now ‘PV-Laden’ works again very well: