Marstek Venus E: Advanced Energy & BMS Control for Zero-Export / Off-Grid (zero immissione / impianti isola)

Hi everyone!
I wanted to share a project I’ve been working on to get the absolute best out of my Marstek Venus E (5.12 kWh) storage system using Home Assistant, specifically optimized for zero-export and off-grid photovoltaic setups (zero immissione / impianti isola).

Since the official app doesn’t support zero-export / off-grid charging modes at all, I set up a complete automation and sensor suite to handle everything smoothly. Here is a brief overview of all the automations:

  • 01 - PV Charging Ramp: Gradually increases battery charging power step-by-step based on available solar production (aiming for 85% of PV generation), dynamically reducing it if grid import is detected while keeping the system in passive mode.

  • 02 - Self-Consumption: Automatically switches the battery to auto mode whenever home grid import exceeds a specific threshold for more than a few seconds to support household loads.

  • 03 - Passive After Import: Safely switches the battery back to passive mode during daytime hours when grid import drops below a safe threshold, optimizing energy flow.

  • 04 - Night Discharge: Manages the evening/night and early morning window by activating auto mode so the battery can discharge and power the house overnight.

  • 05 - Marstek Reload: Triggers a scheduled reload of the device configuration every morning at a set time to be sure that every automation works fine.

  • 06 - Keepalive Venus: Sends periodic afternoon read-commands when the battery is nearly full, preventing the device from entering daytime sleep mode.

  • 07 - Periodic Grid Charge Maintenance: Checks every 14 days if the battery has reached 100% capacity; if not, it safely forces a 2000W grid charge one hour after sunset, stopping automatically once it hits 99.9% or after a 3-hour timeout.

(Note: Make sure to replace the placeholder YOUR_DEVICE_ID in the YAML code below with your actual device ID!)

Supporting Configuration Files (configuration.yaml)

Add these snippets to your main configuration.yaml to handle templates, energy tracking integration, utility meters, and dashboard mapping:

YAML

template:
  - sensor:
      - name: "Battery Status"
        unique_id: battery_status_venus
        state: >
          {% set soc = states('sensor.venus_e_3_0_battery_level')|float(0) %}
          {% set power = states('sensor.venus_e_3_0_battery_power')|float(0) %}
          {% set cap = 5120 %}
          {% if soc >= 99.5 %} 
            100% - Full Charge
          {% elif power >= -10 and power <= 10 %} 
            0h - Standby
          {% elif power < -10 %}
            {% set rimanente = cap * (100 - soc) / 100 %}
            {% set p = power|abs %}
            {% set ore = (rimanente / p) if p > 0 else 0 %}
            {% set h = ore|int %}
            {% set m = ((ore - h)*60)|int %}
            {{ h }}h {{ m }}m to 100%
          {% elif power > 10 %}
            {% set rimanente = cap * (soc - 12) / 100 %}
            {% set ore = (rimanente / power) if power > 0 else 0 %}
            {% set h = ore|int %}
            {% set m = ((ore - h)*60)|int %}
            {{ h }}h {{ m }}m to 12%
          {% endif %}

lovelace:
  dashboards:
    venus-battery:
      mode: yaml
      filename: venus.yaml
      title: Venus
      icon: mdi:battery-high
      show_in_sidebar: true

sensor:
  - platform: integration
    source: sensor.grid_power_import
    name: Total Home Energy from Power
    unique_id: total_home_energy_from_power
    unit_prefix: k
    unit_time: h
    method: left

utility_meter:
  daily_home_energy:
    name: Daily Home Energy Consumption
    unique_id: daily_home_energy_consumption
    source: sensor.total_home_energy_from_power
    cycle: daily
    
  monthly_home_energy:
    name: Monthly Home Energy Consumption
    unique_id: monthly_home_energy_consumption
    source: sensor.total_home_energy_from_power
    cycle: monthly

Dashboard Layout (venus.yaml)

Create a venus.yaml file in your configuration directory with the following layout for real-time monitoring:

YAML

