Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4870833
Minor documentation tweaks
gcoan May 16, 2026
edf4226
Document PVAccuracy chart and Metrics view
gcoan May 16, 2026
7fd69e3
Merge branch 'springfall2008:main' into main
gcoan May 18, 2026
7acc09e
#3934 iboost daily reset
gcoan May 18, 2026
da5635f
Added soc_max_calculated to output_data
gcoan May 18, 2026
ed0ee23
Add error checking to Octopus saving session join, only send joined n…
gcoan May 18, 2026
e74cc2b
#3801 use module name in predbat notifications instead of hard coded …
gcoan May 18, 2026
875cb51
capitalise predbat prefix in alert notifications
gcoan May 21, 2026
49fe906
Updated plan doc to include Axle in $ prefix
gcoan May 21, 2026
5ebaf3d
fix sigenergy, luxpower and kostal inverter setup formatting issues
gcoan May 21, 2026
ec7bb56
Potential fix for pull request finding
springfall2008 Jun 6, 2026
b4ecfaf
Merge branch 'main' into main
gcoan Aug 12, 2026
fbd5a3c
Apply claude review comments
gcoan Aug 12, 2026
600202f
change dummy service call to return success result
gcoan Aug 12, 2026
469bc21
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Aug 12, 2026
7d9f4e7
correct service join call to match octopus.py (add return_response)
gcoan Aug 12, 2026
54325a8
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Aug 12, 2026
06e611e
Merge branch 'springfall2008:main' into main
gcoan Aug 13, 2026
89eda6f
Merge branch 'springfall2008:main' into main
gcoan Aug 14, 2026
360d96c
#4522 Add minimum memory guidance
gcoan Aug 14, 2026
4da7b26
#4523 #4496 load energy entities documentation update
gcoan Aug 14, 2026
54447ff
revert checking octopus saving session join response as service curre…
gcoan Aug 15, 2026
2b30cc9
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Aug 15, 2026
2911688
correct prior fix
gcoan Aug 15, 2026
5917e4c
fix saving session test to match current code (no return response)
gcoan Aug 15, 2026
652407d
Merge branch 'springfall2008:main' into main
gcoan Aug 16, 2026
19a08c9
Merge branch 'springfall2008:main' into main
gcoan Aug 19, 2026
59504f8
#4607 change 'addon_configs' to 'app_configs'
gcoan Aug 20, 2026
b86ae98
Fix missing space warning in agents.md table documentation
gcoan Aug 20, 2026
9c77ef9
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Aug 20, 2026
e4cc70d
fix /app_configs pathname typos
gcoan Aug 20, 2026
7f71662
#4717 clarify that car_charging_threshold is ignored if car_charging_…
gcoan Aug 24, 2026
4e85815
fix merge conflict issues
gcoan Aug 24, 2026
ea97b98
fix deploy merge conflict
gcoan Aug 24, 2026
16b1f40
future changes to coverage/deploy for HA addon/apps renaming
gcoan Aug 24, 2026
0fc450b
revert deploy changes
gcoan Aug 24, 2026
7cc0491
octopus.py merge conflict
gcoan Aug 24, 2026
fc00309
Merge branch 'main' into main
gcoan Aug 24, 2026
9cfe840
Merge branch 'springfall2008:main' into main
gcoan Aug 27, 2026
a4689f8
#4766 Add warning about impact of setting best_soc_max
gcoan Aug 27, 2026
4f5ccc5
#4797 correct grid power positive/negative sense in predbat metrics
gcoan Aug 29, 2026
077980d
Merge branch 'main' into main
gcoan Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ If you want to buy me a beer then please use [Paypal](https://paypal.me/predbat?
* Use my referral code for Axle Energy (UK): <https://vpp.axle.energy/landing/grid?ref=R-VWIICRSA>

If you are finding Home Assistant and Predbat too difficult to set up yourself there is now [PredBat Cloud](https://predbat.com/) which is a paid for version of Predbat hosted in the cloud.
Please note that while I have given permission for PredBat Cloud to operate this is not my company, PredBat will remain open source for everyone personal use.
Please note that while I have given permission for PredBat Cloud to operate, this is not my company and PredBat will remain open source for everyone's personal use.

## Predbat documentation

Expand Down
3 changes: 2 additions & 1 deletion apps/predbat/axle.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ async def _fetch_byok_event(self):
if self.get_arg("set_event_notify"):
local_start = start_time.astimezone(self.local_tz)
local_end = end_time.astimezone(self.local_tz)
self.call_notify("Predbat: Scheduled Axle VPP event {}-{}, {} p/kWh".format(local_start.strftime("%a %d/%m %H:%M"), local_end.strftime("%H:%M"), self.pence_per_kwh))
msg = "Scheduled Axle VPP event " + local_start.strftime("%a %d/%m %H:%M") + "-" + local_end.strftime("%H:%M") + self.pence_per_kwh + " p/kWh"
self.call_notify(f"{self.prefix.capitalize()}: {msg}")
Comment thread
springfall2008 marked this conversation as resolved.
Outdated

self.cleanup_event_history()
self.publish_axle_event()
Expand Down
22 changes: 11 additions & 11 deletions apps/predbat/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def auto_restart(self, reason):
self.log("Warn: Calling restart service {}".format(service))
self.base.call_service_wrapper(service)
if self.base.get_arg("set_system_notify"):
self.base.call_notify("Auto-restart service {} called due to: {}".format(service, reason))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Auto-restart service {service} called due to: {reason}")
self.sleep(15)
raise Exception("Auto-restart triggered")
else:
Expand Down Expand Up @@ -1620,7 +1620,7 @@ def adjust_reserve(self, reserve):
else:
self.write_and_poll_value("reserve", self.base.get_arg("reserve", indirect=False, index=self.id, required_unit="%"), reserve)
if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Target Reserve has been changed to {}% at {}".format(self.id, dp0(reserve), self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Target Reserve has been changed to {dp0(reserve)}% at {self.base.time_now_str()}")
self.mqtt_message(topic="set/reserve", payload=reserve)
else:
self.base.log("Inverter {} Current reserve is {}%, already at target".format(self.id, dp0(current_reserve)))
Expand Down Expand Up @@ -1702,7 +1702,7 @@ def adjust_charge_rate(self, new_rate, notify=True):
self.set_current_from_power("charge", new_rate)

if notify and self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} charge rate changes to {}W at {}".format(self.id, new_rate, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} charge rate changes to {new_rate}W at {self.base.time_now_str()}")
self.mqtt_message(topic="set/charge_rate", payload=new_rate)

