dalsi a dalsi oprava
Some checks failed
CI and deploy / migration-check (push) Failing after 11s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-23 22:41:00 +02:00
parent 0f922c91f5
commit 645f48036d
3 changed files with 101 additions and 45 deletions

View File

@@ -31,18 +31,26 @@ def _prague_date(s: PlanningSlot) -> date:
return s.interval_start.astimezone(_PRAGUE).date()
def _export_window_start(slots: list[PlanningSlot], degrad: float) -> datetime | None:
"""Kopie R__063: sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
result: datetime | None = None
def _export_window_start_by_day(
slots: list[PlanningSlot], degrad: float
) -> dict[date, datetime]:
"""Kopie R__063: první sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
out: dict[date, datetime] = {}
for s in slots:
day = _prague_date(s)
day_min = min(
float(x.buy_price) for x in slots if _prague_date(x) == day
)
day_min = min(float(x.buy_price) for x in slots if _prague_date(x) == day)
if float(s.sell_price) > day_min + degrad:
if result is None or s.interval_start < result:
result = s.interval_start
return result
prev = out.get(day)
if prev is None or s.interval_start < prev:
out[day] = s.interval_start
return out
def _before_day_export(
slots: list[PlanningSlot], t: int, export_by_day: dict[date, datetime]
) -> bool:
start = export_by_day.get(_prague_date(slots[t]))
return start is not None and slots[t].interval_start < start
def _future_sell(slots: list[PlanningSlot], t: int) -> float:
@@ -55,13 +63,13 @@ def _buy_min_next_n(
t: int,
n: int = _LOOKAHEAD_SLOTS,
*,
export_window_start: datetime | None = None,
export_by_day: dict[date, datetime] | None = None,
) -> float | None:
tail = [
float(slots[i].buy_price)
for i in range(t + 1, min(t + 1 + n, len(slots)))
if export_window_start is None or slots[i].interval_start < export_window_start
]
tail: list[float] = []
for i in range(t + 1, min(t + 1 + n, len(slots))):
day_start = export_by_day.get(_prague_date(slots[i])) if export_by_day else None
if day_start is None or slots[i].interval_start < day_start:
tail.append(float(slots[i].buy_price))
return min(tail) if tail else None
@@ -100,7 +108,7 @@ def _select_charge_slots(
default=min(float(s.buy_price) for s in slots),
)
ref_buy_global = min(float(s.buy_price) for s in slots)
export_window_start = _export_window_start(slots, degrad)
export_by_day = _export_window_start_by_day(slots, degrad)
plan_day = _prague_date(slots[0])
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.0)
@@ -161,12 +169,7 @@ def _select_charge_slots(
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, int, float, int]:
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
before_export = (
0
if export_window_start is not None
and slots[t].interval_start < export_window_start
else 1
)
before_export = 0 if _before_day_export(slots, t, export_by_day) else 1
return (today_first, before_export, int(pred), price, t)
if purchase_pricing_mode != "fixed":
@@ -733,6 +736,38 @@ class FixedPurchasePricingTests(unittest.TestCase):
night = {t for t in out if _prague_hour(slots[t]) < 8}
self.assertGreater(len(night), 0, "očekáváno grid nabíjení v noci před večerním výkupem")
def test_fixed_nt_charge_after_yesterday_evening_peak(self) -> None:
"""Včerejší večerní peak nesmí zablokovat dnešní 0006 grid (per-day export okno)."""
base = datetime(2026, 5, 23, 20, 0, tzinfo=_PRAGUE)
slots: list[PlanningSlot] = []
for i in range(64):
t = base + timedelta(minutes=15 * i)
sell = 3.8 if t.hour >= 20 and t.date() == date(2026, 5, 23) else 2.9
if t.date() == date(2026, 5, 24) and t.hour >= 19:
sell = 3.7
slots.append(
_slot(
buy=3.088,
sell=sell,
load=200,
interval_start=t.astimezone(timezone.utc),
)
)
battery = _battery(charge_buf=1.3, uc_wh=12_500.0)
out = _select_charge_slots(
slots,
battery,
current_soc_wh=0.33 * battery.usable_capacity_wh,
purchase_pricing_mode="fixed",
)
may24_night = {
t
for t in out
if _prague_date(slots[t]) == date(2026, 5, 24)
and _prague_hour(slots[t]) < 6
}
self.assertGreater(len(may24_night), 0)
def test_fixed_allows_discharge_on_high_sell(self) -> None:
slots = [
_slot(buy=3.09, sell=1.0, hour_utc=10),

View File

@@ -304,24 +304,33 @@ begin
from _ems_plan_slot_wk wk
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;
-- První výkupní okno: sell nad min(buy) **téhož kalendářního dne** (Prague), ne globální
-- min(buy) zítra (NT) — jinak okno začne už ~15:30 a dnešní PM grid dostane 1 slot.
select min(wk.interval_start)
into v_export_window_start
from _ems_plan_slot_wk wk
where wk.sell_price > (
select coalesce(min(w2.buy_price), wk.buy_price) + v_degrad_czk_kwh
from _ems_plan_slot_wk w2
where (w2.interval_start at time zone 'Europe/Prague')::date
= (wk.interval_start at time zone 'Europe/Prague')::date
);
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
alter table _ems_plan_slot_wk
add column if not exists future_sell_lookahead numeric,
add column if not exists buy_min_next_n numeric,
add column if not exists store_score numeric,
add column if not exists allow_grid_charge boolean default false;
add column if not exists allow_grid_charge boolean default false,
add column if not exists export_window_start_at timestamptz;
-- První výkupní okno **per kalendářní den** (Prague). Globální min přes dny by
-- zablokoval NT grid nabíjení (včerejší večerní peak → dnešní 0006 už „po okně“).
update _ems_plan_slot_wk wk
set export_window_start_at = (
select min(wx.interval_start)
from _ems_plan_slot_wk wx
where (wx.interval_start at time zone 'Europe/Prague')::date
= (wk.interval_start at time zone 'Europe/Prague')::date
and wx.sell_price > (
select coalesce(min(w2.buy_price), wx.buy_price) + v_degrad_czk_kwh
from _ems_plan_slot_wk w2
where (w2.interval_start at time zone 'Europe/Prague')::date
= (wx.interval_start at time zone 'Europe/Prague')::date
)
);
select min(wk.export_window_start_at)
into v_export_window_start
from _ems_plan_slot_wk wk;
update _ems_plan_slot_wk wk
set
@@ -339,8 +348,8 @@ begin
where w2.slot_ord > wk.slot_ord
and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
and (
v_export_window_start is null
or w2.interval_start < v_export_window_start
wk.export_window_start_at is null
or w2.interval_start < wk.export_window_start_at
)
),
store_score =
@@ -431,8 +440,8 @@ begin
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start
when wk.export_window_start_at is not null
and wk.interval_start < wk.export_window_start_at
then 0
else 1
end,
@@ -479,8 +488,8 @@ begin
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start
when wk.export_window_start_at is not null
and wk.interval_start < wk.export_window_start_at
then 0
else 1
end,
@@ -522,8 +531,8 @@ begin
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start
when wk.export_window_start_at is not null
and wk.interval_start < wk.export_window_start_at
then 0
else 1
end,
@@ -567,8 +576,8 @@ begin
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start
when wk.export_window_start_at is not null
and wk.interval_start < wk.export_window_start_at
then 0
else 1
end,

View File

@@ -108,6 +108,18 @@ where pr.site_id = (select id from ems.site where code='BA81') and pr.status='ac
---
## 2026-05-24 (d) — BA81: grid jen 1 slot (globální export okno)
**Problém:** Run **15820** — mírné zlepšení (1× ~4,5 kW grid+bat o půlnoci), ale **00:4505:45 `allow_charge=false`**, max nabíjení pořád ~3,3 kW z FVE.
**Příčina:** `v_export_window_start` = **min přes celý horizont** (včerejší večerní sell 3,7 → čas ~22:15). Grid vrstva B řadí „před oknem“ vůči tomuto **jednomu** času → dnešní NT sloty (0006) už jsou „po okně“ a nedostanou `allow_grid_charge`.
**Oprava:** Sloupec **`export_window_start_at` per kalendářní den** (Prague); grid AM/PM i `buy_min_next_n` používají `wk.interval_start < wk.export_window_start_at`.
**Deploy:** `flyway migrate` (R__063) + replan.
---
## Šablona pro další záznamy
```markdown