Skip to content

Commit a4b71d9

Browse files
Merge pull request #4651 from springfall2008/feat/freeze-export-discharge-rate
feat(prediction): model residual battery discharge during Freeze Export
2 parents 3d27f9d + 1731fdf commit a4b71d9

16 files changed

Lines changed: 186 additions & 7 deletions

apps/predbat/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2443,6 +2443,7 @@
24432443
"pause_end_time": {"type": "sensor_list", "sensor_type": "none|string", "modify": True, "entries": "num_inverters"},
24442444
"inverter_limit": {"type": "sensor_list", "sensor_type": "float", "modify": False, "zero": False, "entries": "num_inverters"},
24452445
"inverter_can_charge_during_export": {"type": "boolean"},
2446+
"inverter_freeze_export_discharge_rate": {"type": "float", "zero": True},
24462447
"pv_ac_limit": {"type": "float", "zero": True},
24472448
"inverter_limit_charge": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
24482449
"inverter_limit_charge_dc": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},

apps/predbat/fetch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,6 +2645,8 @@ def fetch_config_options(self):
26452645
self.inverter_loss = 1.0 - self.get_arg("inverter_loss")
26462646
self.inverter_hybrid = self.get_arg("inverter_hybrid")
26472647
self.pv_ac_limit = self.get_arg("pv_ac_limit", 0.0) / MINUTE_WATT
2648+
self.inverter_freeze_export_discharge_rate = max(self.get_arg("inverter_freeze_export_discharge_rate", 0.0), 0.0) / MINUTE_WATT
2649+
self.log("Freeze Export discharge rate configured: {:.0f} W".format(self.inverter_freeze_export_discharge_rate * MINUTE_WATT))
26482650
self.base_load = self.get_arg("base_load", 100) / 1000.0
26492651

26502652
# Charge curve

apps/predbat/predbat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ def reset(self):
391391
self.battery_loss = 1.0
392392
self.battery_loss_discharge = 1.0
393393
self.inverter_loss = 1.0
394+
self.inverter_freeze_export_discharge_rate = 0.0
394395
self.inverter_hybrid = True
395396
self.pv_ac_limit = 0
396397
self.inverter_soc_reset = False

