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>
67 lines
2.4 KiB
Python
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()
|