Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
318e23a
Correct web console views list
gcoan Jan 7, 2026
a70f1f2
#2049 Clarify that the error monitors are for HA addons, not docker
gcoan Jan 8, 2026
776aeeb
Merge branch 'springfall2008:main' into main
gcoan Jan 10, 2026
0f11102
#3202 Battery charging documentation
gcoan Jan 10, 2026
854d757
Expand weather alert documentation
gcoan Jan 10, 2026
5b74eb7
Weather alert logging tweaks
gcoan Jan 10, 2026
ad54106
#3210 correct predheat chart link
gcoan Jan 11, 2026
6f3cd5c
Battery curve message tweak
gcoan Jan 12, 2026
481f324
#3233 correct Klostal automation charge rate typo
gcoan Jan 14, 2026
26df2ae
#3239 template sensor to convert car charging times to full format
gcoan Jan 14, 2026
cadb134
remove trailing dp from OIG SoC and cost
gcoan Jan 16, 2026
39406ed
#2894 Predbat manual api doc updates
gcoan Jan 18, 2026
049b3ef
#3263 move PV calibration switch description
gcoan Jan 20, 2026
ba8fe11
#3265 Corrected entity friendly names for predbat.pv/grid/load/batter…
gcoan Jan 20, 2026
b12166c
#3264 remove run_every from templates, add to predheat docs
gcoan Jan 22, 2026
d1791cd
Add instructions for predbat manual release install
gcoan Jan 22, 2026
8e52e9e
Add predbat-release image
gcoan Jan 22, 2026
11725f1
#3275 clarify predbat mode must be set correctly to use select.predba…
gcoan Jan 22, 2026
b34d3db
Correct inverter writes markdown to work over year end rollover
gcoan Jan 25, 2026
b880173
Merge branch 'main' into main
gcoan Jan 25, 2026
f3edc72
Fix expected loaded slots in test case 8
gcoan Jan 25, 2026
b423ac5
Fix charge_curve dp1 error
gcoan Jan 25, 2026
4b34a45
#3285 Solax min soc in backup mode
gcoan Jan 25, 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
1 change: 1 addition & 0 deletions .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ startminute
startt
stdlib
stepline
strptime
sunsynk
syscmd
tabindex
Expand Down
20 changes: 10 additions & 10 deletions apps/predbat/alertfeed.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def process_alerts(self, minutes_now, midnight_utc, testing=False):
if not alert_config:
return alerts, alert_active_keep
if not isinstance(alert_config, dict):
self.log("Warn: AlertFeed: Alerts must be a dictionary, ignoring")
self.log("Warn: AlertFeed: Weather alerts must be a dictionary, ignoring")
return alerts, alert_active_keep

# Try apps.yaml
Expand All @@ -80,10 +80,10 @@ def process_alerts(self, minutes_now, midnight_utc, testing=False):

# If latitude and longitude are not found, we cannot process alerts
if latitude and longitude:
self.log("AlertFeed: Processing alerts for approx position latitude {} longitude {}".format(dp1(latitude), dp1(longitude)))
self.log("AlertFeed: Processing weather alerts for approx position latitude {}, longitude {}".format(dp1(latitude), dp1(longitude)))
else:
if not testing:
self.log("Warn: AlertFeed: No latitude or longitude found, cannot process alerts")
self.log("Warn: AlertFeed: No latitude or longitude found, cannot process weather alerts")
return alerts, alert_active_keep

