Marstek Venus E with esphome SML Reader as Virtual Shelly 3 Pro EM

Hey Guys,

I´ve just wanted to share my Projekt with you.
I got so much help and Ideas from this community so i want to give something back.

I´ve bought an Marstek Venus E Battery (Firmware v151) with 5,15Kwh and I have no Shelly Pro 3 EM as SmartMeter.

I use an esp8266 on my energymeter with esphome to read alle values via SML direct from it.

Problem:

I don´t want another device or service like Unimeter,B2500 oder Energy2Shelly to send the Power Data to my Battery. i like it when it even works when my whole system is on maintenance.

I tried to use unimeter and Energy2Shelly but the Battery did not accept them because i just could send Power from all phases(Added) at once and the emulators divide it by 3 and send it even.
It´s Possible to send all values over MQTT but that´s an another point of failure.

The battery tests the Shelly to determine it´s own Phase.
So it didn´t work.

Solution:
My already in Place esphome smart meter.

My Smartmeter Esphome device waits for a UDP Packet on Port 1010 with Content:
…EM.GetStatus…

That packet is send every second via Broadcast when the Battery has no smart meter associated. I´ve checked wit Wireshark. So I could find the neccessary Port 22222.

Then i respond with and udp json Packet that´s formatted as it´s a Shelly Pro 3 EM.

The Marstek Battery Accepts the value because it thinks it´s an Shelly Pro 3 EM.

I´ve found analyses online what the answer JSON has to look like.
I could not make the UDP request dynamic so I´ve set the IP and Port from the Battery manual.
As soon as the Battery Accepts the Battery it send its packets via unicast to my SML Reader.

My Opinion:

It is a bit quick and dirty, but it works for me.
I´m not a programmer but i hope some one can use it.

My Goal was that the Battery gets a precise Energy reading every second.
And it works like a charm for my zero feed in.

Feel free to optimize and change it. It´s just an Idea starter.

Here´s the code from my Energy Meter:

uart:
  id: uart_bus
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 9600
  data_bits: 8
  parity: NONE
  stop_bits: 1

sml:
  id: mysml
  uart_id: uart_bus