apps/predbat/prediction.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def __init__(
128128
self.iboost_rate_threshold_export = base.iboost_rate_threshold_export
129129
self.rate_gas = base.rate_gas
130130
self.inverter_loss = base.inverter_loss
131+
self.inverter_freeze_export_discharge_rate = base.inverter_freeze_export_discharge_rate
131132
self.inverter_hybrid = base.inverter_hybrid
132133
self.inverter_limit = base.inverter_limit
133134
self.export_limit = base.export_limit
@@ -634,6 +635,7 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
634635
battery_rate_max_discharge = self.battery_rate_max_discharge
635636
battery_rate_max_export = self.battery_rate_max_export
636637
battery_rate_min = self.battery_rate_min
638+
inverter_freeze_export_discharge_rate = self.inverter_freeze_export_discharge_rate
637639
carbon_intensity = self.carbon_intensity
638640
set_discharge_during_charge = self.set_discharge_during_charge
639641
battery_charge_power_curve_tuple = charge_curve_to_tuple(self.battery_charge_power_curve)
@@ -1089,7 +1091,24 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
10891091
pv_dc = min(abs(battery_draw), pv_now)
10901092
pv_ac = (pv_now - pv_dc) * inverter_loss_ac
10911093

1092-
battery_state = "fz+" if battery_draw < 0 else "fz~"
1094+
# Some inverters (observed on AlphaESS) continue a small residual battery
1095+
# discharge during Freeze Export. Treat the configured value as battery-side
1096+
# power and feed it into the normal AC balance: house load consumes it first and
1097+
# any surplus may reach the grid. Respect the battery reserve and physical export
1098+
# limit rather than capping the discharge at house demand.
1099+
if inverter_freeze_export_discharge_rate > 0 and battery_draw >= 0:
1100+
freeze_draw = min(inverter_freeze_export_discharge_rate * step * battery_loss_discharge, battery_to_min)
1101+
freeze_diff = get_diff(freeze_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)
1102+
if freeze_diff < 0 and abs(freeze_diff) > export_limit:
1103+
freeze_draw = max(freeze_draw - (abs(freeze_diff) - export_limit) * inverter_loss_recp, 0)
1104+
battery_draw = freeze_draw
1105+
1106+
if battery_draw < 0:
1107+
battery_state = "fz+"
1108+
elif battery_draw > 0:
1109+
battery_state = "fz-"
1110+
else:
1111+
battery_state = "fz~"
10931112
else:
10941113
# ECO Mode
10951114
pv_ac = pv_now * inverter_loss_ac

apps/predbat/prediction_kernel.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
// unconditionally, so loading one against this Python segfaults on the first prediction rather than
4343
// falling back. Bumping makes the loader reject it and use the Python engine, which is the whole
4444
// point of the check.
45-
#define PK_ABI_VERSION 4
46-
#define PK_PARITY_REVISION 7
45+
#define PK_ABI_VERSION 5
46+
#define PK_PARITY_REVISION 9
4747
#define PK_MAX_CARS 8
4848
#define PK_RUN_EVERY 5 // const.py RUN_EVERY
4949

@@ -169,6 +169,7 @@ struct PkContext {
169169
double battery_loss;
170170
double battery_loss_discharge;
171171
double inverter_loss;
172+
double inverter_freeze_export_discharge_rate; // per-minute rate (multiplied by step in the kernel), residual battery-side discharge entering the AC balance during Freeze Export
172173
double inverter_limit; // per-minute rate (multiplied by step in the kernel)
173174
double export_limit; // per-minute rate
174175
double pv_ac_limit; // per-minute rate
@@ -714,6 +715,7 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
714715
const double battery_rate_max_discharge = c->battery_rate_max_discharge;
715716
const double battery_rate_max_export = c->battery_rate_max_export;
716717
const double battery_rate_min = c->battery_rate_min;
718+
const double inverter_freeze_export_discharge_rate = c->inverter_freeze_export_discharge_rate;
717719
// PV10 de-rating of the charge rate - prediction.py:587-592. PV90 is the upside case, no de-rate.
718720
const double battery_rate_max_scaling = is_pv10 ? c->battery_rate_max_scaling10 : c->battery_rate_max_scaling;
719721
const double battery_rate_max_scaling_discharge = c->battery_rate_max_scaling_discharge;
@@ -1077,6 +1079,19 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
10771079
pv_ac = (pv_now - pv_dc) * inverter_loss_ac;
10781080
}
10791081
}
1082+
1083+
// Some inverters (observed on AlphaESS) continue a small residual battery
1084+
// discharge during Freeze Export. Feed the battery-side rate into the normal AC
1085+
// balance so load consumes it first and any surplus may export, while respecting
1086+
// the reserve and the physical grid export limit.
1087+
if (inverter_freeze_export_discharge_rate > 0 && battery_draw >= 0) {
1088+
double freeze_draw = std::min(inverter_freeze_export_discharge_rate * step * battery_loss_discharge, battery_to_min);
1089+
const double freeze_diff = get_diff(freeze_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);
1090+
if (freeze_diff < 0 && std::abs(freeze_diff) > export_limit) {
1091+
freeze_draw = std::max(freeze_draw - (std::abs(freeze_diff) - export_limit) * inverter_loss_recp, 0.0);
1092+
}
1093+
battery_draw = freeze_draw;
1094+
}
10801095
} else {
10811096
// ECO Mode - prediction.py:951-997
10821097
pv_ac = pv_now * inverter_loss_ac;

apps/predbat/prediction_kernel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from utils import get_curve_value, find_battery_temperature_cap, in_car_slot, in_iboost_slot
3232

3333
# Expected ABI/parity revisions of the shared library (see prediction_kernel.cpp)
34-
KERNEL_ABI_VERSION = 4
35-
KERNEL_PARITY_REVISION = 7
34+
KERNEL_ABI_VERSION = 5
35+
KERNEL_PARITY_REVISION = 9
3636

3737
# Maximum number of cars supported by the kernel (PK_MAX_CARS in prediction_kernel.cpp)
3838
KERNEL_MAX_CARS = PREDBAT_MAX_CARS
@@ -78,6 +78,7 @@ class PkContext(ctypes.Structure):
7878
("battery_loss", ctypes.c_double),
7979
("battery_loss_discharge", ctypes.c_double),
8080
("inverter_loss", ctypes.c_double),
81+
("inverter_freeze_export_discharge_rate", ctypes.c_double),
8182
("inverter_limit", ctypes.c_double),
8283
("export_limit", ctypes.c_double),
8384
("pv_ac_limit", ctypes.c_double),
@@ -658,6 +659,7 @@ def create_kernel_context(pred, static_cache=None):
658659
ctx.battery_loss = pred.battery_loss
659660
ctx.battery_loss_discharge = pred.battery_loss_discharge
660661
ctx.inverter_loss = pred.inverter_loss
662+
ctx.inverter_freeze_export_discharge_rate = pred.inverter_freeze_export_discharge_rate
661663
ctx.inverter_limit = pred.inverter_limit
662664
ctx.export_limit = pred.export_limit
663665
ctx.pv_ac_limit = pred.pv_ac_limit
80 Bytes
Binary file not shown.
144 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)