def adjust_discharge_rate(self, new_rate, notify=True):
Expand Down Expand Up @@ -1740,7 +1740,7 @@ def adjust_discharge_rate(self, new_rate, notify=True):
self.set_current_from_power("discharge", new_rate)

if notify and self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} discharge rate changes to {}W at {}".format(self.id, new_rate, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} discharge rate changes to {new_rate}W at {self.base.time_now_str()}")
self.mqtt_message(topic="set/discharge_rate", payload=new_rate)

def adjust_battery_target(self, soc, isCharging=False, isExporting=False):
Expand Down Expand Up @@ -1786,7 +1786,7 @@ def adjust_battery_target(self, soc, isCharging=False, isExporting=False):
self.press_and_poll_button()

if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Target SoC has been changed to {}% at {}".format(self.id, soc, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Target SoC has been changed to {soc}% at {self.base.time_now_str()}")
self.mqtt_message(topic="set/target_soc", payload=soc)
else:
self.base.log("Inverter {} Current Target SoC is {}%, already at target".format(self.id, current_soc))
Expand Down Expand Up @@ -2037,7 +2037,7 @@ def adjust_pause_mode(self, pause_charge=False, pause_discharge=False):
self.write_and_poll_option("pause_mode", entity_mode, new_pause_mode)

if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} pause mode to set {} at time {}".format(self.id, new_pause_mode, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} pause mode to set {new_pause_mode} at time {self.base.time_now_str()}")

self.base.log("Inverter {} set pause mode to {}".format(self.id, new_pause_mode))

Expand Down Expand Up @@ -2110,7 +2110,7 @@ def adjust_inverter_mode(self, force_export, changed_start_end=False):

