fix hard limit pro nabijeni
All checks were successful
CI and deploy / migration-check (push) Successful in 4s
CI and deploy / deploy (push) Successful in 30s

This commit is contained in:
Dusan Vojacek
2026-04-19 14:23:10 +02:00
parent ee27f4e3fd
commit 0814b1d8e8
3 changed files with 166 additions and 22 deletions

View File

@@ -159,10 +159,19 @@ def _select_charge_slots(
current_soc_wh: float,
) -> set[int]:
"""
Pre-select which slots are eligible for battery charging.
Only the X cheapest sell-price PV-surplus slots are selected,
enough to fill the battery with a configurable buffer.
Returns set of slot indices. Empty set = no restriction.
Pre-select which slots are eligible for battery charging (anti-micro-cycling).
Logika:
1) Sloty s PV-surplus (pv_a + pv_b > load_baseline) jsou vždy zahrnuty
jde o „zdarma“ nabíjení z FVE, nemá smysl ho zakazovat.
2) Zbývající energetický rozpočet (cíl = charge_buf × (soc_max current_soc),
snížený o očekávaný přínos z PV-surplus slotů) se doplní nejlevnějšími sloty
podle buy_price (nákupní cena ze sítě).
3) Per-slot kapacita přírůstku SoC = max_charge_power × η × 15 min (plný výkon,
ne limitovaný aktuálním PV-surplus výkonem).
Vrací množinu indexů povolených pro `bc[t] > 0` v MILP. Prázdná množina = žádné
restrikce. `charge_slot_buffer <= 0` v DB ⇒ všechny sloty povoleny.
"""
charge_buf = float(getattr(battery, "charge_slot_buffer", 0) or 0)
if charge_buf <= 0:
@@ -172,28 +181,35 @@ def _select_charge_slots(
if energy_to_fill <= 0:
return set()
candidates: list[tuple[int, float, float]] = []
for t, s in enumerate(slots):
pv_surplus = max(0, s.pv_a_forecast_w + s.pv_b_forecast_w - s.load_baseline_w)
if pv_surplus <= 0:
continue
charge_w = min(float(battery.max_charge_power_w), float(pv_surplus))
charge_wh = charge_w * float(battery.charge_efficiency) * INTERVAL_H
candidates.append((t, float(s.sell_price), charge_wh))
candidates.sort(key=lambda x: x[1])
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.0)
max_p_w = float(getattr(battery, "max_charge_power_w", 0.0) or 0.0)
per_slot_full_wh = max_p_w * eta * INTERVAL_H
selected: set[int] = set()
pv_budget_wh = 0.0
for t, s in enumerate(slots):
pv_surplus_w = max(0, s.pv_a_forecast_w + s.pv_b_forecast_w - s.load_baseline_w)
if pv_surplus_w <= 0:
continue
selected.add(t)
pv_budget_wh += min(float(pv_surplus_w), max_p_w) * eta * INTERVAL_H
target_wh = energy_to_fill * charge_buf
remaining_wh = max(0.0, target_wh - pv_budget_wh)
if remaining_wh <= 0 or per_slot_full_wh <= 0:
return selected
grid_candidates = [
(t, float(s.buy_price)) for t, s in enumerate(slots) if t not in selected
]
grid_candidates.sort(key=lambda x: x[1])
cumulative = 0.0
target = energy_to_fill * charge_buf
for t, _price, wh in candidates:
if cumulative >= target:
for t, _price in grid_candidates:
if cumulative >= remaining_wh:
break
selected.add(t)
cumulative += wh
if cumulative < energy_to_fill:
selected = set(c[0] for c in candidates)
cumulative += per_slot_full_wh
return selected