title: Photovoltaic
icon: mdi:solar-power
views:
  - title: Home
    cards:
      - type: markdown
        content: >
          ## Energy System Overview
      - type: markdown
        content: >
          {% set pv = states('sensor.inverter_power')|float(0) %}
          {% set prod_oggi = states('sensor.inverter_today_production')|float(0) %}
          {% set prod_tot = states('sensor.inverter_total_production')|float(0) %}
          
          ## ☀️ PV Generation
          {% if pv > 100 %}
            ### <font color="#2ecc71">☀️ Instant PV: {{ pv|int }} W</font>
          {% elif pv > 0 %}
            ### <font color="#e67e22">⛅ Instant PV: {{ pv|int }} W</font>
          {% else %}
            ### <font color="#e74c3c">🌙 Instant PV: 0 W</font>
          {% endif %}
          
          #### ⚡ Today Production: {{ prod_oggi }} kWh
          
          #### 📈 Total Production: {{ prod_tot }} kWh

          {% set temp = states('sensor.inverter_inverter_module_temperature')|float(0) %}
          {% if temp < 40 %}
            ### <font color="#2ecc71">🌡️ Inverter Temp: {{ temp|int }} °C</font>
          {% elif temp <= 50 %}
            ### <font color="#e67e22">🌡️ Inverter Temp: {{ temp|int }} °C</font>
          {% else %}
            ### <font color="#e74c3c">🔥 Inverter Temp: {{ temp|int }} °C</font>
          {% endif %}
      - type: markdown
        content: >
          ## 🏠 Home Load 
          {% set power_grid = states('sensor.grid_power_import')|float(0) %}
          {% if power_grid < 50 %}
            ### <font color="#2ecc71">⚡ Grid Import: {{ power_grid|int }} W</font>
          {% elif power_grid <= 500 %}
            ### <font color="#e67e22">⚡ Grid Import: {{ power_grid|int }} W</font>
          {% else %}
            ### <font color="#e74c3c">⚡ Grid Import: {{ power_grid|int }} W</font>
          {% endif %}
          ### 📊 Daily Consumption: {{ states('sensor.daily_home_energy_consumption') | float(0) | round(2) }} kWh
            
          ### 📅 Monthly Consumption: {{ states('sensor.monthly_home_energy_consumption') | float(0) | round(1) }} kWh

      - type: markdown
        content: >
          ## 🔋 Marstek Venus E 5,12 kWh
          {% set soc = states('sensor.venus_e_3_0_battery_level')|float(0) %}
          {% set p = states('sensor.venus_e_3_0_battery_power')|float(0) %}
          {% set cap = 5120 %}
          {% if p < -10 %}
            ### <font color="#2ecc71">⚡ Charging: {{ p|abs|int }} W</font>
            {% set rimanente = cap * (100 - soc) / 100 %}
            {% set ore = (rimanente / p|abs) if p|abs > 0 else 0 %}
            #### ⏳ Time to 100%: {{ ore|int }}h {{( (ore - ore|int)*60 )|int }}m
          {% elif p > 10 %}
            ### <font color="#f1c40f">🔌 Discharging: {{ p|int }} W</font>
            {% set rimanente = cap * (soc - 12) / 100 %}
            {% set ore = (rimanente / p) if p > 0 else 0 %}
            #### ⏳ Remaining to 12%: {{ ore|int }}h {{( (ore - ore|int)*60 )|int }}m
          {% else %}
            ### ⏸️ Standby ({{ soc|int }}%)
          {% endif %}
          
          #### ⚡ Battery Level: {{ soc|int }}%

      - type: markdown
        content: >
          ## 🕒 Recent Triggered Automations
          {% set ns = namespace(ults=[]) %}
          {% for aut in states.automation %}
            {% if aut.attributes.last_triggered is defined and aut.attributes.last_triggered != none %}
              {% set ns.ults = ns.ults + [(aut.attributes.last_triggered, aut.name)] %}
            {% endif %}
          {% endfor %}
          {% set sorted_auts = ns.ults | sort(reverse=true) %}
          {% for time, name in sorted_auts[:2] %}
            - **{{ name }}**: {{ as_timestamp(time) | timestamp_custom('%H:%M - %d/%m') }}
          {% endfor %}