# Notify
if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Force export set to {} at time {}".format(self.id, force_export, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Force export set to {force_export} at time {self.base.time_now_str()}")

self.base.log("Inverter {} set force export to {}".format(self.id, force_export))

Expand Down Expand Up @@ -2396,7 +2396,7 @@ def adjust_force_export(self, force_export, new_start_time=None, new_end_time=No
# Notify
if changed_start_end:
if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Export time slot set to {} - {} at time {}".format(self.id, new_start, new_end, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Export time slot set to {new_start} - {new_end} at time {self.base.time_now_str()}")

def disable_charge_window(self, notify=True):
"""
Expand Down Expand Up @@ -2444,7 +2444,7 @@ def disable_charge_window(self, notify=True):
self.enable_charge_discharge_with_time_current("charge", False)

if self.base.set_inverter_notify and notify:
self.base.call_notify("Predbat: Inverter {} Disabled scheduled charging at {}".format(self.id, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Disabled scheduled charging at {self.base.time_now_str()}")

self.base.log("Inverter {} Turning off scheduled charge".format(self.id))

Expand Down Expand Up @@ -2807,7 +2807,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now):
self.rest_setChargeSlot1(new_start, new_end)

if self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Charge window change to: {} - {} at {}".format(self.id, new_start, new_end, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Charge window change to: {new_start} - {new_end} at {self.base.time_now_str()}")
self.base.log("Inverter {} Updated start and end charge window to {} - {} (old {} - {})".format(self.id, new_start, new_end, old_start, old_end))

if (old_charge_schedule_enable == "off" or have_disabled) and (new_start != new_end):
Expand All @@ -2824,7 +2824,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now):

# Only notify if it's a real change and not a temporary one
if old_charge_schedule_enable == "off" and self.base.set_inverter_notify:
self.base.call_notify("Predbat: Inverter {} Enabling scheduled charging at {}".format(self.id, self.base.time_now_str()))
self.base.call_notify(f"{self.base.prefix.capitalize()}: Inverter {self.id} Enabling scheduled charging at {self.base.time_now_str()}")

self.charge_enable_time = True

Expand Down
18 changes: 12 additions & 6 deletions apps/predbat/octopus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2781,7 +2781,7 @@ def fetch_octopus_sessions(self):
octopus_saving_slots = []
if "octopus_saving_session" in self.args:
saving_rate = 200 # Default rate if not reported
octopoints_per_penny = self.get_arg("octopus_saving_session_octopoints_per_penny", 8) # Default 8 octopoints per found
octopoints_per_penny = self.get_arg("octopus_saving_session_octopoints_per_penny", 8) # Default 8 octopoints per penny

joined_events = []
available_events = []
Expand Down Expand Up @@ -2811,17 +2811,23 @@ def fetch_octopus_sessions(self):
saving_rate = octopoints_kwh / octopoints_per_penny # Octopoints per pence
else:
saving_rate = saving_rate # Use default if not specified
if code: # Join the new Octopus saving event and send an alert
if code: # Join the new Octopus saving event and send an alert if successfully joined
self.log("Octopus: Joining Octopus saving event code {} {}-{} at rate {} p/kWh".format(code, start_time.strftime("%a %d/%m %H:%M"), end_time.strftime("%H:%M"), saving_rate))
entity_id_join = self.get_arg("octopus_saving_session_join", indirect=False)
if entity_id_join:
# Join via selector
self.call_service_wrapper("select/select_option", entity_id=entity_id_join, option=code)
cmd = "select/select_option, entity_id={}, option={}".format(entity_id_join, code)
result = self.call_service_wrapper("select/select_option", entity_id=entity_id_join, option=code)
else:
# Join via octopus event (Bottle Cap Dave)
self.call_service_wrapper("octopus_energy/join_octoplus_saving_session_event", event_code=code, entity_id=entity_id)
if self.get_arg("set_event_notify"):
self.call_notify("Predbat: Joined Octopus saving event {}-{}, {} p/kWh".format(start_time.strftime("%a %d/%m %H:%M"), end_time.strftime("%H:%M"), saving_rate))
cmd = "octopus_energy/join_octoplus_saving_session_event, event_code={}, entity_id={}".format(code, entity_id)
result = self.call_service_wrapper("octopus_energy/join_octoplus_saving_session_event", event_code=code, entity_id=entity_id)
if result:
if self.get_arg("set_event_notify"):
msg = "Joined Octopus saving event " + start_time.strftime("%a %d/%m %H:%M") + "-" + end_time.strftime("%H:%M") + ", " + saving_rate + " p/kWh"
self.call_notify(f"{self.prefix.capitalize()}: {msg}")
Comment thread
gcoan marked this conversation as resolved.
Outdated
else:
self.log("Warn: Unable to join Octoplus saving event with command {}, result was {}".format(cmd, result))
Comment thread
gcoan marked this conversation as resolved.
Outdated
self.octopus_last_joined_try = self.now_utc

# Default saving session rate for when octopoints_per_kwh is not available
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2412,7 +2412,7 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra
# Already in error state, do not notify second error in a single run (spam)
pass
else:
self.call_notify("Predbat status change to: " + message + extra)
self.call_notify(f"{self.prefix.capitalize()} status change to: {message} {extra}")
self.previous_status = message
Comment thread
gcoan marked this conversation as resolved.

error_count = self.get_state_wrapper(self.prefix + ".status", attribute="error_count", default=0)
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ def download_predbat_version(self, version):
if files:
# Notify before killing threads so the WebSocket is still healthy
if self.get_arg("set_system_notify"):
self.call_notify("Predbat: update to: {}".format(version))
self.call_notify(f"{self.prefix.capitalize()}: update to: {version}")

# Kill the current threads
self.log("Kill current threads before update")
Expand Down
6 changes: 3 additions & 3 deletions apps/predbat/userinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ async def async_restore_settings_yaml(self, filename):
self.log("Restore setting: {} = {} (was {})".format(item["name"], item["default"], item["value"]))
await self.async_expose_config(item["name"], item["default"], event=True)
if self.get_arg("set_system_notify"):
await self.async_call_notify("Predbat settings restored from default")
await self.async_call_notify(f"{self.prefix.capitalize()} settings restored from default")
else:
filepath = os.path.join(self.save_restore_dir, filename)
if os.path.exists(filepath):
Expand All @@ -620,7 +620,7 @@ async def async_restore_settings_yaml(self, filename):
self.log("Restore setting: {} = {} (was {})".format(item["name"], item["value"], current["value"]))
await self.async_expose_config(item["name"], item["value"], event=True)
if self.get_arg("set_system_notify"):
await self.async_call_notify("Predbat settings restored from {}".format(filename))
await self.async_call_notify(f"{self.prefix.capitalize()} settings restored from {filename}")
await self.async_expose_config("saverestore", None)

def load_current_config(self):
Expand Down Expand Up @@ -692,7 +692,7 @@ async def async_save_settings_yaml(self, filename=None):
yaml.dump(self.CONFIG_ITEMS, file)
self.log("Saved Predbat settings to {}".format(filepath_p))
if self.get_arg("set_system_notify"):
await self.async_call_notify("Predbat settings saved to {}".format(filename))
await self.async_call_notify(f"{self.prefix.capitalize()} settings saved to {filename}")

def read_debug_yaml(self, filename):
"""
Expand Down
23 changes: 16 additions & 7 deletions docs/apps-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,8 @@ To disable, set it to 1440.
- **iboost_energy_today** - Set to a sensor which tracks the amount of energy sent to your solar diverter, which can also be used to subtract from your historical load
for more accurate predictions.