area = alert_config.get("area", "")
Expand Down Expand Up @@ -121,7 +121,7 @@ def apply_alerts(self, alerts, keep, minutes_now, midnight_utc):
onset_minutes = int((onset - midnight_utc).total_seconds() / 60)
expires_minutes = int((expires - midnight_utc).total_seconds() / 60)
if expires_minutes >= minutes_now:
self.log("Info: AlertFeed: Active alert: {} severity {} certainty {} urgency {} from {} to {} applying keep {}".format(alert.get("event"), severity, certainty, urgency, onset, expires, keep))
self.log("Info: AlertFeed: Active weather alert: {}, severity {}, certainty {}, urgency {} from {} to {}, applying battery keep {}%".format(alert.get("event"), severity, certainty, urgency, onset, expires, keep))
for minute in range(onset_minutes, expires_minutes):
if minute not in alert_active_keep:
alert_active_keep[minute] = keep
Expand Down Expand Up @@ -226,7 +226,7 @@ def filter_alerts(self, alerts, area=None, event=None, severity=None, certainty=

async def download_alert_data(self, url):
"""
Download octopus free session data directly from a URL
Download Weather Alert data directly from a URL
"""
# Check the cache first
now = datetime.now()
Expand All @@ -235,7 +235,7 @@ async def download_alert_data(self, url):
pdata = self.alert_cache[url]["data"]
age = now - stamp
if age.seconds < (30 * 60):
self.log("AlertFeed: Return cached alert data for {} age {} minutes".format(url, dp1(age.seconds / 60)))
self.log("AlertFeed: Return cached weather alert data from URL {}, age {} minutes".format(url, dp1(age.seconds / 60)))
self.update_success_timestamp()
return pdata

Expand All @@ -245,11 +245,11 @@ async def download_alert_data(self, url):
async with session.get(url) as response:
status_code = response.status
if status_code not in [200, 201]:
self.log("Warn: AlertFeed: Error downloading alert data from URL {}, code {}".format(url, status_code))
self.log("Warn: AlertFeed: Error downloading weather alert data from URL {}, error code {}".format(url, status_code))
return None

text = await response.text()
self.log("AlertFeed: Downloaded alert data from {} size {} bytes".format(url, len(text)))
self.log("AlertFeed: Downloaded weather alert data from URL {}, size {} bytes".format(url, len(text)))

# Return new data
self.alert_cache[url] = {}
Expand All @@ -258,7 +258,7 @@ async def download_alert_data(self, url):
self.update_success_timestamp()
return text
except (aiohttp.ClientError, Exception) as e:
self.log("Warn: AlertFeed: Exception downloading alert data from URL {}: {}".format(url, e))
self.log("Warn: AlertFeed: Exception downloading weather alert data from URL {}: {}".format(url, e))
return None

def parse_alert_data(self, xml):
Expand All @@ -272,7 +272,7 @@ def parse_alert_data(self, xml):
try:
root = etree.fromstring(xml)
except Exception as e:
self.log("Warn: Failed to extract alerts from xml data exception: {}".format(e))
self.log("Warn: Failed to extract weather alerts from XML data exception: {}".format(e))

if root:
for entry in root:
Expand Down
8 changes: 4 additions & 4 deletions apps/predbat/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ def publish_inverter_data(self):
self.prefix + ".pv_power",
state=dp3(self.pv_power / 1000.0),
attributes={
"friendly_name": "Predicted PV Power",
"friendly_name": "Current PV Power",
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:battery",
Expand All @@ -849,7 +849,7 @@ def publish_inverter_data(self):
self.prefix + ".grid_power",
state=dp3(self.grid_power / 1000.0),
attributes={
"friendly_name": "Predicted Grid Power",
"friendly_name": "Current Grid Power",
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:battery",
Expand All @@ -859,7 +859,7 @@ def publish_inverter_data(self):
self.prefix + ".load_power",
state=dp3(self.load_power / 1000.0),
attributes={
"friendly_name": "Predicted Load Power",
"friendly_name": "Current Load Power",
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:battery",
Expand All @@ -869,7 +869,7 @@ def publish_inverter_data(self):
self.prefix + ".battery_power",
state=dp3(self.battery_power / 1000.0),
attributes={
"friendly_name": "Predicted Battery Power",
"friendly_name": "Current Battery Power",
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:battery",
Expand Down
12 changes: 6 additions & 6 deletions apps/predbat/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from datetime import datetime, timedelta
from config import INVERTER_DEF, SOLAX_SOLIS_MODES_NEW, SOLAX_SOLIS_MODES
from const import MINUTE_WATT, TIME_FORMAT, TIME_FORMAT_OCTOPUS, INVERTER_TEST, TIME_FORMAT_SECONDS, INVERTER_MAX_RETRY, INVERTER_MAX_RETRY_REST
from utils import calc_percent_limit, compute_window_minutes, dp0, dp2, dp3, dp4, time_string_to_stamp, minute_data, minute_data_state, window2minutes
from utils import calc_percent_limit, compute_window_minutes, dp0, dp1, dp2, dp3, dp4, time_string_to_stamp, minute_data, minute_data_state, window2minutes

TIME_FORMAT_HMS = "%H:%M:%S"

Expand Down Expand Up @@ -752,7 +752,7 @@ def find_charge_curve(self, discharge):

if soc_kwh_sensor and charge_rate_sensor and battery_power_sensor and predbat_status_sensor:
battery_power_sensor = battery_power_sensor.replace("number.", "sensor.") # Workaround as old template had number.
self.log("Find {} curve with sensors {}, {}, {} and {}".format(curve_type, soc_kwh_sensor, charge_rate_sensor, predbat_status_sensor, battery_power_sensor))
self.log("Looking for {} curve with sensors {}, {}, {} and {}".format(curve_type, soc_kwh_sensor, charge_rate_sensor, predbat_status_sensor, battery_power_sensor))
if soc_kwh_percent:
soc_kwh_data = self.base.get_history_wrapper(entity_id=soc_kwh_sensor, days=self.base.max_days_previous, required=False)
else:
Expand Down Expand Up @@ -830,7 +830,7 @@ def find_charge_curve(self, discharge):
for minute in battery_power:
battery_power[minute] = -battery_power[minute]
min_len = min(len(soc_kwh), len(charge_rate), len(predbat_status), len(battery_power))
self.log("Find {} curve has {} days of data, max days {}".format(curve_type, min_len / 60 / 24.0, self.base.max_days_previous))
self.log("Looking for {} curve, have found {} days of history data, max days {}".format(curve_type, dp1(min_len / 60 / 24.0), self.base.max_days_previous))

soc_percent = {}
for minute in range(0, min_len):
Expand Down Expand Up @@ -998,13 +998,13 @@ def find_charge_curve(self, discharge):
self.log("Note: Found incomplete battery {} curve (no data points), maybe try again when you have more data.".format(curve_type))
else:
self.log(
"Note: Cannot find battery {} curve (no final curve found for battery to {}), one of the required settings for {}, {}_rate, battery_power and predbat.status do not have history, check apps.yaml".format(
curve_type, curve_label, soc_label, curve_type
"Note: Cannot find battery {} curve (no full rate {} curve found for battery to {}), one of the required settings for {}, {}_rate, battery_power and predbat.status do not have history, check apps.yaml".format(
curve_type, curve_type, curve_label, soc_label, curve_type
)
)
else:
self.log("Note: Cannot find battery {} curve (missing history), one of the required settings for {}, {}_rate, battery_power and predbat.status do not have history, check apps.yaml".format(curve_type, soc_label, curve_type))
self.log("Note: Sensor with history data lengths: {} {}, {}_rate {}, battery_power {}, predbat_status {}".format(soc_label, len(soc_kwh), curve_type, len(charge_rate), len(battery_power), len(predbat_status)))
self.log("Note: Sensor history data lengths: {} {}, {}_rate {}, battery_power {}, predbat_status {}".format(soc_label, len(soc_kwh), curve_type, len(charge_rate), len(battery_power), len(predbat_status)))
else:
self.log("Note: Cannot find battery {} curve (settings missing), one of the required settings for {}, {}_rate and battery_power are missing from apps.yaml".format(curve_type, soc_label, curve_type))
return {}
Expand Down
12 changes: 6 additions & 6 deletions apps/predbat/octopus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2047,8 +2047,8 @@ def load_octopus_slots(self, octopus_slots, octopus_intelligent_consider_full):
new_slot["average"] = self.rate_import.get(start_minutes, self.rate_min)
if octopus_slot_low_rate and source != "bump-charge":
new_slot["average"] = self.rate_min # Assume price in min
new_slot["cost"] = new_slot["average"] * kwh
new_slot["soc"] = car_soc
new_slot["cost"] = dp2(new_slot["average"] * kwh)
new_slot["soc"] = dp2(car_soc)
new_slots.append(new_slot)

if end_minutes_original > end_minutes:
Expand All @@ -2060,7 +2060,7 @@ def load_octopus_slots(self, octopus_slots, octopus_intelligent_consider_full):
if octopus_slot_low_rate and source != "bump-charge":
new_slot["average"] = self.rate_min # Assume price in min
new_slot["cost"] = 0.0
new_slot["soc"] = car_soc
new_slot["soc"] = dp2(car_soc)
new_slots.append(new_slot)

else:
Expand All @@ -2072,8 +2072,8 @@ def load_octopus_slots(self, octopus_slots, octopus_intelligent_consider_full):
new_slot["average"] = self.rate_import.get(start_minutes, self.rate_min)
if octopus_slot_low_rate and source != "bump-charge":
new_slot["average"] = self.rate_min # Assume price in min
new_slot["cost"] = new_slot["average"] * kwh
new_slot["soc"] = car_soc
new_slot["cost"] = dp2(new_slot["average"] * kwh)
new_slot["soc"] = dp2(car_soc)
new_slots.append(new_slot)
return new_slots

Expand Down Expand Up @@ -2136,7 +2136,7 @@ def rate_add_io_slots(self, rates, octopus_slots):
assumed_price = self.rate_import.get(start_minutes, self.rate_min)

self.log(
"Octopus Intelligent slot at {}-{} assumed price {} amount {} kWh location {} source {} octopus_slot_low_rate {}".format(
"Octopus Intelligent slot at {}-{}, assumed price {}, amount {}, kWh location {}, source {}, octopus_slot_low_rate {}".format(
self.time_abs_str(start_minutes), self.time_abs_str(end_minutes), dp2(assumed_price), dp2(kwh), location, source, octopus_slot_low_rate
)
)
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/tests/test_octopus_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def run_load_octopus_slots_tests(my_predbat):
]

loaded_slots = my_predbat.load_octopus_slots(sample_bad, False)
expected_loaded = "[{'start': 870, 'end': 900, 'kwh': 1.29, 'average': 4, 'cost': 5.16, 'soc': 1.29}, {'start': 900, 'end': 930, 'kwh': 3.17, 'average': 4, 'cost': 12.68, 'soc': 4.46}, {'start': 930, 'end': 960, 'kwh': 3.18, 'average': 4, 'cost': 12.72, 'soc': 7.640000000000001}, {'start': 960, 'end': 990, 'kwh': 3.14, 'average': 4, 'cost': 12.56, 'soc': 10}, {'start': 990, 'end': 1050, 'kwh': 7.47, 'average': 4, 'cost': 29.88, 'soc': 10}, {'start': 1050, 'end': 1080, 'kwh': 3.0, 'average': 4, 'cost': 12.0, 'soc': 10}]"
expected_loaded = "[{'start': 870, 'end': 900, 'kwh': 1.29, 'average': 4, 'cost': 5.16, 'soc': 1.29}, {'start': 900, 'end': 930, 'kwh': 3.17, 'average': 4, 'cost': 12.68, 'soc': 4.46}, {'start': 930, 'end': 960, 'kwh': 3.18, 'average': 4, 'cost': 12.72, 'soc': 7.64}, {'start': 960, 'end': 990, 'kwh': 3.14, 'average': 4, 'cost': 12.56, 'soc': 10}, {'start': 990, 'end': 1050, 'kwh': 7.47, 'average': 4, 'cost': 29.88, 'soc': 10}, {'start': 1050, 'end': 1080, 'kwh': 3.0, 'average': 4, 'cost': 12.0, 'soc': 10}]"
if str(loaded_slots) != expected_loaded:
print("ERROR: Loaded slots should be {}\ngot {}".format(expected_loaded, loaded_slots))
failed = True
Expand Down
Loading