sensor:
  - platform: sml
    name: "Zählerstand"
    sml_id: mysml
    obis_code: "1-0:1.8.0"
    unit_of_measurement: kWh
    accuracy_decimals: 4
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.0000001

  - platform: sml
    name: "Energieverbrauch Alles"
    id: verbrauch
    sml_id: mysml
    obis_code: "1-0:16.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01
 
  - platform: sml
    name: "Energieverbrauch L1"
    sml_id: mysml
    id: l1
    obis_code: "1-0:36.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Energieverbrauch L2"
    sml_id: mysml
    id: l2
    obis_code: "1-0:56.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Energieverbrauch L3"
    sml_id: mysml
    id: l3
    obis_code: "1-0:76.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Spannung L1"
    sml_id: mysml
    id: u1
    obis_code: "1-0:32.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: sml
    name: "Spannung L2"
    sml_id: mysml
    id: u2
    obis_code: "1-0:52.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: sml
    name: "Spannung L3"
    sml_id: mysml
    id: u3
    obis_code: "1-0:72.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: template
    name: "Strom L1"
    id: i1
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u1).has_state() && id(l1).has_state() && id(u1).state > 0) {
        return fabs(id(l1).state / id(u1).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Strom L2"
    id: i2
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u2).has_state() && id(l2).has_state() && id(u2).state > 0) {
        return fabs(id(l2).state / id(u2).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Strom L3"
    id: i3
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u3).has_state() && id(l3).has_state() && id(u3).state > 0) {
        return fabs(id(l3).state / id(u3).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Strom Gesamt"
    id: i_total
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      return id(i1).state + id(i2).state + id(i3).state;
      
  - platform: template
    name: "Leistungsfaktor L1"
    id: pf1
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return 1.0;
      
  - platform: template
    name: "Leistungsfaktor L2"
    id: pf2
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return 1.0;
      
  - platform: template
    name: "Leistungsfaktor L3"
    id: pf3
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return 1.0;
      
  - platform: template
    name: "Netzfrequenz"
    id: freq
    unit_of_measurement: "Hz"
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return 50.0;


udp:
  - id: udp_shelly_sender
    port:
      listen_port:    18001        # Any Port not used
      broadcast_port: 22222        # Destination Port Battery
    addresses:
      - 192.168.0.55               # Ip from  Battery

  - 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;

        - udp.write:
            id: udp_shelly_sender
            data: !lambda |-
              char buf[512];
              int len = snprintf(buf, sizeof(buf),
                "{"
                  "\"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"
                "}",
                id(i1).state, id(u1).state, id(l1).state,
                id(l1).state, id(pf1).state, id(freq).state,
                id(i2).state, id(u2).state, id(l2).state,
                id(l2).state, id(pf2).state, id(freq).state,
                id(i3).state, id(u3).state, id(l3).state,
                id(l3).state, id(pf3).state, id(freq).state,
                id(i_total).state,
                id(verbrauch).state,
                id(verbrauch).state
              );
              return std::vector<uint8_t>(buf, buf + len);
1 Like

Got an Update to v153. It broke down :). Here´s the new config.
It even gives better values.

sml:
  id: mysml
  uart_id: uart_bus

sensor:
  - platform: sml
    name: "Meter Reading"
    sml_id: mysml
    obis_code: "1-0:1.8.0"
    unit_of_measurement: kWh
    accuracy_decimals: 4
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.0000001

  - platform: total_daily_energy
    name: "Daily Export"
    power_id: export_power
    filters:
      - multiply: 0.001
    unit_of_measurement: kWh

  - platform: sml
    name: "Total Power Consumption"
    id: verbrauch
    sml_id: mysml
    obis_code: "1-0:16.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01
    on_value:
      - sensor.template.publish:
          id: export_power
          state: !lambda |-
            if ((id(verbrauch).state) >= 0) {
              return 0; 
            } else {
              return (id(verbrauch).state /-1.0);
            }

  - platform: sml
    name: "Power Consumption L1"
    sml_id: mysml
    id: l1
    obis_code: "1-0:36.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Power Consumption L2"
    sml_id: mysml
    id: l2
    obis_code: "1-0:56.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Power Consumption L3"
    sml_id: mysml
    id: l3
    obis_code: "1-0:76.7.0"
    unit_of_measurement: W
    accuracy_decimals: 2
    filters:
      - multiply: 0.01

  - platform: sml
    name: "Voltage L1"
    sml_id: mysml
    id: u1
    obis_code: "1-0:32.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: sml
    name: "Voltage L2"
    sml_id: mysml
    id: u2
    obis_code: "1-0:52.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: sml
    name: "Voltage L3"
    sml_id: mysml
    id: u3
    obis_code: "1-0:72.7.0"
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - multiply: 0.1

  - platform: template
    name: "Current L1"
    id: i1
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u1).has_state() && id(l1).has_state() && id(u1).state > 0) {
        return fabs(id(l1).state / id(u1).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Current L2"
    id: i2
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u2).has_state() && id(l2).has_state() && id(u2).state > 0) {
        return fabs(id(l2).state / id(u2).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Current L3"
    id: i3
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(u3).has_state() && id(l3).has_state() && id(u3).state > 0) {
        return fabs(id(l3).state / id(u3).state);
      } else {
        return 0.0;
      }
      
  - platform: template
    name: "Total Current"
    id: i_total
    unit_of_measurement: "A"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      return id(i1).state + id(i2).state + id(i3).state;
        
  - platform: template
    name: "Power Factor L1"
    id: pf1
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (!id(l1).has_state()) return 0.95; // Fallback
      
      // Power absolute value for calculation
      float power_abs = fabs(id(l1).state);
      
      // Export (negative power)
      if (id(l1).state < 0) {
        // Inverters typically have better PF (0.95-0.99)
        return 0.95 + (std::rand() % 4) * 0.01;
      }
      // Import (positive power)
      else {
        if (power_abs < 20.0) {
          // Low load: worse PF (0.85-0.90)
          return 0.85 + (std::rand() % 5) * 0.01;
        } else if (power_abs < 100.0) {
          // Medium load: medium PF (0.90-0.94)
          return 0.90 + (std::rand() % 4) * 0.01;
        } else {
          // High load: better PF (0.94-0.98)
          return 0.94 + (std::rand() % 4) * 0.01;
        }
      }

  - platform: template
    name: "Power Factor L2"
    id: pf2
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (!id(l2).has_state()) return 0.95; // Fallback
      
      // Power absolute value for calculation
      float power_abs = fabs(id(l2).state);
      
      // Export (negative power)
      if (id(l2).state < 0) {
        // Inverters typically have better PF (0.95-0.99)
        return 0.95 + (std::rand() % 4) * 0.01;
      }
      // Import (positive power)
      else {
        if (power_abs < 20.0) {
          // Low load: worse PF (0.85-0.90)
          return 0.85 + (std::rand() % 5) * 0.01;
        } else if (power_abs < 100.0) {
          // Medium load: medium PF (0.90-0.94)
          return 0.90 + (std::rand() % 4) * 0.01;
        } else {
          // High load: better PF (0.94-0.98)
          return 0.94 + (std::rand() % 4) * 0.01;
        }
      }

  - platform: template
    name: "Power Factor L3"
    id: pf3
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (!id(l3).has_state()) return 0.95; // Fallback
      
      // Power absolute value for calculation
      float power_abs = fabs(id(l3).state);
      
      // Export (negative power)
      if (id(l3).state < 0) {
        // Inverters typically have better PF (0.95-0.99)
        return 0.95 + (std::rand() % 4) * 0.01;
      }
      // Import (positive power)
      else {
        if (power_abs < 20.0) {
          // Low load: worse PF (0.85-0.90)
          return 0.85 + (std::rand() % 5) * 0.01;
        } else if (power_abs < 100.0) {
          // Medium load: medium PF (0.90-0.94)
          return 0.90 + (std::rand() % 4) * 0.01;
        } else {
          // High load: better PF (0.94-0.98)
          return 0.94 + (std::rand() % 4) * 0.01;
        }
      }

  # Apparent power for phase L1
  - platform: template
    name: "Apparent Power L1"
    id: apparent_power_l1
    unit_of_measurement: "VA"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(l1).has_state() && id(pf1).has_state() && id(pf1).state > 0) {
        return fabs(id(l1).state) / id(pf1).state;
      } else {
        return 0.0;
      }

  # Apparent power for phase L2
  - platform: template
    name: "Apparent Power L2"
    id: apparent_power_l2
    unit_of_measurement: "VA"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(l2).has_state() && id(pf2).has_state() && id(pf2).state > 0) {
        return fabs(id(l2).state) / id(pf2).state;
      } else {
        return 0.0;
      }

  # Apparent power for phase L3
  - platform: template
    name: "Apparent Power L3"
    id: apparent_power_l3
    unit_of_measurement: "VA"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      if (id(l3).has_state() && id(pf3).has_state() && id(pf3).state > 0) {
        return fabs(id(l3).state) / id(pf3).state;
      } else {
        return 0.0;
      }

  # Total apparent power
  - platform: template
    name: "Total Apparent Power"
    id: apparent_power_total
    unit_of_measurement: "VA"
    accuracy_decimals: 2
    update_interval: 1s
    lambda: |-
      return id(apparent_power_l1).state + id(apparent_power_l2).state + id(apparent_power_l3).state;

  - platform: template
    name: "Grid Frequency"
    id: freq
    unit_of_measurement: "Hz"
    accuracy_decimals: 1
    update_interval: 10s
    lambda: |-
      static float last_freq = 50.0;
      // Simulate realistic, slightly fluctuating grid frequency
      // The frequency changes only slowly, never abruptly
      float new_freq = last_freq + ((float)(rand() % 5 - 2) / 20.0);  // -0.1 to +0.1 Hz change
      
      // Limit to realistic range
      if (new_freq < 49.8) new_freq = 49.8;
      if (new_freq > 50.2) new_freq = 50.2;
      
      last_freq = new_freq;
      return new_freq;

