solar by numbers

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.

apps.yaml ↓ raw
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&currency=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.

predbat_sigenergy_bridge.yaml ↓ raw
- 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).

helpers.yaml ↓ raw

# ── 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).

enable_entities.jq ↓ raw
(.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.

energy_prefs.json ↓ raw
{
  "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.