The iboost energy sensor should reset to zero each day so if your source sensor doesn't, then its recommended to wrap it in a utility meter and configure Predbat to use the utility meter.

## Inverter control configurations

### **inverter_limit**
Expand Down Expand Up @@ -1332,9 +1334,16 @@ Called when a charge/discharge is cancelled and the inverter goes back to home d
topic: **topic**/set/auto
payload: true

## Solcast Solar Forecast
## Solar Forecast

As described in the [Predbat installation instructions](install.md#solar-forecast-install), Predbat needs a solar forecast
in order to predict solar generation and battery charging.

The Solar forecast configuration in `apps.yaml` should be configured for either for the [Solcast integration](#solcast-solar-forecast), [Forecast.solar](#forecastsolar-solar-forecast) or [Open-Meteo](#open-meteo-solar-forecast).

### Solcast Solar Forecast

As described in the [Predbat installation instructions](install.md#solcast-install), Predbat needs a solar forecast
As described in the [Predbat installation instructions](install.md#solar-forecast-install, Predbat needs a solar forecast
in order to predict solar generation and battery charging which can be provided by the Solcast integration.
Comment thread
gcoan marked this conversation as resolved.

By default, the template `apps.yaml` is pre-configured to use the [Solcast forecast integration](install.md#solcast-home-assistant-integration-method) for Home Assistant.
Expand Down Expand Up @@ -1408,7 +1417,7 @@ This can have an impact on planning, especially for things like freeze charging

See also [PV configuration options in Home Assistant](customisation.md#solar-pv-adjustment-options).

## Forecast.solar Solar Forecast
### Forecast.solar Solar Forecast

The Forecast.solar service can also be used in Predbat, the free version offer access without an API Key but is limited to hourly data and does not provide any 10% or 90% data.
Predbat Solar calibration can use past data to improve this information and provide the 10% data.
Expand Down Expand Up @@ -1450,11 +1459,11 @@ Optionally you can set an api_key for personal or professional accounts and you

Note you can omit any of these settings for a default value. They do not have to be exact if you use Predbat auto calibration for PV to improve the data quality.

## Open-Meteo Solar Forecast
### Open-Meteo Solar Forecast

[Open-Meteo](https://open-meteo.com/) is a free, open-source weather API that provides solar irradiance forecasts with no API key required.
Predbat fetches the Global Tilted Irradiance (GTI) for each array and converts it to a power estimate using a PVWatts cell-temperature model.
Ensemble members are used to derive a P10 pessimistic estimate alongside the central P50.
Ensemble members are used to derive a PV10 pessimistic estimate alongside the central PV50.

You can define one or more rooftop arrays by providing a list; they will be summed automatically.

Expand Down Expand Up @@ -1524,7 +1533,7 @@ These are described in detail in [Energy Rates](energy-rates.md) and are listed
- **rates_export_override** - Over-ride export rate for specific date and time range
- **futurerate_url** - URL of future energy market prices for Agile users
- **futurerate_adjust_import** and **futurerate_adjust_export** - Whether tomorrow's predicted import or export prices should be adjusted based on market prices or not
- **futurerate_adjust_auto** - Auto-detect which of import/export are Agile and calibrate only those rates; overrides `futurerate_adjust_import` / `futurerate_adjust_export`; requires the Octopus Energy integration or Predbat's Octopus Component
- **futurerate_adjust_auto** - Auto-detect which of the import/export rates are Agile and calibrate only those rates; overrides `futurerate_adjust_import` / `futurerate_adjust_export`; requires the Octopus Energy integration or Predbat's Octopus Component
- **futurerate_peak_start** and **futurerate_peak_end** - start/end times for peak-rate adjustment
- **carbon_postcode** - Postcode to retrieve Carbon intensity grid information for
- **carbon_automatic** - Retrieve Carbon intensity information automatically based upon postcode
Expand Down Expand Up @@ -1739,7 +1748,7 @@ rather than the usual *givtcp_SERIAL_NUMBER_soc* GivTCP entity so everything lin
Default false. When set to true Predbat will automatically calculate `battery_scaling` based on historical charge data rather than using the static value above.

The calculation uses `find_battery_size()` to estimate the actual usable battery capacity from historical charging periods and
compares it to the nominal capacity (`soc_max`). A 7-day rolling history of daily estimates is stored in a new sensor
compares it to the nominal capacity (`soc_max`). A 7-day rolling history of daily estimates is stored in the sensor
`sensor.predbat_soc_max_calculated` (or `sensor.predbat_soc_max_calculated_N` for inverter N > 0).
The sensor state is the trimmed mean of the history (the highest and lowest samples are discarded when 3 or more data points exist,
giving a stable average that is robust to occasional outliers).
Expand Down
Loading
Loading