Skip to content

Victron Support #2846

Open
Open

Description

@jfreegard1

Currently trying to integrate Victron with charge/export freeze support. Using the HACs Victron MQTT integration. Whilst my setup is non-standard, it's the Victron controls that I struggle with, control options as follows:

Grid Set Point (W) - number.victron_mqtt_(victron id)_system_0_system_ac_power_set_point (positive pulls from grid, negative sends to grid)

Min battery SoC (%) - number.victron_mqtt_(victron id)_system_0_system_ess_min_soc_limit

AC Input Limit (%) - number.victron_mqtt_(victron id)_)_system_0_system_ac_input_limit

AC Export Limit (%) - number.victron_mqtt_(victron id)_system_0_system_ac_export_limit

Inverter Mode - select.victron_mqtt_(victron id)_vebus_276_vebus_inverter_mode
Options:
ID Value Display Name

  1. Charger Only - 1 Charger Only
  2. Inverter Only - 2 Inverter Only
  3. On - 3 On
  4. Off - 4 Off

Lots more configuration/sensors available as documented by the project here:
https://github.com/tomer-w/victron_mqtt?tab=readme-ov-file

My system uses another inverter manufacturer for solar, hence not including details here (simple sensor to connect) I also had to consolidate a few sensors/calibrate, hence the VRM sensors. Historic data in the apps.yaml is in KWH

current apps.yaml is 
pred_bat:
# Home Assistant API connection

  ha_url: http://XXX.XXX.XXX.XXX:8123
  ha_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

  module: predbat
  class: PredBat

  # Home Assistant entity prefix
  prefix: predbat

  # Timezone for scheduling
  timezone: Europe/London

  # Custom inverter type code 'VC' (Victron Custom)
  inverter_type: "VC"
  num_inverters: 1

  # Currency symbols
  currency_symbols:
  - '£'
  - 'p'

  # Thread configuration
  threads: auto

  # Historical data sensors
  load_today:
  - sensor.vrm_equiv_1_ac_loads_daily_complete
  import_today:
  - sensor.vrm_equiv_1_grid_import_daily_complete
  export_today:
  - sensor.vrm_equiv_1_grid_export_daily_complete
  pv_today:
  - sensor.vrm_equiv_1_solar_daily_complete

  # Power sensors
  battery_power:
  - sensor.dc_battery_power_inverted_predbat
  pv_power:
  - sensor.vrm_equiv_1_solar_power_complete
  load_power:
  - sensor.vrm_equiv_1_ac_loads_complete
  grid_power:
  - sensor.victron_mqtt_(enter victron ID)_grid_32_grid_power

  # Grid power is negative for import, positive for export
  grid_power_invert:
  - true

  # Battery configuration
  soc_percent:
  - sensor.victron_mqtt_(enter victron ID)_battery_512_battery_soc
  soc_max:
  - 87.36    # 1824Ah * 48V / 1000 = 87.36 kWh
  battery_rate_max:
  - 20000    # 2 x 15kW inverter max (MultiPlus-II 48/15000)
  battery_voltage:
  - sensor.victron_mqtt_(enter victron ID)_battery_512_battery_voltage

  # Grid setpoint control
  charge_rate:
  - number.victron_mqtt_(enter victron ID)_system_0_system_ac_power_set_point

  discharge_rate:
  - number.victron_mqtt_(enter victron ID)_system_0_system_ac_power_set_point

  # ESS Minimum SOC (Reserve) Control
  reserve:
  - number.victron_mqtt_(enter victron ID)_system_0_system_ess_min_soc_limit

  # Inverter Mode Control
  inverter_mode:
  - select.victron_mqtt_(enter victron ID)_vebus_276_vebus_inverter_mode

  # Inverter output limit in Watts
  inverter_limit:
  - 25000

  # PV forecasts (Solcast)
  pv_forecast_today: sensor.solcast_pv_forecast_forecast_today
  pv_forecast_tomorrow: sensor.solcast_pv_forecast_forecast_tomorrow
  pv_forecast_d3: sensor.solcast_pv_forecast_forecast_day_3
  pv_forecast_d4: sensor.solcast_pv_forecast_forecast_day_4

  ### Octopus rates and integration

  # Using manual rates for testing
  # metric_octopus_import: "sensor.octopus_energy_electricity_(account_Info)_current_rate"
  # metric_octopus_export: "sensor.octopus_energy_electricity_(account_Info)_export_current_rate"

  # Fixed manual Octopus Cosy import rates (p/kWh)
  rates_import:
  - start: "00:00:00"
    end: "04:00:00"
    rate: 27.69
  - start: "04:00:00"
    end: "07:00:00"
    rate: 13.58
  - start: "07:00:00"
    end: "13:00:00"
    rate: 27.69
  - start: "13:00:00"
    end: "16:00:00"
    rate: 13.58
  - start: "16:00:00"
    end: "19:00:00"
    rate: 41.54
  - start: "19:00:00"
    end: "22:00:00"
    rate: 27.69
  - start: "22:00:00"
    end: "23:59:59"
    rate: 13.58

  # Manual export rates (15p/kWh flat rate - adjust to your actual rate)
  rates_export:
  - start: "00:00:00"
    end: "23:59:59"
    rate: 15.0

  # Octopus saving session configuration
  octopus_saving_session: 're:(event.octopus_energy_([0-9a-z_]+|)_octoplus_saving_session_events)'
  octopus_saving_session_octopoints_per_penny: 8

  # Octopus free session configuration
  octopus_free_session: 're:(event.octopus_energy_([0-9a-z_]+|)_octoplus_free_electricity_session_events)'

  # Carbon Intensity data
  carbon_intensity: 'sensor.carbon_intensity_westmidlands_current_rating'
  carbon_intensity_forecast: 'event.carbon_intensity_westmidlands_current_day_rates'

  # Historical data configuration
  days_previous:
  - 7
  days_previous_weight:
  - 1
  forecast_hours: 48

  # Notification devices
  notify_devices:

  # ==========================================
  # CUSTOM INVERTER DEFINITION
  # ==========================================
  
  inverter:
    VC:
      name: "Victron MultiPlus-II ESS"
      has_rest_api: false
      has_mqtt_api: false
      
      # Power control configuration
      output_charge_control: "power"
      has_charge_enable_time: false
      has_discharge_enable_time: false
      has_time_window: false
      charge_control_immediate: true
      
      # SOC and reserve configuration
      has_target_soc: false
      has_reserve_soc: true
      soc_units: "%"
      
      # Inverter mode support
      has_ge_inverter_mode: false
      inverter_mode_options:
      - "On"
      - "Inverter Only"
      - "Charger Only"
      - "Off"
      
      # Freeze mode support
      support_charge_freeze: true
      support_discharge_freeze: true
      
      # Technical settings
      num_load_entities: 1
      charge_time_format: "S"
      charge_time_entity_is_option: false
      time_button_press: false
      clock_time_format: "%Y-%m-%d %H:%M:%S"
      write_and_poll_sleep: 2
      current_dp: 0