udp:
  - id: udp_sender
    port:
      listen_port: 18511
      broadcast_port: 18512
    addresses:
      - 192.168.0.54

  # 1) Sender: unicast to Venus battery 192.168.0.55:22222
  - id: udp_shelly_sender
    port:
      listen_port: 18001        # any local sending port
      broadcast_port: 22222     # target port of the battery
    addresses:
      - 192.168.0.55           # fixed IP of the battery

  # 2) Server: listens on port 1010 for 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;
      - udp.write:
          id: udp_shelly_sender
          data: !lambda |-
            char buf[1024];
            
            int len = snprintf(buf, sizeof(buf),
              "{"
                "\"id\":0,"
                "\"src\":\"shellypro3em-e682e89c1724\","
                "\"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"
                "}"
              "}",
              id(i1).state, id(u1).state, id(l1).state,
              id(apparent_power_l1).state, id(pf1).state, id(freq).state,
              id(i2).state, id(u2).state, id(l2).state,
              id(apparent_power_l2).state, id(pf2).state, id(freq).state,
              id(i3).state, id(u3).state, id(l3).state,
              id(apparent_power_l3).state, id(pf3).state, id(freq).state,
              id(i_total).state,
              id(verbrauch).state,
              id(apparent_power_total).state
            );
            return std::vector<uint8_t>(buf, buf + len);


Hello,
I bought the same battery (not yet shipped).
Do you have the battery connected to home assistant?
I have a shelly 3pm, so I guess I want need your work.
But I ont know (yet) how to integrate the battery to home assistant.
Any advice?

Yea I´ve added it via an esp32 with esphome over modbus.
It´s even possible to integrate it with an RS485 wifi or ethernet adapter.
Just search “Marstek Venus E Modbus” on google. there are several solutions for this.
I have not all possible Values integrated, just the one I´m interested in.


Ohh wow das sieht ja perfekt aus :slightly_smiling_face:
Looks perfect, just like I imaged it should be.

This is exaclty what I am searching for.
@jibbo can you please share your esphome Code?

Would you share your esphome yaml configuration with us?