Skip to content

Commit 1e6f573

Browse files
feat: Add pv_ac_limit for AC coupled inverter PV clipping
Adds a new pv_ac_limit config setting that allows users with AC-coupled inverters (non-hybrid) to model clipping from their PV system (e.g., microinverters with an AC output limit). - config.py: Add pv_ac_limit to APPS_SCHEMA as a float - fetch.py: Read pv_ac_limit (in Watts) and store as kWh/min on self - predbat.py: Initialize self.pv_ac_limit = 0 - prediction.py: Pass pv_ac_limit through and apply AC clipping in the non-hybrid (else) branch after inverter_limit handling - tests/test_infra.py: Add pv_ac_limit parameter to simple_scenario and reset_inverter - tests/test_model.py: Add four new test scenarios for pv_ac_limit - docs/apps-yaml.md: Document the new pv_ac_limit setting Agent-Logs-Url: https://github.com/springfall2008/batpred/sessions/c2b66364-9d11-4c05-8b9f-88095c31c6dd Co-authored-by: springfall2008 <48591903+springfall2008@users.noreply.github.com>
1 parent 5e67c96 commit 1e6f573

7 files changed

Lines changed: 60 additions & 0 deletions

File tree

‎apps/predbat/config.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,7 @@
20822082
"pause_start_time": {"type": "sensor_list", "sensor_type": "none|string", "modify": True, "entries": "num_inverters"},
20832083
"pause_end_time": {"type": "sensor_list", "sensor_type": "none|string", "modify": True, "entries": "num_inverters"},
20842084
"inverter_limit": {"type": "sensor_list", "sensor_type": "float", "modify": False, "zero": False, "entries": "num_inverters"},
2085+
"pv_ac_limit": {"type": "float", "zero": True},
20852086
"inverter_limit_charge": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
20862087
"inverter_limit_charge_dc": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
20872088
"inverter_limit_discharge": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},

‎apps/predbat/fetch.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,6 +2156,7 @@ def fetch_config_options(self):
21562156
self.battery_loss_discharge = 1.0 - self.get_arg("battery_loss_discharge")
21572157
self.inverter_loss = 1.0 - self.get_arg("inverter_loss")
21582158
self.inverter_hybrid = self.get_arg("inverter_hybrid")
2159+
self.pv_ac_limit = self.get_arg("pv_ac_limit", 0) / MINUTE_WATT
21592160
self.base_load = self.get_arg("base_load", 0) / 1000.0
21602161

21612162
# Charge curve

‎apps/predbat/predbat.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ def reset(self):
529529
self.battery_loss_discharge = 1.0
530530
self.inverter_loss = 1.0
531531
self.inverter_hybrid = True
532+
self.pv_ac_limit = 0
532533
self.inverter_soc_reset = False
533534
self.inverter_set_charge_before = True
534535
self.best_soc_min = 0

‎apps/predbat/prediction.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def __init__(self, base=None, pv_forecast_minute_step=None, pv_forecast_minute10
157157
self.inverter_hybrid = base.inverter_hybrid
158158
self.inverter_limit = base.inverter_limit
159159
self.export_limit = base.export_limit
160+
self.pv_ac_limit = base.pv_ac_limit
160161
self.battery_rate_min = base.battery_rate_min
161162
self.battery_rate_max_charge = base.battery_rate_max_charge
162163
self.battery_rate_max_charge_dc = base.battery_rate_max_charge_dc
@@ -511,6 +512,7 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
511512
car_energy_reported_load = self.car_energy_reported_load
512513
inverter_limit = self.inverter_limit * step
513514
export_limit = self.export_limit * step
515+
pv_ac_limit = self.pv_ac_limit * step
514516
set_charge_low_power = self.set_charge_window and self.set_charge_low_power and (save in ["best", "best10", "test"])
515517
carbon_enable = self.carbon_enable
516518
reserve = self.reserve
@@ -1007,6 +1009,12 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
10071009
else:
10081010
battery_draw = min(battery_draw + over_limit * inverter_loss, 0)
10091011

1012+
# Clip PV AC for non-hybrid (AC coupled) inverters with a PV AC limit (e.g. microinverters)
1013+
if pv_ac_limit > 0 and pv_ac > pv_ac_limit:
1014+
pv_ac_before = pv_ac
1015+
pv_ac = pv_ac_limit
1016+
clipped_today += pv_ac_before - pv_ac
1017+
10101018
# Export limit, clip PV output
10111019
diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)
10121020
if diff < 0 and abs(diff) > export_limit:

