Skip to content
Open
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 on lines +371 to +372

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 on lines +2826 to +2828
else:
self.log("Warn: Unable to join Octoplus saving event with command {}, result was {}".format(cmd, result))
Comment on lines +2819 to +2830
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 on lines +2415 to 2416

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 on lines +1346 to 1347

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