|
| 1 | +from pathlib import Path |
| 2 | + |
| 3 | + |
| 4 | +def replace_once(path, old, new): |
| 5 | + p = Path(path) |
| 6 | + text = p.read_text() |
| 7 | + count = text.count(old) |
| 8 | + if count != 1: |
| 9 | + raise RuntimeError(f"{path}: expected exactly one match, found {count}") |
| 10 | + p.write_text(text.replace(old, new, 1)) |
| 11 | + |
| 12 | + |
| 13 | +replace_once( |
| 14 | + "apps/predbat/prediction.py", |
| 15 | + ''' # Some inverters (observed on AlphaESS) continue to discharge the battery\n # during Freeze Export to supply house load. The configured value is measured\n # battery-side power, so pass it through the normal discharge and inverter-loss\n # paths, cap it at remaining house demand, and never create additional export.\n if inverter_freeze_export_discharge_rate > 0 and battery_draw >= 0:\n freeze_house_demand = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)\n if freeze_house_demand > 0:\n freeze_soc_available = min(inverter_freeze_export_discharge_rate * step / 60000.0, max(soc - reserve_expected, 0))\n freeze_draw_limit = freeze_soc_available * battery_loss_discharge\n freeze_draw_for_house = freeze_house_demand * inverter_loss_recp\n battery_draw = min(freeze_draw_limit, freeze_draw_for_house)\n''', |
| 16 | + ''' # Some inverters (observed on AlphaESS) continue a residual battery\n # discharge during Freeze Export. The configured value is measured battery-side\n # power; feed it into the normal AC energy balance so house load consumes it first\n # and any excess naturally becomes grid export.\n if inverter_freeze_export_discharge_rate > 0 and battery_draw >= 0:\n freeze_soc_available = min(inverter_freeze_export_discharge_rate * step / 60000.0, max(soc - reserve_expected, 0))\n freeze_draw_limit = freeze_soc_available * battery_loss_discharge\n battery_draw = max(battery_draw, freeze_draw_limit)\n''', |
| 17 | +) |
| 18 | + |
| 19 | +replace_once( |
| 20 | + "apps/predbat/prediction_kernel.cpp", |
| 21 | + ''' // Some inverters (observed on AlphaESS) continue to discharge the battery\n // during Freeze Export to supply house load. The configured value is battery-side\n // power; route it through normal discharge and inverter losses and cap at load.\n if (inverter_freeze_export_discharge_rate > 0 && battery_draw >= 0) {\n const double freeze_house_demand = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);\n if (freeze_house_demand > 0) {\n const double freeze_soc_available = std::min(inverter_freeze_export_discharge_rate * step / 60000.0, std::max(soc - reserve_expected, 0.0));\n const double freeze_draw_limit = freeze_soc_available * battery_loss_discharge;\n const double freeze_draw_for_house = freeze_house_demand * inverter_loss_recp;\n battery_draw = std::min(freeze_draw_limit, freeze_draw_for_house);\n }\n }\n''', |
| 22 | + ''' // Some inverters (observed on AlphaESS) continue a residual battery\n // discharge during Freeze Export. The configured value is battery-side power; feed\n // it into the normal AC energy balance so house load consumes it first and any excess\n // naturally becomes grid export.\n if (inverter_freeze_export_discharge_rate > 0 && battery_draw >= 0) {\n const double freeze_soc_available = std::min(inverter_freeze_export_discharge_rate * step / 60000.0, std::max(soc - reserve_expected, 0.0));\n const double freeze_draw_limit = freeze_soc_available * battery_loss_discharge;\n battery_draw = std::max(battery_draw, freeze_draw_limit);\n }\n''', |
| 23 | +) |
| 24 | + |
| 25 | +replace_once( |
| 26 | + "apps/predbat/prediction_kernel.cpp", |
| 27 | + "#define PK_PARITY_REVISION 8\n", |
| 28 | + "#define PK_PARITY_REVISION 9\n", |
| 29 | +) |
| 30 | + |
| 31 | +replace_once( |
| 32 | + "apps/predbat/prediction_kernel.py", |
| 33 | + "KERNEL_PARITY_REVISION = 8\n", |
| 34 | + "KERNEL_PARITY_REVISION = 9\n", |
| 35 | +) |
| 36 | + |
| 37 | +replace_once( |
| 38 | + "apps/predbat/predbat.py", |
| 39 | + " # Battery-side discharge (W) that may continue to supply house load during Freeze Export.\n", |
| 40 | + " # Battery-side residual discharge (W) during Freeze Export; AC balance decides house use vs export.\n", |
| 41 | +) |
| 42 | + |
| 43 | +replace_once( |
| 44 | + "apps/predbat/tests/test_model.py", |
| 45 | + ''' # Freeze Export discharge should represent real battery energy supplied to house load.\n # Normal battery discharge is disabled here so only this configured path is under test.\n # 240 W for one hour is 0.24 kWh battery-side.\n''', |
| 46 | + ''' # Freeze Export residual discharge is real battery energy delivered to the AC side.\n # House load consumes it first and any excess can flow to grid export. Normal battery\n # discharge is disabled here so only this configured path is under test.\n # 240 W for one hour is 0.24 kWh battery-side.\n''', |
| 47 | +) |
| 48 | + |
| 49 | +replace_once( |
| 50 | + "apps/predbat/tests/test_model.py", |
| 51 | + ''' failed |= simple_scenario(\n "freeze_export_house_supply_no_load",\n my_predbat,\n 0,\n 0,\n assert_final_metric=0,\n assert_final_soc=10.0,\n battery_size=10.0,\n battery_soc=10.0,\n discharge=99,\n end_record=60,\n inverter_freeze_export_discharge_rate=240.0,\n battery_rate_max_charge=0.0,\n assert_battery_cycle=0.0,\n )\n''', |
| 52 | + ''' failed |= simple_scenario(\n "freeze_export_residual_exports_when_no_load",\n my_predbat,\n 0,\n 0,\n assert_final_metric=-1.2,\n assert_final_soc=9.76,\n battery_size=10.0,\n battery_soc=10.0,\n discharge=99,\n end_record=60,\n inverter_freeze_export_discharge_rate=240.0,\n battery_rate_max_charge=0.0,\n assert_battery_cycle=0.24,\n )\n''', |
| 53 | +) |
| 54 | + |
| 55 | +print("Freeze Export residual AC-flow rewrite applied") |
0 commit comments