‎apps/predbat/tests/test_infra.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ def reset_inverter(my_predbat):
448448
my_predbat.inverter_limit = 1 / 60.0
449449
my_predbat.num_inverters = 1
450450
my_predbat.export_limit = 10 / 60.0
451+
my_predbat.pv_ac_limit = 0
451452
my_predbat.inverters = [TestInverter()]
452453
my_predbat.charge_window = []
453454
my_predbat.export_window = []
@@ -598,6 +599,7 @@ def simple_scenario(
598599
set_charge_freeze=True,
599600
calculate_export_on_pv=True,
600601
assert_clipped=0,
602+
pv_ac_limit=0,
601603
):
602604
"""
603605
No PV, No Load
@@ -656,6 +658,7 @@ def simple_scenario(
656658
my_predbat.inverter_hybrid = hybrid
657659
my_predbat.export_limit = export_limit / 60.0
658660
my_predbat.inverter_limit = inverter_limit / 60.0
661+
my_predbat.pv_ac_limit = pv_ac_limit / 60.0
659662
my_predbat.reserve = reserve
660663
my_predbat.inverter_loss = inverter_loss
661664
my_predbat.battery_rate_max_charge = battery_rate_max_charge / 60.0

‎apps/predbat/tests/test_model.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,6 +1870,30 @@ def run_model_tests(my_predbat):
18701870
assert_iboost_running_solar=True,
18711871
)
18721872

1873+
# PV AC limit tests (AC-coupled / non-hybrid inverters only)
1874+
reset_rates(my_predbat, import_rate, export_rate)
1875+
reset_inverter(my_predbat)
1876+
# No clipping when pv_ac_limit is above the actual PV output
1877+
failed |= simple_scenario("pv_ac_limit_no_clip", my_predbat, 0, 1.0, assert_final_metric=-export_rate * 24, assert_final_soc=0, with_battery=False, pv_ac_limit=2.0)
1878+
# Clipping when pv_ac_limit is below the actual PV output (non-hybrid AC-coupled)
1879+
failed |= simple_scenario("pv_ac_limit_ac_clip", my_predbat, 0, 2.0, assert_final_metric=-export_rate * 24 * 1.5, assert_final_soc=0, with_battery=False, pv_ac_limit=1.5, assert_clipped=24 * 0.5)
1880+
# With a load, clipping still applies; load is met from grid when pv is capped
1881+
failed |= simple_scenario(
1882+
"pv_ac_limit_ac_clip_with_load",
1883+
my_predbat,
1884+
0.5,
1885+
2.0,
1886+
assert_final_metric=-export_rate * 24 * 1.0,
1887+
assert_final_soc=0,
1888+
with_battery=False,
1889+
pv_ac_limit=1.5,
1890+
assert_clipped=24 * 0.5,
1891+
)
1892+
# pv_ac_limit must NOT apply to hybrid inverters (PV is DC-coupled, clipping handled by inverter_limit)
1893+
failed |= simple_scenario("pv_ac_limit_hybrid_ignored", my_predbat, 0, 2.0, assert_final_metric=-export_rate * 24, assert_final_soc=24, with_battery=True, hybrid=True, pv_ac_limit=1.5)
1894+
if failed:
1895+
return failed
1896+
18731897
if failed:
18741898
print("**** ERROR: Some Model tests failed ****")
18751899
return failed

‎docs/apps-yaml.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,28 @@ If you have multiple inverters then set the value of each one in a list format.
913913

914914
NB: inverter_limit is ONLY used by Predbat to improve the quality of the plan, any solar clipping is done by the inverter and is not controlled by Predbat.
915915

916+
### **pv_ac_limit**
917+
918+
Optional, applies to **non-hybrid (AC coupled) inverters only**.
919+
920+
Controls the way Predbat models the maximum AC output of your PV system.
921+
This does not change the way the inverter is controlled.
922+
923+
When set, defines the maximum AC output power in watts for your PV system (e.g. microinverters).
924+
This is used by Predbat to model clipping that occurs when your PV generation exceeds the AC output limit,
925+
for example if you have microinverters with a rated maximum AC output.
926+
927+
This setting is ignored for hybrid inverters (`inverter_hybrid: true`) because in a hybrid system the PV
928+
connects directly to the DC bus and clipping is already modelled via `inverter_limit`.
929+
930+
Example:
931+
932+
```yaml
933+
pv_ac_limit: 3600
934+
```
935+
936+
NB: pv_ac_limit is ONLY used by Predbat to improve the quality of the plan, any solar clipping is done by the inverter and is not controlled by Predbat.
937+
916938
### **export_limit**
917939

918940
One per inverter (optional).

0 commit comments

Comments
 (0)