Activity

  1. gcoan commented on Oct 31, 2025

    @gcoan
    Collaborator

    I don't know if its the way you pasted your apps.yaml in to github, but a lot of your YAML appears to be misaligned

    e.g.

    instead of

          inverter_mode_options:
          - "On"
          - "Inverter Only"
          - "Charger Only"
          - "Off"
    

    it should be

          inverter_mode_options:
            - "On"
            - "Inverter Only"
            - "Charger Only"
            - "Off"
    

    each sub entry should be indented by a further two spaces

    But getting back to your request. Unless there is another victron user here that has got the inverter working we're not easily going to be able to help you.

    The first thing you need to do is to work out how to manually control your inverter from HA, to start and stop charging to a % limit, start and stop exporting, return to Eco mode or equivalent.

    Once you have worked out how to do this then we can translate those into predbat commands.
    If you want to create the series of commands in an automation script then predbat can execute the script if that's easier.

  2. AlexCPU commented on Oct 31, 2025

    @AlexCPU

    There a discussion on this #798

    When I've got a bit more time I'll try and write up a template using the HACS MQTT integration, but I'm currently using a modbus TCP implementation using the raw modbus component as the HACS one didn't exist when I started.

    Below is an extract from my apps.yaml. Obviously you'll need to find your appropriate entities. The important bit is the home assistant automations that control the battery. Mine won't be helpful as they're ModbusTCP, but the only two settings I'm changing are the grid set point to 24000W to force charging, and change the inverter mode to charger only for freeze discharge.

      num_inverters: 1
    
      inverter_type: VICTRON
      inverter:
        name: "Victron Multiplus-II"
        has_rest_api: False
        has_mqtt_api: False
        output_charge_control: "power"
        has_charge_enable_time: False
        has_discharge_enable_time: False
        has_target_soc: False
        has_reserve_soc: False
        has_timed_pause: False
        charge_time_format: "S"
        charge_time_entity_is_option: False
        soc_units: "%"
        num_load_entities: 1
        has_ge_inverter_mode": False
        time_button_press: False
        clock_time_format: "%Y-%m-%d %H:%M:%S"
        write_and_poll_sleep: 2
        has_time_window: False
        support_charge_freeze: True
        support_discharge_freeze: True
    
      # Services to control charging/discharging
      charge_start_service:
        service: automation.trigger
        entity_id: automation.predbat_start_charging
      charge_stop_service:
        service: automation.trigger
        entity_id: automation.predbat_stop_charging
      discharge_freeze_service:
        service: automation.trigger
        entity_id: automation.predbat_freeze_discharge
      charge_freeze_service:
        service: automation.trigger
        entity_id: automation.predbat_freeze_discharge
        
    
      #
      # Run balance inverters every N seconds (0=disabled) - only for multi-inverter
      balance_inverters_seconds: 60
      #
      battery_power:
      - sensor.battery_power
      battery_power_invert:
      - true
      charge_rate:
      - input_number.battery_charge_rate
      discharge_rate:
      - input_number.battery_discharge_rate
      pv_power:
      - sensor.pv_on_input
      load_power:
      - sensor.ac_consumption_l1
      grid_power:
      - sensor.grid_l1
      grid_power_invert:
      - true
      soc_percent:
      - sensor.battery_soc
      soc_max:
      - input_number.battery_capacity
      battery_rate_max:
      - 9880 # 190A * 52V
    
      # Inverter max AC limit (one per inverter). E.g for a 3.6kw inverter set to 3600
      # If you have a second inverter for PV only please add the two values together
      inverter_limit:
      - 10000
      inverter_limit_charge:
      - 3100
      inverter_limit_discharge:
      - 5000
    

    My automations are effectively:
    Start charging:

    • Set grid set point to 24000
    • Set inverter mode to on
      Stop charging:
    • Set grid set point to 10
    • Set inverter mode to on
      Freeze discharge:
    • Set grid set point to 10
    • Set inverter mode to charger only

    I haven't looked at forcing discharge, as none of the maths for my usage says it's worth giving up my FIT for, but I'll try and look at it as part of writing a template, hopefully within the next couple of weeks.

  3. self-assigned this
    on Nov 5, 2025
  4. Lanmate commented on Jul 12, 2026

    @Lanmate

    To help someone else figure this out in future, I have come up with the following for my own use.

    System

    • Cerbo GX
    • MultiPlus-II 48/15000/200-100
    • Victron GX Integration
    • AC coupled solar with separate PV inverter - When writing below about DC solar, this is theoretical based on how I understand the inverter will behave.

    Entities

    Cerbo GX

    • AC power setpoint = number.gx_device_ac_power_setpoint
    • ESS mode = select.gx_device_ess_mode
    • ESS mode (Hub4) = select.gx_device_ess_mode_hub4
    • ESS max charge power limit = number.gx_device_ess_max_charge_power_limit
    • ESS max inverter power limit = number.gx_device_ess_max_inverter_power_limit
    • ESS max feed-in power = gx_device_ess_max_feed_in_power

    Inverter

    • Inverter mode = select.multiplus_ii_48_15000_200_100_id_276

    Common Settings

    • Inverter mode = On - Explanation at the end on why.
    • ESS mode = Optimised (without BatteryLife) - I have LiFePO4 batteries.
    • ESS mode (Hub4) = Optimised mode or 'keep batteries charged' and phase compensation enabled - I have single phase, no idea on what the difference is between phase compensation enabled/disabled.

    Modes

    Normal Running (Demand)

    For use with charge_stop_service and discharge_stop_service

    • AC power setpoint = 0
    • ESS max charge power limit = -1
    • ESS max inverter power limit = -1

    Inverter will power house loads, excess solar will charge the battery, grid will top up house load if needed.

    Charging (Max Power)

    For use with charge_start_service

    • AC power setpoint = 24000 (I have a 100A supply, adjust appropriately if you have smaller)
    • ESS max charge power limit = -1
    • ESS max inverter power limit = -1

    Grid will power house loads, inverter will charge the batteries at the maximum rate.

    Discharging (Max Power)

    For use with discharge_start_service

    • AC power setpoint = -8500 (I have a 8.5kW export restriction set via ESS max feed0in power, adjust appropriately if you have a different one)
    • ESS max charge power limit = -1
    • ESS max inverter power limit = -1

    Inverter will power house loads, remaining inverter capacity will be used to export to the grid up to the maximum rate.

    Freeze charging

    For use with charge_freeze_service

    • AC power setpoint = 0
    • ESS max charge power limit = -1
    • ESS max inverter power limit = 0

    The behaviour of this changes depending on AC vs DC solar:

    • AC solar: Grid will power house loads, unless there is more solar than the house load demands and then the excess will charge the battery.
    • DC solar: Grid will power house loads, solar will charge the battery.

    Freeze exporting

    For use with discharge_freeze_service

    • AC power setpoint = 0
    • ESS max charge power limit = 0
    • ESS max inverter power limit = -1

    The behaviour of this changes depending on AC vs DC solar:

    • AC solar: Inverter will power house loads, unless there is more solar than the house load demands and then the excess will be exported to the grid.
    • DC solar: Inverter will power house loads, solar will be directed through the inverter to power house loads, unless there is more solar than the house load demands and then the excess will be exported to the grid.

    Notes

    AC power setpoint could be set to a small positive number e.g. 10, to stop leak back to grid from battery in normal operations.

    I specifically avoided using the "Inverter only" and "Charger only" modes, or "External Control" in the Cerbo GX. These are bad in my opinion because:

    • Inverter only - Prevents battery charging, even from excess solar, which is desirable in all modes except Freeze exporting.
    • Charger only - If the grid connection fails when this mode is set, the house cannot be powered as the inverter is physically disabled.
    • External Control - Would need home assistant to have some complex automations to precisely manage the power levels. Would be a problem if home assistant goes offline. I'd rather set general "aims" for the inverter to meet and let it manage the precise power levels to achieve those aims.
    • Changing the inverter mode physically opens/closes relays in the inverter, which I felt was a bad idea to do multiple times a day when switching mode. The above settings are all software limits.

    Edit: Fix various typos and move sections around which didn't belong where I originally had them

  5. Lanmate commented on Jul 20, 2026

    @Lanmate

    An update - I've been using these since just before the last post and they've been working brilliantly since. All modes have been thoroughly tested in the last week.

  6. WesSec commented on Aug 3, 2026

    @WesSec
    Sponsor

    To add my 2 cents, i've gone various (difficult) routes before and decided to keep it simple for now. predbat exposes the requested state via predbat.status within node red i take that value, and set gridpoint accordingly:

    Image

    I have not implemented freeze modes yet, will update if done

  7. jeangroen commented on Sep 14, 2026

    @jeangroen
    Contributor

    A working target-SoC setup on Victron — traps, freeze export, and a watchdog

    Updated 21 September 2026. Changes since the first version of 14 September: the step-5 "trap" turned out to be a side effect of BatteryLife mode and is withdrawn; a much bigger BatteryLife trap is added; freeze export now works (freeze capabilities override + charge current); the mode advice is now Control charge & discharge; the watchdog heartbeat has been replaced because the old one gave false alarms; and there is a new section on Predbat not recovering after a Home Assistant restart. Full apps.yaml updated at the bottom.

    @Lanmate's mode mapping above is the better control model where you can use it: an AC power setpoint plus ESS limits gives you real charge and discharge modes, which the target-SoC route cannot. This comment covers the other route, the one the Victron section of the docs currently describes, where Predbat mainly writes a target SoC. It has been running here since 13 September and has planned and executed freeze export since 15 September.

    System: MultiPlus-II 48/5000/70-50 with Cerbo GX (Venus OS v3.79), Seplos 16 kWh, Enphase AC-coupled PV (east/west, 9.66 kWp), single phase, Home Assistant with the Predbat add-on (v9.0.3). Victron GX integration, so the same gx_device_* entity names as @Lanmate.

    The thing to understand first: on a Victron the target SoC register is the minimum SoC. Every value Predbat writes as "charge up to here" is read by the Cerbo at the same time as "never go below here". Most of what follows comes from that.


    1. Set the ESS mode to "Optimized (without BatteryLife)" — or nothing you write is binding

    This one cost me an evening and it gives no error anywhere.

    In Optimized with BatteryLife (select.gx_device_ess_mode = optimized_battery_life) the minimum SoC you write is only a lower bound. The Cerbo keeps its own, dynamically moving value next to it, the Active SoC limit (sensor.<name>_active_soc_limit in the GX integration), and that is what actually applies. number.gx_device_ess_min_soc_limit reads back exactly what you wrote, so everything looks fine.

    Measured on 14 September:

    time written min SoC Active SoC limit
    13 Sep 21:44 10 15 (BatteryLife +5% because the battery did not reach 100% that day)
    14 Sep 15:24 10 80 (set by the Cerbo itself)
    14 Sep 17:48 10 75

    That evening the battery stopped discharging with 11.6 kWh in it and the house ran on the grid at peak price. After switching to optimized_no_battery_life, the Active SoC limit went from 75 to 10 within one second and the battery resumed discharging.

    Keep the Active SoC limit next to your written min SoC on a dashboard. As long as the two move together, Predbat is really in control. If they drift apart, something (a firmware update, a restored setting, a second controller) has let the Cerbo go its own way again.

    Also check that Dynamic ESS (select.gx_device_dess_mode) is off, otherwise two optimisers fight over the same knobs.

    2. Use SK as the base type, and override its freeze capabilities

    @jfreegard1 and @AlexCPU both hit the wall of a custom type. A fully custom inverter_type has to be defined completely; an incomplete block gives:

    Inverter 0 unable to read charge window time - no source is configured
    

    The shortcut: SK (Sunsynk) is a built-in type whose capability profile matches what the docs describe for Victron:

    key SK
    has_charge_enable_time False — no charge window to open
    has_discharge_enable_time False — no discharge window to open
    has_target_soc True — only a target SoC to write
    has_time_window False
    output_charge_control "current" — charge rate is written in amps

    To be clear, this does not claim there is a Sunsynk here. It borrows the closest matching capability set until there is a real Victron type (the word "Victron" does not appear in INVERTER_DEF).

    But SK has both freeze capabilities set to False, and execute.py then switches freeze charge and freeze export off again every compute cycle, whatever you set in HA. The only trace is in the log:

    Note: Inverter does not support discharge freeze - disabled
    Note: Inverter does not support charge freeze - disabled
    

    The inverter: block in apps.yaml overwrites every key of the profile (inverter.py: INVERTER_DEF[self.inverter_type][key] = inverter_def[key]), so this is enough:

    inverter_type: "SK"
    inverter:
      name: "Victron MultiPlus-II 48/5000/70-50"
      support_charge_freeze: true
      support_discharge_freeze: true

    After saving, the two "does not support ... freeze" lines disappear and set_export_freeze_only(True) shows up in the planner options. I checked the v9.1.0 diff: the override mechanism and both capability keys are unchanged. After every Predbat update, check the log for those two lines.

    3. Mode: "Control charge & discharge", with a lock on forced battery export

    Control SOC only sounds exactly right for an inverter that can only write a SoC. It is the wrong choice: Predbat sets set_charge_window to False, isCharging never becomes True, execute.py ends up at adjust_battery_target(0), and nothing ever charges.

    Control charge works. I now run Control charge & discharge, because that is what lets Predbat plan freeze export (solar to the grid instead of into the battery when export pays more than storing). It also opens up forced battery export, which I do not want (my supplier pays a solar bonus that does not apply to battery energy). Freeze and forced export sit behind different thresholds in plan.py, so they can be separated:

    setting value effect
    input_number.predbat_metric_min_improvement_export 50 forced export has to gain 50p per half hour before it is planned — in practice never
    input_number.predbat_metric_min_improvement_export_freeze 0.1 freeze export stays attractive
    switch.predbat_export_more_solar on Predbat actively looks for freeze-export slots

    Note that this is an economic threshold, not a lock. If forced export ever pays more than 50p per slot, Predbat will plan it. On a Victron with a slightly negative grid setpoint nothing physically exports from the battery anyway, but the plan will raise the min SoC.

    set_export_freeze_only does nothing useful here Corrected 29 September: set_export_freeze_only is honoured in plan.py too (for export windows the planner then only tries idle or freeze). It only appeared to do nothing because the SK profile had freeze disabled (see section 2).

    4. Freeze export via the charge current, not the charge power

    SK controls by current: Predbat writes (power ÷ battery voltage) to timed_charge_current, and during a freeze slot execute.py calls adjust_charge_rate(0). That 0 is the only signal needed:

    timed_charge_current:
      - input_number.predbat_charge_current_request     # helper, translated by an automation
    timed_discharge_current:
      - input_number.predbat_discharge_current_request  # nobody reads this; it only prevents an error status

    An automation writes 0 to number.gx_device_ess_max_charge_current when the helper is 0, and −1 (unlimited) for any other value. I deliberately do not pass the ampere value through: Predbat assumes 3500 W → 67 A while the MultiPlus can do 70 A, so passing it would throttle normal charging for no reason.

    Why current and not ess_max_charge_power_limit (a note for @Lanmate's freeze-export mapping): on my system, with AC-coupled PV, the charge power limit at 0 did not stop the battery charging from solar surplus. It kept taking 285 W. The charge current limit at 0 brought it down to 26 W within a minute. Your firmware or topology may behave differently, but it is worth checking with a meter.

    Measured end to end on 15 September:

    11:30:52  predbat charge current helper   67 → 0
    11:31:27  gx_device_ess_max_charge_current -1 → 0    (automation, 30 s stability delay)
    11:32:21  battery 59 W with 3,636 W of solar         → surplus goes to the grid
    11:32:28  helper back to 67
    11:32:58  max_charge_current               0 → -1
    11:34:07  battery 2,541 W with 3,671 W of solar      → charging normally again
    

    5. Trap: the min-SoC register has a step of 5 — withdrawn

    In the first version I reported that number.gx_device_ess_min_soc_limit (it advertises step: 5) silently discards any value that is not a multiple of 5 (12 vanished, 15 came through). That was measured in BatteryLife mode. Since switching to Optimized without BatteryLife the Cerbo accepts every whole percent, even though the entity still reports step: 5. For example, on 20 September it took 22, 23, 24, 26, 28, 29, 30 … 41 in sequence. So my suggestion to honour the entity's step attribute would actually be counterproductive here. If you see this behaviour, check your ESS mode first (section 1).

    6. Trap: reset_inverter() writes 100% as its "safe" value

    In apps/predbat/execute.py, reset_inverter() calls:

    inverter.adjust_battery_target(100.0, False)

    On a GivEnergy that is safe: 100 means "you may charge up to 100" inside a time window. On a Victron it means "never go below 100". The battery charges itself full from the grid and is then forbidden to deliver anything. The same call appears in the calibration branch and in Inverter.self_test().

    It runs on every Predbat start and on every change of select.predbat_mode or switch.predbat_set_read_only. Normally Predbat overwrites it within the same cycle, so the 100 only stands for seconds. But it is the wrong direction for any inverter where the target SoC acts as a minimum, and anything that stops Predbat in between leaves it standing.

    Suggestion: make the reset value configurable per inverter type, or for has_target_soc-only inverters reset to the current SoC or the configured reserve instead of 100.

    7. The translation layer

    charge_limit points at a helper, not at the Cerbo, and an automation translates it. The one-minute delay swallows the transient 100 from section 6. A real charge target holds for half an hour or more, so starting a minute later costs nothing, while a value that stands for seconds never arrives.

    Simplified version (mine also has a 5-minute heartbeat and the readiness check from section 8):

    alias: Predbat target SoC -> Victron ESS min SoC
    triggers:
      - trigger: state
        entity_id: input_number.predbat_target_soc
        for:
          minutes: 1
    actions:
      - variables:
          v: "{{ states('input_number.predbat_target_soc') | float(10) }}"
          # Only round up to a multiple of 5 if the ESS mode is NOT optimized_no_battery_life
          stepped: >-
            {{ v if is_state('select.gx_device_ess_mode', 'optimized_no_battery_life')
               else ((v / 5) | round(0, 'ceil')) * 5 }}
          target: "{{ [[stepped | int, 10] | max, 100] | min }}"
      - action: number.set_value
        target:
          entity_id: number.gx_device_ess_min_soc_limit
        data:
          value: "{{ target }}"
    mode: queued

    Clamped between 10 and 100. The lower bound repeats what Predbat already clamps to via battery_min_soc, so no mistake in apps.yaml can pull the battery below the floor. Mine also sends a notification when the ESS mode drifts away from optimized_no_battery_life, because of section 1.

    8. A watchdog — which I think every Victron route needs (revised)

    This part is not specific to the target-SoC approach. Whatever you write to a Victron stays written. A min SoC left at 80 means the battery can no longer discharge; a charge current left at 0 means it can no longer charge from solar; a grid setpoint left at 24000 is the same failure with a bigger bill. Predbat stopping is not the same as Predbat undoing.

    The heartbeat in my first version was wrong. I used the age of last_reported of sensor.predbat_pv_forecast_h0 with a 30-minute threshold. That sensor is written only once every 30 minutes, so there was no margin, and it gave a false alarm at 02:45 one night. Lesson: measure how often your heartbeat entity is actually written, and make the threshold a multiple of that.

    predbat.status is updated every compute cycle (5 minutes). Together with predbat.last_started it gives one template helper that everything else uses:

    {%- set s = states.predbat.status -%}
    {%- set ls = as_datetime(states('predbat.last_started')) -%}
    {%- if s is none or ls is none -%}unknown
    {%- elif s.last_updated < ls -%}{{ 'stalled' if (now() - ls).total_seconds() > 900 else 'restarting' }}
    {%- elif (now() - s.last_updated).total_seconds() > 900 -%}stalled
    {%- else -%}ready
    {%- endif -%}
    • ready: the status was updated after the last start (first compute cycle done) and is less than 15 minutes old.
    • restarting: Predbat has started but has not finished a cycle yet.
    • stalled: the status is older than 15 minutes, or a restart has not finished a cycle within 15 minutes.
    • unknown: the predbat.* entities do not exist. Predbat creates them over the API and they do not survive a Home Assistant restart.

    The translation automations use it like this:

    state min SoC charge current
    restarting / stalled / helper unavailable immediately 10 immediately −1
    unknown write nothing (Predbat is back within a cycle); after 15 min still 10 same; after 15 min −1
    ready pass through once ready ≥ 1 min and the value has been stable ≥ 1 min same with 30 s

    The stability check is deliberately separate from the helper. Predbat sometimes writes a status in the middle of a cycle, so the helper can say ready a little too early. The stability check is what really keeps transient reset_inverter() values away from the Cerbo. After a Predbat restart in the middle of a freeze slot, the freeze was restored 86 seconds later.

    9. Predbat does not always recover after a Home Assistant restart

    Update 29 September: fixed upstream in v9.1.0 (#5135). Predbat now stops itself when the HA interface is missing, so the add-on restarts it. My hang was on v9.0.x; the watchdog below is now only a safety net.

    When HA restarts, Predbat sometimes stops with Too many API errors, stopping, and then the add-on watchdog restarts it before HA is back:

    Cannot initialise Home Assistant Interface
    HAHistory: No HAInterface available, cannot start history updates   (every 2 minutes, forever)
    

    The HA connection is only set up at start and never retried, and the watchdog sees a running process, so it stays like that. Whether it happens depends on timing (which API drops first). I have seen both the good and the bad case.

    My workaround is an automation: if the add-on is running (binary_sensor.predbat_running, enable it in the Supervisor integration) and the helper above has not been ready for 10 minutes, send a notification, call hassio.app_restart, then wait 30 minutes (mode: single). A healthy Predbat is ready within a minute after an add-on restart and within one cycle after an HA restart, so 10 minutes is a safe threshold.

    Possibly worth a separate issue: retry the HA interface initialisation instead of giving up.

    10. Two sensors not to use as indicators

    • sensor.gx_device_dc_battery_state flips between charging and idle every few seconds around zero (ESS control dead band of 0–32 W). I measured eight changes in nine minutes. It is useless as a "battery is charging" indicator.
    • The Victron per-phase consumption figures are wrong if the MultiPlus is not on L1 (mine is on L3). Venus OS assumes L1 and adds the inverter power to the wrong phase. Totals are correct, so feed Predbat a load_power you calculate yourself (grid + PV + battery AC), not ac_loads.

    Full apps.yaml

    apps.yaml (entity serials anonymised; comments explain the why)
    pred_bat:
      module: predbat
      class: PredBat
      prefix: predbat
      timezone: Europe/Amsterdam
      currency_symbols:
      - '€'
      - 'c'
      threads: auto
      notify_devices:
      - notify
    
      # --- Inverter -------------------------------------------------------
      # Never leave inverter_type empty: Predbat then assumes a GivEnergy
      # and caps charge/discharge at 2600 W.
      # No charge_rate/discharge_rate/service keys: a Victron has no use for
      # them, and every missing entity Predbat tries to write puts its status
      # into error.
      num_inverters: 1
    
      # SK is NOT a claim that this is a Sunsynk. It is the built-in type whose
      # capability profile matches Victron: no charge/discharge window, target
      # SoC only, control by current. The inverter block below is an OVERRIDE
      # on that type, not a full definition.
      inverter_type: "SK"
      inverter:
        name: "Victron MultiPlus-II 48/5000/70-50"
        # SK sets both freeze capabilities to False, and execute.py then turns
        # freeze charge and freeze export off every cycle. The inverter block
        # overwrites every key of the profile (inverter.py). After every update:
        # check the log for "does not support ... freeze".
        support_charge_freeze: true
        support_discharge_freeze: true
    
      # --- Power limits, all LITERAL WATTS ----------------------------------
      # battery_rate_max 3500 is the MultiPlus charger, not the battery: the
      # "70" in 48/5000/70-50 is the DC charge current. Measured peak
      # 67.3 A / 3,569 W; the BMS allows 180 A.
      # inverter_limit 4000: continuous AC power of the 5 kVA MultiPlus-II.
      battery_rate_max: 3500
      inverter_limit: 4000
      inverter_reserve_max: 100
    
      # --- Battery ---------------------------------------------------------
      # soc_max 16.08 kWh = 314 Ah x 51.2 V. A FIXED number: if the BMS
      # capacity ever changes, update this by hand.
      # reserve as a fixed number: Predbat cannot write the reserve on a
      # Cerbo, but this tells it there is a floor at 10%.
      soc_max:
      - 16.08
      reserve:
      - 10
      battery_temperature: 20
    
      # battery_min_soc is the floor Predbat clamps every target to. Without
      # it Predbat used its own default of 4% and wrote a 4 outside charge
      # windows.
      battery_min_soc:
      - 10
    
      # --- History ---------------------------------------------------------
      # import/export point straight at the P1 meter totals (never reset,
      # years of history). load/pv are utility meters on own sensors.
      load_today:
      - sensor.house_consumption_today
      import_today:
      - sensor.p1_meter_xxxxxxxxxxxx_total_power_import
      export_today:
      - sensor.p1_meter_xxxxxxxxxxxx_total_power_export
      pv_today:
      - sensor.pv_production_today
    
      # --- Live power ------------------------------------------------------
      # MIND THE SIGNS. Predbat expects battery_power positive = DISCHARGING,
      # grid_power negative = IMPORT. Wrong signs make the plan look normal
      # while every decision is mirrored.
      # load_power is a self-calculated sensor (grid + PV + battery AC), NOT
      # the Victron figure: Venus OS adds the MultiPlus to L1 while it is on L3.
      battery_power:
      - sensor.gx_device_dc_battery_power
      battery_power_invert:
      - true
      grid_power:
      - sensor.p1_meter_xxxxxxxxxxxx_active_power
      grid_power_invert:
      - true
      pv_power:
      - sensor.envoy_xxxxxxxxxxxx_current_power_production
      load_power:
      - sensor.house_consumption_total
      soc_percent:
      - sensor.gx_device_dc_battery_charge
    
      # --- The target SoC --------------------------------------------------
      # On a Victron this is at the same time the minimum SoC. It points at a
      # helper, not at number.gx_device_ess_min_soc_limit: an automation
      # translates it after 1 minute of stability (swallows the transient 100
      # from reset_inverter()) and clamps it between 10 and 100.
      charge_limit:
      - input_number.predbat_target_soc
    
      # --- Charge current --------------------------------------------------
      # SK has output_charge_control "current". Predbat writes 0 here during a
      # freeze slot; an automation then sets
      # number.gx_device_ess_max_charge_current to 0, otherwise to -1.
      # The discharge helper is read by nobody; it only exists because a
      # missing entity puts Predbat into error status every cycle.
      timed_charge_current:
      - input_number.predbat_charge_current_request
      timed_discharge_current:
      - input_number.predbat_discharge_current_request
    
      # --- Solar forecast --------------------------------------------------
      # Via the Solcast HACS integration, NOT polled by Predbat itself: a hobby
      # account has 10 API calls a day and every refresh costs one per roof
      # plane. Only the integration polls; Predbat and the Energy dashboard
      # both read it.
      pv_forecast_today: 're:(sensor.(solcast_|)(pv_forecast_|)forecast_today)'
      pv_forecast_tomorrow: 're:(sensor.(solcast_|)(pv_forecast_|)forecast_tomorrow)'
      pv_forecast_d3: 're:(sensor.(solcast_|)(pv_forecast_|)forecast_(day_3|d3))'
      pv_forecast_d4: 're:(sensor.(solcast_|)(pv_forecast_|)forecast_(day_4|d4))'
      pv_array_kwp: 9.66
    
      # --- Tariffs ---------------------------------------------------------
      # Own sensors that convert the supplier price series to Predbat's
      # raw_today/raw_tomorrow format (VAT, fees and tax included).
      # The export price includes a solar bonus that does NOT apply to battery
      # energy, hence metric_min_improvement_export = 50 in HA (section 3).
      metric_octopus_import: 'sensor.predbat_import_rate'
      metric_octopus_export: 'sensor.predbat_export_rate'
    
      # --- Load forecast ---------------------------------------------------
      load_filter_threshold: 30
    
      # No car, no iBoost. Without num_cars: 0 Predbat goes looking for Zappi
      # and Wallbox sensors.
      num_cars: 0

    Offer (done)

    Done: the docs PR is #5332, merged on 9 October 2026. The Victron section of docs/inverter-setup.md and templates/victron.yaml are now in main.

    docs/inverter-setup.md still links to templates/victron.yaml, but as of today that file does not exist in the repository (only sunsynk.yaml and the others). The link 404s.

    @AlexCPU mentioned writing a template for the HACS MQTT integration. Are you still on that? If not, I am happy to open the PR: @Lanmate's setpoint mapping as the main documented route, the target-SoC route above as the simpler fallback, the SK base type with the freeze override, the BatteryLife warning, and the watchdog. I would raise the code points (reset_inverter() writing 100 for target-SoC-only inverters, and freeze export at 100% SoC never calling discharge_freeze_service) as a separate issue, so a docs PR does not get held up behind them. (Done: #5302 for the freeze export, #5303 for reset_inverter().)

  8. jeangroen commented on Sep 21, 2026

    @jeangroen
    Contributor

    I've updated my comment above after a week of running it. The main changes: the step-5 trap was a BatteryLife side effect and is withdrawn, BatteryLife mode itself is a much bigger trap, freeze export now works (freeze capability override on SK plus the charge current), and the watchdog heartbeat has been replaced. The offer to write the Victron docs/template PR still stands. @gcoan, would that be welcome?

  9. WesSec commented on Sep 21, 2026

    @WesSec
    Sponsor

    Thanks for your extensive testing @jeangroen! Although I think your approach is solid in the way it's designed, I'd personally prefer a method that doesn't touch the Min SoC setting, similar to what @Lanmate described. There is no good or bad, just different usecases. If the docs could have both methods documented and worked out, that would be great.

    My Node-RED flow (mentioned above) has been updated with freeze nodes:

    Image

    Only 3 values in the Victron system are now controlled:

    • Max Charge Current (number...ess_max_charge_current)
    • Max Inverter Power (number...ess_max_inverter_power_limit)
    • AC Setpoint (number...ac_power_setpoint)

    Here is how each Predbat mode maps to those parameters:

    Predbat Mode / Route AC Setpoint Max Charge Current Max Inverter Power Description / Behavior
    Charging 24000 W thermal_charge_current_a 0 W Grid charging enabled; inverting disabled.
    Hold battery
    50 W thermal_charge_current_a 0 W Solar charging allowed; discharging disabled.
    Exporting -5700 W 0 A victron_discharge_limit Battery discharging to grid; charging blocked.
    Freeze exporting
    50 W 0 A victron_discharge_limit Discharging to cover home loads allowed; charging blocked.
    Demand / safe fallback 50 W thermal_charge_current_a victron_discharge_limit Normal operation (both charge & discharge enabled).

    (Note: thermal_charge_current_a is dynamically calculated based on battery temperature/limits (hot and cold), and victron_discharge_limit reflects my inverter's maximum output limit). These are global variables but can also be set directly

  10. jeangroen commented on Sep 22, 2026

    @jeangroen
    Contributor

    Thanks @WesSec, and agreed: no good or bad, just different situations.

    My setup has only been in production for a short while. I'm still in the phase of monitoring it and adjusting where needed. It's built around my own situation: my contract pays a solar bonus that doesn't apply to energy exported from the battery, so battery export gains me nothing and I don't do it.

    I find it interesting to see how others approach this, and I want to learn from that and test further on my own system. The goal is a design that is as simple and clear as possible, usable for more than one battery strategy, and that relies as much as possible on Predbat's own planning.

    Precisely because these are different use cases, I think it's important that the docs spell out those differences, so that people searching end up faster on the route that fits their situation.

    One thing that already stands out: for freeze exporting we independently arrived at the same thing, max charge current to 0 and everything else open. That seems like the core that belongs in any Victron route.

    A few questions so I understand your route properly:

    1. What is it about your use case that makes you choose this route? For example your contract or tariffs, whether battery export pays off for you, your installation (AC- or DC-coupled PV, battery, location), or simply preference.
    2. Charging doesn't say up to which percentage. Does your Victron keep charging until Predbat changes the status in its next cycle, or do you stop at a target SoC?
    3. Doesn't your battery pass a charge current limit (CCL) through CAN/DVCC? On my system the BMS throttles on temperature itself, so I'm curious why you do that in Node-RED.

    I'm really curious about your answers, as they're important input for what we'd eventually put in the docs. I'll take a few more weeks to investigate before proposing anything there. @gcoan, so my offer for the PR still stands, just a bit later than I wrote.

  11. WesSec commented on Sep 22, 2026

    @WesSec
    Sponsor
    1. What is it about your use case that makes you choose this route? For example your contract or tariffs, whether battery export pays off for you, your installation (AC- or DC-coupled PV, battery, location), or simply preference.
      For context: victron on L1, house is 3 phase and all 3 phase loads are on the AC in side. Most 1 phase groups (except for some technicals/heatpump) are on AC-OUT. PV is AC-AC-OUT coupled. Dynamic tarrif (Tibber)

    It's a personal preference to be able to view a flow of an automation. Although the same behavior can ben created in pure code. I like node red (without function nodes). If an automation breaks, the steps are easy to follow without need for extensive documentation. This also allows for other automation (temp throttling, discussed later), or limiting the Victron charger current when the EV is charging (i charge very cheap so i want EV charging to have a higher priority than my ESS)

    1. Charging doesn't say up to which percentage. Does your Victron keep charging until Predbat changes the status in its next cycle, or do you stop at a target SoC?

    I do not stop at a target SoC, Predbat recalculates often enough (every 10 minutes) to adjust for this in my situation. When prediction is a bit off, this can result in target SoC being reached earlier than expected, possibly for a little higher price. I prefer this over not reaching the target at all because theres no charging headroom due to unplanned demand.

    1. Doesn't your battery pass a charge current limit (CCL) through CAN/DVCC? On my system the BMS throttles on temperature itself, so I'm curious why you do that in Node-RED.>

    Yes, i use JKBMS with DVCC. My battery is in the (ventilated) shed and temps fluctuate a bit (max reported ambient was 36c this summer). My cells specify charge rates at different temps, and JKBMS only supports a hard stop at a certain temp:

    Image

    Using node red, i can modify fan speed, or limit charging/discharing power when temps are out of a comfortable or specced range. In practice i limit charging speed a bit when ambient is above 36, lower thresholds ave not been met yet, i expect them to hit this fall/winter.

    When limiting, it is important to do this on multiple levels:

    • JKBMS > Hard fallback/hard stop when cell temps are out of spec.
    • Victron > Use a more gradual limiting based on various temp sensors and ambient sensor.
    • Predbat > When a form of limiting is applied, i update predbat via the manual_api: ("option":"inverter_limit_charge(0)=" & $string(predbat_charge_limit) and {"option":"inverter_limit_discharge(0)=" & $string(predbat_discharge_limit)} entity to let it know it's current limits, this triggers a recalculation based on those limits, making the plan accurate for the current restrictions. There is debouncing implemented in case the battery hovers around a tigger temp.

    The gradual limiting can also be done via JKBMS directly (i have the entities in Home Assistant), but i prefer not to touch those and have that as a hard fall back in case anything fails.

    To summarize: in order to combine these various systems in automations, node red is the tool for me to design and implement it. But that's very personal and although i like to explain and give context about it. It is not very relevant to the Victron support module in predbat.

    My complete control flow looks like this (temp limit calculation and fan speed for shed is in another flow):
    Image

    I'm really curious about your answers, as they're important input for what we'd eventually put in the docs. I'll take a few more weeks to investigate before proposing anything there. @gcoan, so my offer for the PR still stands, just a bit later than I wrote.

    Hope this helps you, I am happy to explain any choices further if needed. And thanks again for your contributions!

  12. jeangroen commented on Sep 29, 2026

    @jeangroen
    Contributor

    Thanks @WesSec, that answers all three, and it helps me see where our routes really differ.

    What I take from it for the docs:

    • Freeze exporting is the same in both routes: max charge current to 0, everything else open. That looks like the part every Victron route shares.
    • Charging to a target or not is a real design choice. You let Predbat's recalculation stop the charge; the target-SoC route stops at the target itself. Both are valid; the docs should say which one you get.
    • Temperature limiting: your three layers (BMS hard stop, Victron gradual, Predbat informed via inverter_limit_charge) make sense for a BMS that only has a hard stop. My Seplos passes its temperature-dependent charge current limit over CAN/DVCC, so I only give Predbat the temperature curve in apps.yaml and do not throttle myself. Worth a line in the docs: whether you need your own limiting depends on what your BMS sends.

    Two corrections to my long comment above, after checking the code again:

    1. set_export_freeze_only is used in plan.py (it limits the planner to idle/freeze for export windows). What I saw came from the SK profile: with support_discharge_freeze false, freeze is disabled entirely, so the setting has nothing to act on. With the override from section 2 it works as documented.
    2. HA interface not recovering after an HA restart (section 9) is fixed since v9.1.0 (update_time_loop raises AttributeError before it can set fatal_error when ha_interface is None #5135): Predbat now stops itself when the interface is missing, and the add-on restarts it. My hang was on v9.0.x. I keep the watchdog as a safety net, but the separate issue for it is no longer needed.

    I've corrected both in the comment above.

  13. Lanmate commented on Oct 5, 2026

    @Lanmate

    A few questions so I understand your route properly:

    1. What is it about your use case that makes you choose this route? For example your contract or tariffs, whether battery export pays off for you, your installation (AC- or DC-coupled PV, battery, location), or simply preference.
    2. Charging doesn't say up to which percentage. Does your Victron keep charging until Predbat changes the status in its next cycle, or do you stop at a target SoC?
    3. Doesn't your battery pass a charge current limit (CCL) through CAN/DVCC? On my system the BMS throttles on temperature itself, so I'm curious why you do that in Node-RED.

    I'll add my answers here too:

    1. I don't get paid for export at all, unless there is a VPP event from Axle which I've signed up to. I also use a large amount of power (2.76MWh in September) and have a smallish solar array (3.6kW) so there is never any danger of my batteries (~80kWh) being full and therefore exporting solar to the grid. My aim is therefore entirely self use of solar + batteries during peak rate periods (I'm on EDF Go-Electric) and charging up the batteries during the off-peak rate periods.
    2. Yes, this is how I've found it to be working, In the off-peak period for example, when the charge is at the level Predbat desires, it is setting Freeze charging or Hold charging status.
    3. I'm fortunate to live in a part of the UK where the maximum temperature this summer was 25C, and the batteries live in a large garage joining to the house so they neither get too hot nor too cold. The highest cell temperature this summer was 27C and the lowest so far 14C. Temperature hasn't been a consideration thus far but I'll be keeping an eye on the temperature going into winter.
  14. jeangroen commented on Oct 6, 2026

    @jeangroen
    Contributor

    Thanks @Lanmate, that's really helpful, and thanks for confirming the route B summary in #5332.

    Your answers make the difference between the routes quite clear:

    • Your case: no export income, high consumption, a battery that never fills from solar. The goal is purely shifting from off-peak to peak, so acting on Predbat's status is all you need.
    • My case (route A): dynamic tariff with paid export, plus a solar bonus that applies to solar export only, never to battery export. AC-coupled PV regularly fills the battery. So freeze export and "never discharge to the grid" matter, which is why I steer through the target SoC (→ ESS min-SoC) and the charge current.
    • Temperature: same conclusion as WesSec and me. If the BMS passes its CCL through CAN/DVCC, there's no need for extra throttling.
  15. jeangroen commented on Oct 10, 2026

    @jeangroen
    Contributor

    The Victron docs are now in main (#5332, merged by @springfall2008 on 9 October). The Victron section of docs/inverter-setup.md describes three routes: A (target SoC), B (grid setpoint and ESS limits, by @Lanmate) and C (Node-RED following predbat.status, by @WesSec), plus the shared prerequisites and known limitations. The templates/victron.yaml link no longer 404s.

    Thanks @Lanmate and @WesSec for sharing your set-ups and checking the summaries, and @springfall2008 for merging.

    Once #5302 or #5303 is fixed, I'll update the known limitations in the docs.

    @jfreegard1 @gcoan: from my side the documentation part of this issue is done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

VictrondocumentationImprovements or additions to documentationhelp wantedExtra attention is needed

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions