Copy-paste ready · sanitized
The configs
These are the actual files running this system, not examples. The only changes are that credentials and meter identifiers have been swapped for placeholders. If you're putting Predbat on a Sigenergy inverter, this page should save you some evenings. The mode-bridge automations took the most trial and error, and as far as we know they aren't documented anywhere else.
Before you copy: entity IDs assume the HACS Sigenergy ESS integration's
default naming (sigen_plant_*) and a single plant. Check yours in
Developer Tools → States. Nothing here contains secrets. API keys live in
secrets.yaml, which is never published.
apps.yaml
The full Predbat configuration for a Sigenergy EC-series hybrid (inverter_type: SIG) on Octopus Agile + fixed export, with Solcast forecasting and Saving Session support. The regex entity matchers mean it should drop onto most single-inverter Sigenergy installs unchanged, and the rate limits are read from sensors rather than hard-coded, which paid off when our inverter turned out to be a different model than ordered.
pred_bat:
module: predbat
class: PredBat
# Sets the prefix for all created entities in HA - only change if you want to run more than once instance
prefix: predbat
# Timezone to work in
timezone: Europe/London
# XXX: Template configuration, delete this line once you have set up for your system
# template: True
# If you are using Predbat outside of HA then set the HA URL and Key (long lived access token here)
#ha_url: 'http://homeassistant.local:8123'
#ha_key: 'xxx'
# Currency, symbol for main currency second symbol for 1/100s e.g. $ c or £ p or e c
currency_symbols:
- '£'
- 'p'
# Number of threads to use in plan calculation
# Can be auto for automatic, 0 for off or values 1-N for a fixed number
threads: auto
#
# Sensors, currently more than one can be specified and they will be summed up automatically
# however if you have two inverters only set one of them as they will both read the same.
#
inverter_type: "SIG"
num_inverters: 1
#
# Controls/status - must by 1 per inverter
#
load_today:
- sensor.sigen_plant_daily_load_consumption
import_today:
- sensor.sigen_plant_daily_grid_import_energy
export_today:
- sensor.sigen_plant_daily_grid_export_energy
pv_today:
- sensor.sigen_plant_daily_pv_energy
charge_rate:
- input_number.charge_rate
discharge_rate:
- input_number.discharge_rate
battery_power:
- sensor.sigen_plant_battery_power
pv_power:
- sensor.sigen_plant_pv_power
load_power:
- sensor.sigen_plant_consumed_power
grid_power:
- sensor.sigen_plant_grid_active_power
# change the positive/negative sense of power sensors to match what Predbat expects
battery_power_invert: true
grid_power_invert: true
soc_percent:
- sensor.sigen_plant_battery_state_of_charge
soc_kw:
- sensor.sigen_plant_available_max_discharging_capacity
soc_max:
- sensor.sigen_plant_rated_energy_capacity
battery_temperature:
- sensor.sigen_inverter_battery_average_cell_temperature
battery_min_soc:
- 0
reserve:
- number.sigen_plant_ess_discharge_cut_off_state_of_charge
charge_limit:
- number.sigen_plant_ess_charge_cut_off_state_of_charge
# Services to control the inverter
charge_start_service:
- service: input_select.select_option
entity_id: input_select.predbat_requested_mode
option: "Charging"
charge_stop_service:
- service: input_select.select_option
entity_id: input_select.predbat_requested_mode
option: "Demand"
discharge_start_service:
- service: input_select.select_option
entity_id: input_select.predbat_requested_mode
option: "Discharging"
charge_freeze_service:
- service: input_select.select_option
entity_id: input_select.predbat_requested_mode
option: "Freeze Charging"
discharge_freeze_service:
- service: input_select.select_option
entity_id: input_select.predbat_requested_mode
option: "Freeze Discharging"
# Inverter max AC limit (one per inverter)
# If you have a second inverter for PV only please add the two values together
inverter_limit:
- sensor.sigen_plant_available_max_active_power
# Set the maximum charge/discharge rate of the battery
battery_rate_max:
- sensor.sigen_plant_available_max_active_power
# Export limit is a software limit set on your inverter that prevents exporting above a given level
# When enabled Predbat will model this limit
#export_limit:
# - 3600
# - 3600
#
# The maximum rate the inverter can charge and discharge the battery can be overwritten, this will change
# the register programming and thus cap the max rates. The default is to use the maximum supported rates (recommended)
#
#inverter_limit_charge:
# - 4000
#inverter_limit_discharge:
# - 4000
# Some inverters don't turn off when the rate is set to 0, still charge or discharge at around 200w
# The value can be set here in watts to model this (doesn't change operation)
#inverter_battery_rate_min:
# - 100
# Some batteries tail off their charge rate at high soc%
# enter the charging curve here as a % of the max charge rate for each soc percentage.
# the default is 1.0 (full power)
#battery_charge_power_curve:
# 91 : 0.91
# 92 : 0.81
# 93 : 0.71
# 94 : 0.62
# 95 : 0.52
# 96 : 0.43
# 97 : 0.33
# 98 : 0.24
# 99 : 0.24
# 100 : 0.24
# Inverter clock skew in minutes, e.g. 1 means it's 1 minute fast and -1 is 1 minute slow
# Separate start and end options are applied to the start and end time windows, mostly as you want to start late (not early) and finish early (not late)
# Separate discharge skew for discharge windows only
inverter_clock_skew_start: 0
inverter_clock_skew_end: 0
inverter_clock_skew_discharge_start: 0
inverter_clock_skew_discharge_end: 0
# Clock skew adjusts the Appdaemon time
# This is the time that Predbat takes actions like starting discharge/charging
# Only use this for workarounds if your inverter time is correct but Predbat is somehow wrong (AppDaemon issue)
# 1 means add 1 minute to AppDaemon time, -1 takes it away
clock_skew: 0
# Solcast cloud interface, set this or the local interface below
#solcast_host: 'https://api.solcast.com.au/'
#solcast_api_key: 'xxxx'
#solcast_poll_hours: 8
# Set these to match solcast sensor names if not using the cloud interface
# The regular expression (re:) makes the solcast bit optional
# If these don't match find your own names in Home Assistant
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))
# car_charging_energy defines an incrementing sensor which measures the charge added to your car
# is used for car_charging_hold feature to filter out car charging from the previous load data
# Automatically set to detect Wallbox and Zappi, if it doesn't match manually enter your sensor name
# Also adjust car_charging_energy_scale if it's not in kwH to fix the units
# car_charging_energy: 're:(sensor.myenergi_zappi_[0-9a-z]+_charge_added_session|sensor.wallbox_portal_added_energy)'
num_cars: 0
# car_charging_planned is set to a sensor which when positive indicates the car will charged in the upcoming low rate slots
# This should not be needed if you use Octopus Intelligent Slots which will take priority if enabled
# The list of possible values is in car_charging_planned_response
# Auto matches Zappi and Wallbox, or change it for your own
# car_charging_planned:
# - 're:(sensor.wallbox_portal_status_description|sensor.myenergi_zappi_[0-9a-z]+_plug_status)'
# - 'connected'
# car_charging_planned_response:
# - 'yes'
# - 'on'
# - 'true'
# - 'connected'
# - 'ev connected'
# - 'charging'
# - 'paused'
# - 'waiting for car demand'
# - 'waiting for ev'
# - 'scheduled'
# - 'enabled'
# - 'latched'
# - 'locked'
# - 'plugged in'
# To make planned car charging more accurate, either using car_charging_planned or Octopus Intelligent
# specify your battery size in kwh, charge limit % and current car battery soc % sensors/values
# If you have intelligent the battery size and limit will be extracted from Intelligent directly
# Set the car SoC% if you have it to give an accurate forecast of the cars battery levels
# One entry per car if you have multiple cars
# car_charging_battery_size:
# - 75
# car_charging_limit:
# - 're:number.tsunami_charge_limit'
# car_charging_soc:
# - 're:sensor.tsunami_battery'
# If you have Octopus Intelligent Go and are not using the Octopus Direct connection method, enable the intelligent slot information to add to pricing
# Will automatically disable if not found, or comment out to disable fully
# When enabled it overrides the 'car_charging_planned' feature and predict the car charging based on the intelligent plan (unless Octopus intelligent charging is False)
# This matches the intelligent slot from the Octopus Energy integration
octopus_intelligent_slot: 're:(binary_sensor.octopus_energy([0-9a-z_]+|)_intelligent_dispatching)'
octopus_ready_time: 're:((select|time).octopus_energy_([0-9a-z_]+|)_intelligent_target_time)'
octopus_charge_limit: 're:(number.octopus_energy([0-9a-z_]+|)_intelligent_charge_target)'
# Carbon Intensity data from National grid
# carbon_postcode: 'SW1 5NA'
# carbon_automatic: True
# Example alternative configuration for Ohme integration release >=v0.6.1
#octopus_intelligent_slot: 'binary_sensor.ohme_slot_active'
#octopus_ready_time: 'time.ohme_target_time'
#octopus_charge_limit: 'number.ohme_target_percent'
# Set this to False if you use Octopus Intelligent slot for car planning but when on another tariff e.g. Agile
#octopus_slot_low_rate: False
# Octopus saving session points to the saving session Sensor in the Octopus plugin, when enabled saving sessions will be at the assumed
# Rate is read automatically from the add-in and converted to pence using the conversion rate below (default is 8)
octopus_saving_session: 're:(event.octopus_energy([0-9a-z_]+|)_saving_session_event(s|))'
octopus_saving_session_octopoints_per_penny: 8
# Enter your Axle VPP API key if you have signed up to the Axle service in the UK
# axle_api_key: "xxxxxxx"
# Energy rates
# Please set one of these three, if multiple are set then Octopus is used first, second rates_import/rates_export and latest basic metric
# Set import and export entity to point to the Octopus Energy plugin
# automatically matches your meter number assuming you have only one
# Will be ignored if you don't have the sensor
# Or manually set it to the correct sensor names e.g:
# sensor.octopus_energy_electricity_xxxxxxxxxx_xxxxxxxxxxxxx_current_rate
# sensor.octopus_energy_electricity_xxxxxxxxxx_xxxxxxxxxxxxx_export_current_rate
metric_octopus_import: 're:(sensor.(octopus_energy_|)electricity_[0-9a-z]+_[0-9a-z]+_current_rate)'
metric_octopus_export: 're:(sensor.(octopus_energy_|)electricity_[0-9a-z]+_[0-9a-z]+_export_current_rate)'
# Standing charge can be set to a sensor (e.g. Octopus) or manually entered in pounds here (e.g. 0.50 is 50p)
metric_standing_charge: 're:(sensor.(octopus_energy_|)electricity_[0-9a-z]+_[0-9a-z]+_current_standing_charge)'
# Or set your actual rates across time for import and export
# If start/end is missing it's assumed to be a fixed rate
# Gaps are filled with 0
# rates_import:
# - start: "23:30:00"
# end: "05:30:00"
# rate: 7.5
# - start: "05:30:00"
# end: "23:30:00"
# rate: 30.0
# rates_export:
# - rate: 15.0
# Can be used instead of the plugin to get import rates directly online
# Overrides metric_octopus_import and rates_import
# See the 'energy rates' part of the documentation for instructions on how to find the correct URL for your tariff and DNO region
#
# rates_import_octopus_url : "https://api.octopus.energy/v1/products/FLUX-IMPORT-23-02-14/electricity-tariffs/E-1R-FLUX-IMPORT-23-02-14-A/standard-unit-rates"
# rates_import_octopus_url : "https://api.octopus.energy/v1/products/AGILE-24-10-01/electricity-tariffs/E-1R-AGILE-24-10-01-A/standard-unit-rates"
# Overrides metric_octopus_export and rates_export
# rates_export_octopus_url: "https://api.octopus.energy/v1/products/FLUX-EXPORT-23-02-14/electricity-tariffs/E-1R-FLUX-EXPORT-23-02-14-A/standard-unit-rates"
# rates_export_octopus_url: "https://api.octopus.energy/v1/products/AGILE-OUTGOING-19-05-13/electricity-tariffs/E-1R-AGILE-OUTGOING-19-05-13-A/standard-unit-rates/"
# Import rates can be overridden with rate_import_override
# Export rates can be overridden with rate_export_override
# Use the same format as above, but a date can be included if it just applies for a set day (e.g. Octopus power ups)
# This will override even the Octopus plugin rates if enabled
#
#rates_import_override:
# - date: '2023-09-10'
# start: '14:00:00'
# end: '14:30:00'
# rate: 5
# Days previous is the number of days back to find historical load data
# Recommended is 7 to capture day of the week but 1 can also be used
# if you have more history you could use 7 and 14 (in a list) but the standard data in HA only lasts 10 days
# NOTE (2026-06-04): set to 1 because the Sigenergy integration only started recording today.
# Change to 7 once a full week of load history exists (around 2026-06-11).
days_previous:
- 1
# Days previous weight can be used to control the weighting of the previous load points, the values are multiplied by their
# weights and then divided through by the total weight. E.g. if you used 1 and 0.5 then the first value would have 2/3rd of the weight and the second 1/3rd
days_previous_weight:
- 1
# Number of hours forward to forecast, best left as-is unless you have specific reason
forecast_hours: 30
# The number of hours ahead to count in charge planning (for cost estimates)
# It's best to set this on your charge window repeat cycle (24) but you may want to set it higher for more variable
# tariffs like Agile
forecast_plan_hours: 30
# Specify the devices that notifies are sent to, the default is 'notify' which goes to all
#notify_devices:
# - mobile_app_treforsiphone12_2
# Battery scaling makes the battery smaller (e.g. 0.9) or bigger than its reported
# If you have an 80% DoD battery that falsely reports it's kwh then set it to 0.8 to report the real figures
battery_scaling: 1.0
# Can be used to scale import and export data, used for workarounds
import_export_scaling: 1.0
# Export triggers:
# For each trigger give a name, the minutes of export needed and the energy required in that time
# Multiple triggers can be set at once so in total you could use too much energy if all run
# Creates an entity called 'binary_sensor.predbat_export_trigger_<name>' which will be turned On when the condition is valid
# connect this to your automation to start whatever you want to trigger
export_triggers:
- name: 'large'
minutes: 60
energy: 1.0
- name: 'small'
minutes: 15
energy: 0.25
# Nordpool market energy rates
#futurerate_url: 'https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices?date=DATE&market=N2EX_DayAhead&deliveryArea=UK¤cy=GBP'
#futurerate_adjust_import: False
#futurerate_adjust_export: False
#futurerate_peak_start: "16:00:00"
#futurerate_peak_end: "19:00:00"
#futurerate_peak_premium_import: 14
#futurerate_peak_premium_export: 6.5
# If you have a sensor that gives the energy consumed by your solar diverter then add it here
# this will make the predictions more accurate. It should be an incrementing sensor, it can reset at midnight or not
# It's assumed to be in Kwh but scaling can be applied if need be
#iboost_energy_today: 'sensor.xxxxx'
#iboost_energy_scaling: 1.0 predbat_sigenergy_bridge.yaml
The three automations that bridge Predbat to the Sigenergy EMS. Predbat sets input_select.predbat_requested_mode; the first automation maps each mode (Demand / Charging / Discharging / both Freezes) onto sigen_plant_remote_ems_control_mode and the charge/discharge cut-off registers. The other two clamp Predbat's requested W rates onto the inverter's kW limit registers.
- id: predbat_requested_mode_action
alias: "Predbat Requested Mode Action"
description: "Maps input_select.predbat_requested_mode to select.sigen_plant_remote_ems_control_mode (Predbat <-> Sigenergy bridge)"
mode: restart
triggers:
- trigger: state
entity_id:
- input_select.predbat_requested_mode
conditions: []
actions:
- action: select.select_option
metadata: {}
target:
entity_id: select.sigen_plant_remote_ems_control_mode
data:
option: >
{% if is_state('input_select.predbat_requested_mode', "Demand") %}Maximum Self Consumption
{% elif is_state('input_select.predbat_requested_mode', "Charging") %}Command Charging (PV First)
{% elif is_state('input_select.predbat_requested_mode', "Freeze Charging") %}Maximum Self Consumption
{% elif is_state('input_select.predbat_requested_mode', "Discharging") %}Command Discharging (PV First)
{% elif is_state('input_select.predbat_requested_mode', "Freeze Discharging") %}Maximum Self Consumption
{% endif %}
- choose:
# Freeze Charging: hold current SoC - self consumption with discharging prohibited
- conditions:
- condition: state
entity_id: input_select.predbat_requested_mode
state: "Freeze Charging"
sequence:
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_charge_cut_off_state_of_charge
data:
value: 100
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge
data:
value: 100
- action: number.set_value
target:
entity_id: number.sigen_plant_grid_import_limitation
data:
value: 0
# Freeze Discharging: hold current SoC - self consumption with charging prohibited
- conditions:
- condition: state
entity_id: input_select.predbat_requested_mode
state: "Freeze Discharging"
sequence:
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_charge_cut_off_state_of_charge
data:
value: 0
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge
data:
value: 0
- action: number.set_value
target:
entity_id: number.sigen_plant_grid_import_limitation
data:
value: 0
# Default: restore normal cut-off limits
- conditions:
- condition: not
conditions:
- condition: state
entity_id: input_select.predbat_requested_mode
state: "Freeze Charging"
- condition: state
entity_id: input_select.predbat_requested_mode
state: "Freeze Discharging"
sequence:
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_charge_cut_off_state_of_charge
data:
value: 100
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_discharge_cut_off_state_of_charge
data:
value: 0
- action: number.set_value
target:
entity_id: number.sigen_plant_grid_import_limitation
data:
value: 100
- id: automation_sigen_ess_max_charging_limit_input_number_action
alias: "Predbat max charging limit action"
description: "Maps input_number.charge_rate (W) to number.sigen_plant_ess_max_charging_limit (kW), clamped to rated power"
mode: single
triggers:
- trigger: state
entity_id: input_number.charge_rate
actions:
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_max_charging_limit
data:
value: "{{ [(states('input_number.charge_rate') | float / 1000) | round(2), states('sensor.sigen_plant_ess_rated_charging_power') | float] | min }}"
- id: automation_sigen_ess_max_discharging_limit_input_number_action
alias: "Predbat max discharging limit action"
description: "Maps input_number.discharge_rate (W) to number.sigen_plant_ess_max_discharging_limit (kW), clamped to rated power"
mode: single
triggers:
- trigger: state
entity_id: input_number.discharge_rate
actions:
- action: number.set_value
target:
entity_id: number.sigen_plant_ess_max_discharging_limit
data:
value: "{{ [(states('input_number.discharge_rate') | float / 1000) | round(2), states('sensor.sigen_plant_ess_rated_discharging_power') | float] | min }}" helpers.yaml
The input_select and input_number helpers the bridge automations depend on. Append to configuration.yaml (or your packages).
# ── Predbat ↔ Sigenergy helpers (added 2026-06-04) ──────────────────
input_select:
predbat_requested_mode:
name: "Predbat Requested Mode"
options:
- "Demand"
- "Charging"
- "Freeze Charging"
- "Discharging"
- "Freeze Discharging"
initial: "Demand"
icon: mdi:battery-unknown
input_number:
charge_rate:
name: Battery charge rate
initial: 7600
min: 0
max: 20000
step: 1
mode: box
unit_of_measurement: W
discharge_rate:
name: Battery discharge rate
initial: 7600
min: 0
max: 20000
step: 1
mode: box
unit_of_measurement: W enable_entities.jq
The Sigenergy integration ships several control entities disabled by default, including the remote EMS mode select that the whole bridge depends on. This jq filter flips them on in the entity registry (run it against core.entity_registry with HA stopped, or just enable them by hand in the UI).
(.data.entities[]
| select(.entity_id | IN(
"sensor.sigen_plant_available_max_discharging_capacity",
"sensor.sigen_plant_available_max_active_power",
"number.sigen_plant_ess_backup_state_of_charge",
"number.sigen_plant_ess_charge_cut_off_state_of_charge",
"number.sigen_plant_ess_discharge_cut_off_state_of_charge",
"number.sigen_plant_ess_max_charging_limit",
"number.sigen_plant_ess_max_discharging_limit",
"select.sigen_plant_remote_ems_control_mode",
"number.sigen_plant_grid_import_limitation",
"switch.sigen_plant_remote_ems_controlled_by_home_assistant"))
| .disabled_by) = null energy_prefs.json
Energy dashboard source config: grid import/export and battery from the Sigenergy cumulative meters, live import pricing from the Octopus integration, solar forecast from Solcast. Replace the MPAN/serial and Solcast config-entry placeholders with your own.
{
"version": 1,
"minor_version": 3,
"key": "energy",
"data": {
"energy_sources": [
{
"type": "grid",
"stat_energy_from": "sensor.sigen_plant_total_imported_energy",
"stat_energy_to": "sensor.sigen_plant_total_exported_energy",
"stat_cost": null,
"stat_compensation": null,
"entity_energy_price": "sensor.octopus_energy_electricity_YOUR_MPAN_YOUR_SERIAL_current_rate",
"number_energy_price": null,
"entity_energy_price_export": null,
"number_energy_price_export": null,
"cost_adjustment_day": 0.0
},
{
"type": "solar",
"stat_energy_from": "sensor.sigen_plant_total_pv_generation",
"config_entry_solar_forecast": ["YOUR_SOLCAST_CONFIG_ENTRY_ID"]
},
{
"type": "battery",
"stat_energy_from": "sensor.sigen_plant_total_discharged_energy_of_the_ess",
"stat_energy_to": "sensor.sigen_plant_total_charged_energy_of_the_ess"
}
],
"device_consumption": [],
"device_consumption_water": []
}
} The stats-publishing pipeline
The utility_meter helpers and the 00:15 GitHub push that feed the stats page are running on the live system now. They'll be published here once they've survived a few real midnights. The design is described on the stack page.