deye ridi maximalni flow do baterie hlavne z gridu
This commit is contained in:
@@ -429,7 +429,14 @@ def solve_dispatch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- Proměnné ---
|
# --- Proměnné ---
|
||||||
gi = [pulp.LpVariable(f"gi_{t}", 0, grid.max_import_power_w) for t in range(T)]
|
# gi[t] horní mez: site breaker (max_import_power_w) je fyzický strop, ale o jeho dodržení
|
||||||
|
# se v reálném čase stará **Deye reg 128** (grid charge current) + firmware throttling —
|
||||||
|
# dynamicky sníží nabíjení baterie, když aktuální `load + bc` přesáhne breaker. Proto LP
|
||||||
|
# povolí nominálně import až **breaker + BMS max charge**, aby mohl plánovat `bc = BMS max`
|
||||||
|
# i v slotech s vyšší baseline zátěží (jinak tvrdý strop zbytečně osekává arbitráž v cenově
|
||||||
|
# nejlepších 15min oknech). Reálný hardware nikdy víc než breaker nenatáhne.
|
||||||
|
gi_upper = float(grid.max_import_power_w) + float(battery.max_charge_power_w)
|
||||||
|
gi = [pulp.LpVariable(f"gi_{t}", 0, gi_upper) for t in range(T)]
|
||||||
ge = [pulp.LpVariable(f"ge_{t}", 0, grid.max_export_power_w) for t in range(T)]
|
ge = [pulp.LpVariable(f"ge_{t}", 0, grid.max_export_power_w) for t in range(T)]
|
||||||
bc = [pulp.LpVariable(f"bc_{t}", 0, battery.max_charge_power_w) for t in range(T)]
|
bc = [pulp.LpVariable(f"bc_{t}", 0, battery.max_charge_power_w) for t in range(T)]
|
||||||
bd = [pulp.LpVariable(f"bd_{t}", 0, battery.max_discharge_power_w) for t in range(T)]
|
bd = [pulp.LpVariable(f"bd_{t}", 0, battery.max_discharge_power_w) for t in range(T)]
|
||||||
|
|||||||
@@ -259,5 +259,58 @@ class PlanningDispatchMilpTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_grid_import_cap_allows_full_bms_charge_above_breaker(self) -> None:
|
||||||
|
"""
|
||||||
|
Cheap buy, load 3.7 kW, PV malé → breaker 17 kW limituje gi, ale bc musí moct být
|
||||||
|
plných BMS 18 kW (Deye reg 128 + firmware throttling chrání jistič fyzicky).
|
||||||
|
"""
|
||||||
|
slots = [
|
||||||
|
_slot(load=3700, buy=0.4, sell=-0.3, pv_a=0, pv_b=1500),
|
||||||
|
_slot(load=2000, buy=5.0, sell=4.5, pv_a=0, pv_b=0),
|
||||||
|
_slot(load=2000, buy=5.0, sell=4.5, pv_a=0, pv_b=0),
|
||||||
|
]
|
||||||
|
battery = _battery(uc_wh=64_000.0, min_pct=12.0, arb_pct=20.0)
|
||||||
|
battery.max_charge_power_w = 18_000
|
||||||
|
battery.max_discharge_power_w = 18_000
|
||||||
|
hp = SimpleNamespace(
|
||||||
|
rated_heating_power_w=0,
|
||||||
|
tuv_min_temp_c=45.0,
|
||||||
|
tuv_target_temp_c=55.0,
|
||||||
|
)
|
||||||
|
grid = SimpleNamespace(max_import_power_w=17_000, max_export_power_w=13_500)
|
||||||
|
vehicles = [
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0,
|
||||||
|
battery_capacity_kwh=1.0,
|
||||||
|
default_target_soc_pct=80.0,
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0,
|
||||||
|
battery_capacity_kwh=1.0,
|
||||||
|
default_target_soc_pct=80.0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
soc0 = 0.55 * battery.usable_capacity_wh
|
||||||
|
results, _ms = solve_dispatch(
|
||||||
|
slots,
|
||||||
|
battery,
|
||||||
|
hp,
|
||||||
|
grid,
|
||||||
|
[None, None],
|
||||||
|
vehicles,
|
||||||
|
soc0,
|
||||||
|
50.0,
|
||||||
|
tuv_delta_stats=None,
|
||||||
|
operating_mode="AUTO",
|
||||||
|
price_failsafe_active=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(results), 3)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
results[0].battery_setpoint_w,
|
||||||
|
17_500,
|
||||||
|
msg="LP must be able to target near-BMS-max charge even when gi would exceed breaker",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
|
|||||||
|-----|-------|--------|----------|---------------|
|
|-----|-------|--------|----------|---------------|
|
||||||
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | EMS počítá proud v **SQL**: `COALESCE(deye_register_max_charge_a, FLOOR(LEAST(W)/51.2))` — sloupec stropu v **A** je volitelný (NULL = jen odvod z kW); při vyplnění např. 350 při W→351 A se použije 350. V Pythonu se navíc **clampuje horní strop 350 A** (`DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A`), aby firmware nevracel 350 při zápisu 351. |
|
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | EMS počítá proud v **SQL**: `COALESCE(deye_register_max_charge_a, FLOOR(LEAST(W)/51.2))` — sloupec stropu v **A** je volitelný (NULL = jen odvod z kW); při vyplnění např. 350 při W→351 A se použije 350. V Pythonu se navíc **clampuje horní strop 350 A** (`DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A`), aby firmware nevracel 350 při zápisu 351. |
|
||||||
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Stejně: `COALESCE(deye_register_max_discharge_a, FLOOR(LEAST(W)/51.2))` + **clamp 350 A** jako u 108. |
|
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Stejně: `COALESCE(deye_register_max_discharge_a, FLOOR(LEAST(W)/51.2))` + **clamp 350 A** jako u 108. |
|
||||||
| 128 | Grid charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Nabíjení ze sítě |
|
| 128 | Grid charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Nabíjení ze sítě. Firmware automaticky sníží reálný proud tak, aby `load + battery_charge` nepřekročil velikost jističe — proto LP v `planning_engine.py` plánuje `gi[t]` až **do `max_import_power_w + BMS_max_charge`**, aby uměl využít cenově nejlepší 15min okna pro nabíjení na plný BMS strop (viz `planning.md` sekce „Plánovací strop gi[t] vs. fyzický jistič"). Fyzické dodržení jističe drží reg 128 + firmware. |
|
||||||
| 130 | Grid charge enable | 0/1 | — | 1 = povolit nabíjení ze sítě |
|
| 130 | Grid charge enable | 0/1 | — | 1 = povolit nabíjení ze sítě |
|
||||||
| 141 | Energy mgmt mode | bitmask | — | EMS vždy **0** (neměnit jinak) |
|
| 141 | Energy mgmt mode | bitmask | — | EMS vždy **0** (neměnit jinak) |
|
||||||
| 142 | Limit control (System work mode) | 0/1/2 | — | **0** = selling first, **1** = zero export to load, **2** = zero export to CT. Hodnota v non-SELL režimech pochází z `asset_inverter.deye_zero_export_mode` (závisí na instalaci – viz tabulka níže). V režimu SELL vždy **0**. |
|
| 142 | Limit control (System work mode) | 0/1/2 | — | **0** = selling first, **1** = zero export to load, **2** = zero export to CT. Hodnota v non-SELL režimech pochází z `asset_inverter.deye_zero_export_mode` (závisí na instalaci – viz tabulka níže). V režimu SELL vždy **0**. |
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ Solver optimalizuje celý horizont (typicky 36h) najednou, čímž přirozeně z
|
|||||||
- Max import ze sítě: dle `site_grid_connection.max_import_power_w`
|
- Max import ze sítě: dle `site_grid_connection.max_import_power_w`
|
||||||
- Konfigurovatelné per site v DB
|
- Konfigurovatelné per site v DB
|
||||||
|
|
||||||
|
#### Plánovací strop `gi[t]` vs. fyzický jistič
|
||||||
|
|
||||||
|
V LP má `grid_import[t]` (proměnná `gi`) horní mez **`max_import_power_w + battery.max_charge_power_w`**, ne jen `max_import_power_w`. Důvod:
|
||||||
|
|
||||||
|
- Ceny se mění co 15 min a cílem je nabíjet baterii v **cenově nejlepších oknech** na BMS max (17–18 kW), i když baseline zátěž doma navíc sežere část jističe.
|
||||||
|
- O fyzické dodržení jističe se stará **Deye reg 128** (grid charge current) + firmware — v reálném čase sníží `bc`, když `load + bc` přesáhne breaker.
|
||||||
|
- Pokud bychom `gi[t] ≤ max_import_power_w` nechali jako tvrdé LP omezení, LP by v slotech s vyšší `load_baseline_w` zbytečně osekával `bc` dolů (viděno např. 2026-04-19 13:30: load 3.7 kW, breaker 17 kW → `bc ≤ 17 − 3.7 + pv_b ≈ 14.7 kW`, i když BMS zvládne 18 kW). Optimistický `gi` horní strop umožní plánovat plné využití BMS v cenových oknech; reálný HW nikdy nepřetáhne jistič.
|
||||||
|
- **Trade-off**: `expected_cost` v plánu může být mírně optimistický (LP spočítá s ~20 kW importem, reálně občas míň kvůli skokům domácí zátěže). Rozdíl se automaticky dohání rolling replanem co 15 min.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Energetická bilance (pro každý 15min slot t)
|
## Energetická bilance (pro každý 15min slot t)
|
||||||
@@ -82,7 +91,7 @@ kde:
|
|||||||
|
|
||||||
| Proměnná | Typ | Rozsah | Popis |
|
| Proměnná | Typ | Rozsah | Popis |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `grid_import[t]` | kontinuální | 0 – max_import | Nákup ze sítě v W |
|
| `grid_import[t]` | kontinuální | 0 – (max_import + bms_max_charge) | Nákup ze sítě v W; breaker fyzicky drží Deye reg 128 |
|
||||||
| `grid_export[t]` | kontinuální | 0 – max_export (13500) | Prodej do sítě v W |
|
| `grid_export[t]` | kontinuální | 0 – max_export (13500) | Prodej do sítě v W |
|
||||||
| `battery_charge[t]` | kontinuální | 0 – max_charge | Nabíjení baterie v W |
|
| `battery_charge[t]` | kontinuální | 0 – max_charge | Nabíjení baterie v W |
|
||||||
| `battery_discharge[t]` | kontinuální | 0 – max_discharge | Vybíjení baterie v W |
|
| `battery_discharge[t]` | kontinuální | 0 – max_discharge | Vybíjení baterie v W |
|
||||||
@@ -200,7 +209,7 @@ soc_min_wh <= soc[t] <= soc_max_wh # min_soc_percent z DB (provozní podlaha,
|
|||||||
```python
|
```python
|
||||||
0 <= battery_charge[t] <= battery.max_charge_power_w
|
0 <= battery_charge[t] <= battery.max_charge_power_w
|
||||||
0 <= battery_discharge[t] <= battery.max_discharge_power_w
|
0 <= battery_discharge[t] <= battery.max_discharge_power_w
|
||||||
0 <= grid_import[t] <= grid.max_import_power_w
|
0 <= grid_import[t] <= grid.max_import_power_w + battery.max_charge_power_w # LP soft; fyzicky drží Deye reg 128
|
||||||
0 <= grid_export[t] <= grid.max_export_power_w # = 13500 pro home-01
|
0 <= grid_export[t] <= grid.max_export_power_w # = 13500 pro home-01
|
||||||
0 <= pv_a_curtailed[t] <= pv_a_forecast[t]
|
0 <= pv_a_curtailed[t] <= pv_a_forecast[t]
|
||||||
0 <= ev_charge[t] <= ev_max_total_w
|
0 <= ev_charge[t] <= ev_max_total_w
|
||||||
@@ -271,7 +280,9 @@ def solve_dispatch(
|
|||||||
prob = pulp.LpProblem("ems_dispatch", pulp.LpMinimize)
|
prob = pulp.LpProblem("ems_dispatch", pulp.LpMinimize)
|
||||||
|
|
||||||
# --- Proměnné ---
|
# --- Proměnné ---
|
||||||
grid_import = [pulp.LpVariable(f"gi_{t}", 0, grid.max_import_power_w) for t in range(T)]
|
# gi horní mez = breaker + BMS max_charge (LP optimistický strop, Deye reg 128 chrání fyzicky)
|
||||||
|
gi_upper = grid.max_import_power_w + battery.max_charge_power_w
|
||||||
|
grid_import = [pulp.LpVariable(f"gi_{t}", 0, gi_upper) for t in range(T)]
|
||||||
grid_export = [pulp.LpVariable(f"ge_{t}", 0, grid.max_export_power_w) for t in range(T)]
|
grid_export = [pulp.LpVariable(f"ge_{t}", 0, grid.max_export_power_w) for t in range(T)]
|
||||||
batt_charge = [pulp.LpVariable(f"bc_{t}", 0, battery.max_charge_power_w) for t in range(T)]
|
batt_charge = [pulp.LpVariable(f"bc_{t}", 0, battery.max_charge_power_w) for t in range(T)]
|
||||||
batt_discharge = [pulp.LpVariable(f"bd_{t}", 0, battery.max_discharge_power_w) for t in range(T)]
|
batt_discharge = [pulp.LpVariable(f"bd_{t}", 0, battery.max_discharge_power_w) for t in range(T)]
|
||||||
|
|||||||
Reference in New Issue
Block a user