Automations Code

YAML

- id: '01'
  alias: 01 - PV Charging Ramp
  description: Increases charge +100W every 60s. 85% PV start, reduces charge based on grid import. Passive only, PV>100W, SOC<99%
  triggers:
    - trigger: time_pattern
      minutes: /1
  conditions:
    - condition: state
      entity_id: select.venus_e_3_0_operating_mode
      state: passive
    - condition: template
      value_template: '{% set sunrise = as_datetime(state_attr(''sun.sun'', ''next_rising'')) - timedelta(days=1) %} {{ now() > sunrise + timedelta(hours=1, minutes=30) }}'
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_level
      below: 99
    - condition: template
      value_template: '{{ states(''sensor.inverter_power'') | float(0) > 200 or states(''input_number.marstek_last_charge_power'') | float(0) > 0 }}'
    - condition: numeric_state
      entity_id: sensor.inverter_power
      above: 100
    - condition: not
      conditions:
        - condition: state
          entity_id: sensor.inverter_power
          state: unavailable
  actions:
    - variables:
        pv: '{{ states(''sensor.inverter_power'') | float(0) }}'
        last: '{{ states(''input_number.marstek_last_charge_power'') | float(0) }}'
        import_now: '{{ states(''sensor.grid_power_import'') | float(0) }}'
        pv_85: '{{ (pv * 0.85) | int }}'
        target: '{% set imp = [import_now, 0] | max %} {% if last == 0 %} {% set t = pv_85 %} {% elif imp > 100 %} {% set t = last - imp - 50 %} {% elif imp > 40 %} {% set t = last - imp %} {% else %} {% set t = last + 100 %} {% endif %} {% set t = [t, 2500] | min %} {% set t = [t, 50] | max %} {{ t | int }} '
        delta: '{{ (target | int - last | int) | abs }}'
    - choose:
        - conditions:
            - condition: template
              value_template: '{{ delta | int >= 100 }}'
          sequence:
            - action: marstek.set_passive_mode
              data:
                device_id: YOUR_DEVICE_ID
                power: '{{ target | int * -1 }}'
            - action: input_number.set_value
              target:
                entity_id: input_number.marstek_last_charge_power
              data:
                value: '{{ target }}'
  mode: single

- id: '02'
  alias: 02 - Self Consumption
  description: Forces AUTO if import > 700W (both on threshold exceed and if already present)
  triggers:
    - trigger: numeric_state
      entity_id: sensor.grid_power_import
      above: 700
      for:
        seconds: 10
    - trigger: time_pattern
      minutes: /1
  conditions:
    - condition: numeric_state
      entity_id: sensor.grid_power_import
      above: 700
    - condition: state
      entity_id: select.venus_e_3_0_operating_mode
      state: passive
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_level
      above: 12
  actions:
    - action: select.select_option
      target:
        entity_id: select.venus_e_3_0_operating_mode
      data:
        option: auto
  mode: single

- id: '03'
  alias: 03 - Passive After Import
  description: 'Cyclic check: returns to passive if import < 500W for 2 min, daytime only'
  triggers:
    - trigger: numeric_state
      entity_id: sensor.grid_power_import
      below: 500
      for:
        minutes: 2
    - trigger: time_pattern
      minutes: /2
  conditions:
    - condition: template
      value_template: '{% set sunrise = as_datetime(state_attr(''sun.sun'', ''next_rising'')) - timedelta(days=1) %} {% set sunset = as_datetime(state_attr(''sun.sun'', ''next_setting'')) %} {{ now() > sunrise + timedelta(minutes=80) and now() < sunset - timedelta(minutes=90) }} '
    - condition: state
      entity_id: select.venus_e_3_0_operating_mode
      state: auto
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_power
      below: 500
    - condition: not
      conditions:
        - condition: state
          entity_id: sensor.inverter_power
          state: unavailable
  actions:
    - action: marstek.set_passive_mode
      data:
        device_id: YOUR_DEVICE_ID
        power: 0
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_last_charge_power
      data:
        value: 0
  mode: single

