Files
ems/backend/tests/test_ev_session_parse.py
Dusan Vojacek d81a150014 fix(planner): EV session viditelna i bez deadline / nad targetem (BUG2)
Zivy incident home-01: aktivni plan mel ev_sessions:0, ac session bezela
(target 70 %). Planovac neviděl ~6 kW zatez auta a spatne rozvrhl baterii
(zbytecny vecerni import).

Root cause (dve pasti):
- fn_planning_site_context vracela session jako null, kdyz needed_wh=0
  (auto nad targetem) i kdyz target_deadline is null.
- _ev_session_from_json (Python) zahazovala session bez deadline.

Fix:
- R__038 fn_ev_session_planning_json: session se vyradi (null) JEN bez tvrdych
  dat (kapacita vozidla / soc_at_connect). target_deadline smi byt NULL --
  solver hard deadline constraint aplikuje jen pri needed>0; oportunisticka
  vrstva bezi i bez deadline. Auto nad targetem zustava v planu jako znama
  zatez i s headroomem k levnemu doplneni. R__039 vola helper (deduplikace
  dvou inline poddotazu, SQL-first).
- _ev_session_from_json si NULL deadline ponecha (energy_needed_wh default 0).
- testy test_ev_session_parse.py; docs ev-charging + planning-changelog;
  CLAUDE.md funkce.

Navrh agresivnejsiho oportunistickeho algoritmu (P50 levnych oken z
market_price_stats misto konstanty 1 Kc/kWh) -- NEnasazeno, k rozhodnuti,
sepsano v docs/04-modules/planning.md (EV oportunismus); riziko regrese
golden ekonomiky, nutny EV fixture + eval.

Overeni: pytest -q 362 passed; golden replay gate 7 passed; solver_v2_eval
beze zmeny (fixtures bez EV session).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:03:27 +02:00

67 lines
2.4 KiB
Python

"""Parser EV session z fn_planning_site_context (_ev_session_from_json).
Bug 2026-06-13: session BEZ deadline (auto nad targetem / bez cíle) se v
parseru zahazovala (None), takže plánovač neviděl zátěž auta ani oportunismus.
Oprava: session bez deadline zůstává objektem s energy_needed_wh=0 a headroom.
"""
import unittest
from services.planning.db_io import _ev_session_from_json
class EvSessionParseTests(unittest.TestCase):
def test_none_and_empty_return_none(self) -> None:
self.assertIsNone(_ev_session_from_json(None))
self.assertIsNone(_ev_session_from_json([]))
self.assertIsNone(_ev_session_from_json(123))
def test_session_without_deadline_kept_for_opportunism(self) -> None:
sess = _ev_session_from_json(
{
"target_deadline": None,
"energy_needed_wh": 0,
"headroom_wh": 18000.0,
"opportunistic_value_czk_kwh": 1.0,
}
)
self.assertIsNotNone(sess)
assert sess is not None
self.assertIsNone(sess.target_deadline)
self.assertEqual(sess.energy_needed_wh, 0.0)
self.assertEqual(sess.headroom_wh, 18000.0)
self.assertEqual(sess.opportunistic_value_czk_kwh, 1.0)
def test_session_with_deadline_and_need(self) -> None:
sess = _ev_session_from_json(
{
"target_deadline": "2026-06-14T05:00:00+00:00",
"energy_needed_wh": 12000.0,
"headroom_wh": 6000.0,
"opportunistic_value_czk_kwh": 1.0,
}
)
assert sess is not None
self.assertIsNotNone(sess.target_deadline)
self.assertEqual(sess.energy_needed_wh, 12000.0)
def test_missing_needed_defaults_zero(self) -> None:
sess = _ev_session_from_json(
{"target_deadline": None, "headroom_wh": 1000.0}
)
assert sess is not None
self.assertEqual(sess.energy_needed_wh, 0.0)
self.assertEqual(sess.opportunistic_value_czk_kwh, 0.0)
def test_json_string_payload(self) -> None:
sess = _ev_session_from_json(
'{"target_deadline": null, "energy_needed_wh": 0, '
'"headroom_wh": 5000, "opportunistic_value_czk_kwh": 1.0}'
)
assert sess is not None
self.assertEqual(sess.headroom_wh, 5000.0)
if __name__ == "__main__":
unittest.main()