- id: '04'
  alias: '04 - Night Discharge'
  description: Switches to AUTO every 5 min within sunset-60min / sunrise+100min window if not already in AUTO and SOC > 10%
  triggers:
    - trigger: time_pattern
      minutes: /5
  conditions:
    - condition: template
      value_template: '{% set is_night = is_state(''sun.sun'', ''below_horizon'') %} {% set sunrise = as_datetime(state_attr(''sun.sun'', ''next_rising'')) %} {% set sunset = as_datetime(state_attr(''sun.sun'', ''next_setting'')) %} {% if is_night %} {{ now() <= sunrise + timedelta(hours=1, minutes=40) }} {% else %} {{ now() >= sunset - timedelta(minutes=60) }} {% endif %}'
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_level
      above: 10
    - condition: not
      conditions:
        - condition: state
          entity_id: select.venus_e_3_0_operating_mode
          state: auto
  actions:
    - action: select.select_option
      target:
        entity_id: select.venus_e_3_0_operating_mode
      data:
        option: auto
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_last_charge_power
      data:
        value: 0
  mode: single

- id: '05'
  alias: 05 - Marstek Reload
  triggers:
    - trigger: time
      at: 06:30:00
  actions:
    - action: homeassistant.reload_config_entry
      target:
        device_id: YOUR_DEVICE_ID

- id: '06'
  alias: 06 - Venus Keepalive
  description: Reads every 30min 12:00-19:30 if in passive and SOC>98%, prevents daytime sleep
  triggers:
    - trigger: time_pattern
      minutes: /30
  conditions:
    - condition: state
      entity_id: select.venus_e_3_0_operating_mode
      state: passive
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_level
      above: 98
    - condition: time
      after: '12:00:00'
      before: '19:00:00'
  actions:
    - action: marstek.set_passive_mode
      data:
        device_id: YOUR_DEVICE_ID
        power: 0
  mode: single

- id: '07'
  alias: 07 - Periodic Marstek 2000W Grid Charge Maintenance
  description: Forces 2000W grid charge 1h after sunset every 14gg if 100% was not reached in the last 2 weeks. Stops at 100% or after 3h.
  triggers:
    - trigger: sun
      event: sunset
      offset: "+01:00:00"
  conditions:
    - condition: numeric_state
      entity_id: sensor.venus_e_3_0_battery_level
      below: 99
    - condition: template
      value_template: >
        {% set last = state_attr('automation.07_manutenzione_ricarica_periodica_marstek_2000w', 'last_triggered') %}
        {{ last is none or (as_timestamp(now()) - as_timestamp(last)) / 86400 >= 14 }}
  actions:
    - action: marstek.set_passive_mode
      data:
        device_id: YOUR_DEVICE_ID
        power: -2000
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_last_charge_power
      data:
        value: 2000
    - wait_for_trigger:
        - trigger: numeric_state
          entity_id: sensor.venus_e_3_0_battery_level
          above: 99.9
      timeout: "03:00:00"
    - action: marstek.set_passive_mode
      data:
        device_id: YOUR_DEVICE_ID
        power: 0
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_last_charge_power
      data:
        value: 0
  mode: single

I’m currently running this setup and it’s working like a charm. Just a quick heads-up: I’m an enthusiast rather than a professional programmer, so while I’ll do my best to help and reply, I might not always be able to handle very complex feature requests or dedicate a lot of time to troubleshooting. Contributions, feedback, and improvements from the community are always more than welcome!

Here is a quick preview of how the dashboard looks in action once everything is set up:

:warning: Disclaimer: This configuration works smoothly on my personal setup, but I take no responsibility for any system malfunctions, unexpected behaviors, battery wear, or potential hardware damage if you decide to use or adapt these scripts. Always double-check your configurations, hardware IDs, and safety limits before running them on your own equipment.

If you find this useful or if it saved you some time, feel free to buy me a coffee :hot_beverage:: :backhand_index_pointing_right: https://ko-fi.com/dibilino

Let me know if you have any questions, suggestions, or improvements!