Files
ems/CLAUDE.md
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

243 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md EMS Platform (Cursor Agent)
Čti před každou implementační změnou. Stručná orientace; detail v `docs/` a SQL v `db/`.
---
## 1. Co to je
Multi-site Energy Management System: optimalizuje FVE, baterii a flexibilní zátěž (EV, TČ) podle spotových cen OTE CZ a předpovědí; výstupy řídí zařízení (Modbus) a informuje Loxone jako exekutora. Referenční lokalita v seedu: `home-01` (Deye, baterie, 2× EV Teltonika, Samsung TČ).
---
## 2. Technologický stack
| Vrstva | Technologie |
|--------|-------------|
| DB | PostgreSQL 16 + TimescaleDB |
| Migrace | Flyway (`db/migration`, `db/routines`, `db/views`) |
| API | PostgREST (REST ze schématu `ems`) + FastAPI (logika, joby plán v docs) |
| Frontend | React + TypeScript + Vite (očekáváno u kořene / Docker); výběr lokality comboboxem (`SiteSelectionContext`, `GET /api/v1/me/sites`, persist `localStorage` `ems.selected_site_id`) |
| Pole / zařízení | Modbus TCP (`pymodbus`), HTTP (Loxone, případně API vozidel) |
| Solver | PuLP + HiGHS (`HiGHS_CMD`) |
| Runtime | Docker Compose |
| **Živá DB přes MCP (Cursor)** | Server ID **`user-postgres-ems`**, nástroj **`query`**, `{ "sql": "…" }` — viz **`docs/07-mcp-postgres-ems.md`** a pravidlo **`.cursor/rules/mcp-postgres-ems.mdc`** |
---
## 2b. MCP — živá EMS databáze (read-only)
Když uživatel napíše **„použij MCP“** nebo potřebuje **aktuální řádky z Postgresu** (plán, telemetrie, journal):
1. Zavolej MCP nástroj **`query`** na serveru **`user-postgres-ems`** s argumentem `{"sql": "<SELECT …>"}`.
2. **Neodmlouvej** bez pokusu (typ „nepřipojím se“, „MCP neexistuje“). Po chybě popiš **skutečnou** chybu a co zkontrolovat.
3. Kanonický popis, příklady a bezpečnost: **`docs/07-mcp-postgres-ems.md`**.
---
## 3. Adresářová struktura
| Cesta | Účel |
|-------|------|
| `CLAUDE.md`, `.env.example`, `docker-compose.yml` | Kořen: pravidla, env šablona, compose |
| `docs/` | Produktová a technická specifikace (overview, architektura, datový model, integrace) |
| `docs/04-modules/` | Modulové specifikace (ceny, forecast, spotřeba, TČ, telemetrie, řízení, plánování, režimy, EV) |
| `docs/loxone-integration.md` | Loxone watchdog, heartbeat, role exekutora |
| `docs/06-open-questions.md` | Nedokončené rozhodnutí doplňovat místo hádání |
| `docs/07-mcp-postgres-ems.md` | MCP read-only SQL na EMS DB (server `user-postgres-ems`, nástroj `query`) |
| `db/migration/` | Flyway versioned migrace `V00x__*.sql` (schéma, seed, alter) |
| `db/routines/` | Repeatable SQL: funkce `ems.fn_*` |
| `db/views/` | Repeatable SQL: view `ems.vw_*` |
| `backend/services/` | Python služby (v repozitáři zatím hlavně plánování) |
| `backend/services/planning/` | Moduly plánovače: `constants` (vč. všech ekonomických penalt), `types`, `forecast`, `db_io`, `heuristics`; `planning_engine.py` = solver + orchestrace + fasáda (re-export, importy beze změny) |
| `backend/tests/golden/` + `scripts/harness/` | Ekonomický regresní harness: golden replay gate (`test_golden_replay.py`), `extract_fixtures.py`, `economics_report.py`, `penalty_audit.py` — viz `scripts/harness/README.md`; **při změně plánovače musí projít golden gate** |
---
## 4. Pravidla NIKDY neporušovat
1. **15min logika pro plán/ceny/baseline/audit/forecast intervaly.** Časové řady v těchto doménách = 15min sloty. Telemetrie zařízení je 1min (hypertables) agregace do 15min přes SQL/job, ne ukládat „hodinové“ řádky jako primární plánovací záznam.
2. **Všechny doménové záznamy vázat na `site_id`** (telemetrie, plány, audit, konfigurace aktiv, session, …). Výjimka: `market_interval_price` je globální pro zdroj/trh; vazba na site je přes konfiguraci a view.
3. **Raw ceny ≠ efektivní ceny.** `ems.market_interval_price` = bez marží. Efektivní nákup/prodej jen přes `ems.vw_site_effective_price` (join na platnou `site_market_config`).
4. **Loxone = exekutor + autonomní fallback, ne optimalizátor.** Logika a plán v EMS. Watchdog v Loxone nesmí záviset na čtení DB (`site_heartbeat` je jen pro EMS UI/diagnostiku).
5. **FVE pole B (`controllable = false`, typicky ongrid GEN) žádný curtailment.** Curtailment jen pole A (Deye). Solver smí omezovat jen `pv_a`; pole B může mít zelený bonus na `asset_pv_array` (`green_bonus_*`), audit `pv_b_production_wh` / `green_bonus_czk`.
6. **Záporná prodejní cena → `grid_export == 0`** v LP (hard constraint kde zapnuté): buď **`deye_gen_microinverter_cutoff_enabled`** na `deye-main`, nebo **`ems.site_grid_connection.block_export_on_negative_sell`** (default false). **home-01** kvůli neriťitelnému PV B často **bez** druhého přepínače — přebytek pole B nesmí dělat PL infeasible; **KV1** (bez pole B / fixní nákup) migrace **V074** nastavuje `block_export_on_negative_sell = true`.
7. **Záporná nákupní cena → omezit import** na realistický horní strop (viz `solve_dispatch` v `planning_engine.py` nesmí „nekonečný“ import).
8. **PuLP + HiGHS** pro dispatch; žádný návrat k greedy `fn_plan_day` jako primárnímu řešení (SQL wrapper může zůstat pro uložení výsledků dle docs). **Ekonomika slotů:** masky + guardy v `solve_dispatch` — viz `docs/04-modules/planning.md`. **Arbitráž baterie:** neúčtovat `buy[t]`/`sell[t]` ve stejném 15min slotu jako nákup/prodej téže kWh; `min(buy)` horizontu ≠ cena nabití (home-01 nabíjí hodiny, ne jednu čtvrthodinu). Povinné: `docs/04-modules/planning-arbitrage-accounting.md`.
9. **Zelený bonus je na `asset_pv_array`** (sloupce `green_bonus_*`), **nikdy** v `site_market_config`. Výpočet přes `fn_green_bonus_revenue()`. Bonus se nepočítá v solveru pouze v audit_filler (`fn_fill_audit_interval`).
10. **Deye Modbus: čtení i zápis** (setpointy). RS485→Waveshare→TCP, knihovna `pymodbus`.
11. **Přepínání provozního režimu** přes DB API / `ems.fn_set_mode` držet konzistenci s `operating_mode_def` a Loxone `loxone_mode_value`.
### SQL-first a read-model (Python jen tenká orchestrace)
Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a stabilní čtecí rozhraní patří do **PostgreSQL** (`ems.fn_*`, případně **`ems.vw_*`**). Python (FastAPI, joby) volá DB; neskladá vlastní dotazy nad schématem mimo výjimky níže.
**Formát SQL v repu (`db/migration`, `db/routines`, `db/views`):** odsazení **2 mezery** na úroveň vnoření; **rezervovaná klíčová slova PostgreSQL vždy malými písmeny** (`create table`, `select`, `where`, `references`, …). Identifikátory (`ems.*`, sloupce) **`snake_case`**; typy v deklaracích též malými (`int`, `text`, `timestamptz`, `jsonb`). Nový / upravený SQL v tomto stylu — nesmí se objevovat verzované migrace psané „ALL CAPS keywords“.
- **Preferuj:** novou nebo rozšířenou **`ems.fn_*(…)`** s jasnými parametry; potřebuješ často stejné sloupce z více tabulek → **`ems.vw_*`** (view zapouzdřuje joiny a strukturu DB; z Pythonu je `SELECT … FROM ems.vw_*` v pořádku).
- **Nechtěné:** skládání dotazů v Pythonu (**vlastní JOIN / WITH / poddotazy** nad `ems.*` tabulkami). Místo toho funkce nebo view v `db/routines/` / `db/views/` + jedno volání z aplikace.
- **Jediné SQL v `backend/services/*.py` a `backend/app/routers/*.py`:** `SELECT 1` / `EXISTS`; **`select ems.fn_*(…)`**; **`SELECT … FROM ems.vw_*`** (read přes view); žádné jiné ad-hoc **`SELECT`/`INSERT`/`UPDATE`**. IO (Modbus, HTTP); **PuLP**; orchestrace scheduleru.
- **Health a Loxone po změně režimu:** `fn_health_summary`, `fn_health_detailed_db`, `fn_vw_site_directory_active`, `fn_site_economics_yesterday_notification`, `fn_site_mode_loxone_bundle` v repeatable `db/routines/R__073_fn_health_site_jobs_mode_bundle.sql`; FastAPI je v [`app/main.py`](backend/app/main.py) + joby v [`app/lifespan.py`](backend/app/lifespan.py).
### Provozní režimy (operating_mode)
- Pět hodnot `mode_code` v `ems.site_operating_mode`: **AUTO**, **SELF_SUSTAIN**, **CHARGE_CHEAP**, **PRESERVE**, **MANUAL**.
- Režim se načítá v `planning_engine._load_site_context()`; **dodatečné LP constraints** podle režimu jsou v **`solve_dispatch()`** (žádný export / limit importu / zákaz nabíjení nebo vybíjení baterie podle módu).
- **Fyzické režimy Deye** (PASSIVE / SELL / CHARGE) se odvozují v `exporter_monolith.get_deye_mode` a zapisují v `write_inverter_setpoints`.
- **`lock_battery=True`** u `ControlSetpoints` (PRESERVE): registry **108/109 = 0** Deye baterii nepoužívá. Výjimka oproti obecnému pravidlu max A ve PASSIVE/SELL.
12. **`forecast_pv_run` a `forecast_pv_interval` se NESMÍ mazat** historické běhy zůstávají v DB pro tracking přesnosti (`forecast_accuracy`, `fn_fill_forecast_accuracy`).
13. **Endpoint `GET …/forecast/pv`** vrací `DISTINCT ON (interval_start, pv_array_id)` seřazené podle nejnovějšího `forecast_pv_run.created_at`, aby UI nemělo duplikáty slotů; plná historie běhů zůstává v tabulkách.
13a. **PV delta kalibrace:** `GET …/forecast/pv-delta-profile` vrací JSON z `fn_pv_forecast_delta_profile`; `GET …/configuration` obsahuje `pv_forecast_calibration` z `ems.site_pv_forecast_calibration`; `PATCH …/configuration/pv-forecast-calibration` mění cutoff / policy / přepsání parametrů delty. **Referenční dny** špičkové produkce zpětně: tabulka **`ems.site_pv_forecast_reference_day`** (V076) + volitelně sloupec **`reference_day_weight_mult`** v kalibraci — v `fn_pv_forecast_delta_profile` zvednou váhu řádků `forecast_accuracy` těchto kalendářních dní (datum ve `site.timezone` jako u slotů); doplňovat lze **`ems.fn_pv_forecast_sync_reference_days`**. Provozní mazání uložené predikce za den (hranice **Europe/Prague**, ne TZ site): **`ems.fn_delete_forecast_pv_prague_calendar_day`**. Telemetrie `telemetry_inverter.is_export_limited` / `pv_derating_flags` (V058) řídí vyloučení slotu z učení v `fn_fill_forecast_accuracy` (`telemetry_derating`); `telemetry_collector` je plní čtením Deye reg **145** a **179** při poll střídače.
14. **Příchod a odjezd EV** detekuje `telemetry_collector` z telemetrie nabíječky: přechod `available``preparing` / `charging` (resp. jakýkoli stav ≠ `available`) znamená příjezd; přechod na `available` uzavře `ev_session`. Tabulka `ev_arrival_stats` se při příjezdu doplňuje přes `fn_update_ev_arrival_stats` a **nemá se mazat** (dlouhodobá historická statistika).
15. **Bazální spotřeba** = `load_power_w` minus řízené zátěže (součet EV z `telemetry_ev_charger`, TČ z `telemetry_heat_pump`). Tabulka `consumption_baseline_stats` se plní denně (APScheduler 00:30) přes `fn_update_baseline_stats`; **bez EMA „ocasu“** přepočítáš smaž+hromadný update přes **`ems.fn_rebuild_consumption_baseline_stats(site_id, lookback)`** (`site_id NULL` → všechny lokality). **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`.
16. **Dynamický horizont plánování (jen OTE):** konec okna z **`ems.fn_planning_horizon_end(site_id, horizon_start)`** (`min` posledního OTE konce a `start + 36 h`, NULL pokud je známé OTE kratší než **1 h** od startu rolling se přeskočí; denní plán při NULL použije **1 h** fallback v Pythonu). Strop a práh měnit v SQL (defaultní argumenty funkce / repeatable migrace), ne přes env. Solver používá **výhradně** sloty s efektivní cenou z `vw_site_effective_price` (žádná predikce v LP). Účelová funkce má **terminal SoC shadow price**: `(průměr buy v prvních 24 h slotů × planner_terminal_soc_value_factor / 1000) × soc[T1]` (Kč; SoC v Wh), kde **`planner_terminal_soc_value_factor`** je **`ems.asset_battery.planner_terminal_soc_value_factor`** načtené přes **`ems.fn_planning_site_context`** (žádný skrytý faktor v Pythonu). **Fázované SoC v okně `sell < 0` (v32):** `planner_neg_sell_prep_soc_percent`, `planner_neg_sell_full_soc_tail_slots`, `planner_neg_sell_vent_min_sell_czk_kwh` na **`asset_battery`**; curtail A → reg 340, plná baterie = solar sell off bez zápisu 340. `market_price_stats` / `fn_get_predicted_price` zůstávají pro statistiky; detail: `docs/04-modules/planning.md`, `docs/04-modules/planning-extended-horizon.md`.
17. **Modbus zápis = journal.** Každý zápis do zařízení přes control exporter se loguje do `ems.modbus_command`. **Verifikační job** běží každé **2 minuty** a ověřuje nedávno zápis (`written` → čtení registru). Při **mismatch** po max. **3** pokusech o zápis → u běžných registrů přepnutí na **SELF_SUSTAIN** (`run_fn_set_mode_with_discord``fn_set_mode`, `activated_by` = `system:mismatch`) + **Discord** při skutečné změně režimu. **Výjimka:** souvislý blok Deye **6264** (čas) → po 3 neúspěšných ověřeních **bez** změny režimu, kritický **Discord** (`notify_modbus_clock_verify_exhausted`). **Obecně:** při jakékoli změně `mode_code` z Pythonu (`POST /api/v1/sites/{id}/mode`, mismatch → SELF_SUSTAIN, `fn_expire_modes`) lze Discord zapnout přes `DISCORD_WEBHOOK_URL`. Detail: `docs/04-modules/modbus-command-journal.md`.
18. **Deye zápis registrů 60499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzický režim Deye** (`PASSIVE` / `CHARGE` / `SELL`): **výhradně** `get_deye_mode` v `exporter_monolith.py` (bez wattových prahů: **SELL** při `battery_w` < 0 a `grid_setpoint_w` < 0; **CHARGE** při obou > 0; jinak **PASSIVE**). **PASSIVE (ZERO, AUTO):** **`export_mode=PV_SURPLUS`** → **108=0**, **109=max**, **142**=`deye_zero_export_mode` (ne selling first); jinak **108/109** dle `deye_battery_charge_discharge_amps` / `_deye_zero_export_amps_for_passive` (import bez vybíjení → **109=0**); **TOU SOC** (reg 166+): **PASSIVE** = **`min_soc_percent`**, **CHARGE** = **`max_soc_percent`** (clamp 10100 z DB), **SELL** = **`reserve_soc_percent`** (`_deye_passive_tou_battery_soc_pct`, `_deye_tou_params`). **SELL:** reg **108** EMS **nezapisuje** (selling first = **142**), **109**=max, **178**=32 (peak shaving off), **143** omezeno podle `|grid_setpoint_w|`; **142/145/TOU** jako v `write_inverter_setpoints`. **Reg 340** (*max solar power*, W): jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (strop z `deye_reg340_max_solar_w`, typ. 32k home-01 / 65k jinde, ne součet Wp; min `deye_reg340_min_solar_w`, home-01 400); hodnota z plánu / curtailu (AUTO). **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. **PRESERVE:** `lock_battery` → 108/109=0. **Čas 6264**, bloky TOU **12** vs **36**, verify, Discord: beze změny oproti dřívějšímu chování — plný popis **`docs/04-modules/modbus-registers.md`** a **`docs/04-modules/operating-modes.md`**.
19. **HARD LIMIT exportu na fakturačním elektroměru — NIKDY nepřekročit.** Překročení rezervovaného exportního výkonu (home-01: 13.5 kW) byť o desetiny kW = smluvní pokuta v řádu desítek tisíc Kč za kW. Jediný bezpečný invariant: **reg 143 (limit na svorkách střídače) <= max_export_power_w (limit ulice) VŽDY** — v nejhorším případě (spotřeba mezi střídačem a CT odpadne) je ulice rovna svorkám. **ZAKÁZÁNO** jakékoli feed-forward navyšování terminálového limitu o měřenou spotřebu (výpadek spotřeby = přestřelení ulice). Vyšší vytěžení smí přinést jedině interní regulace střídače proti CT (firmware smyčka), nikdy náš software s 1min telemetrií a 15min ticky.
20. **Baterie export v LP:** V `solve_dispatch` binárka `z_export[t]`: pokud `grid_export` v daném slotu **≥ 1** W, platí koncové `soc[t] ≥ arb_base_wh` (ekonomická rezerva z DB, ne časová řada `arb_floor_series`). Bez exportu může plán jít k `min_soc_percent` (provozní podlaha; u paralelních packů často 1112 %, migrace V029 + komentář sloupce).
---
## 5. Schéma `ems` tabulky (jedna věta)
| Tabulka | Popis |
|---------|--------|
| `site` | Lokalita (časová zóna, GPS, aktivita). |
| `site_endpoint` | Endpointy: Modbus, Loxone HTTP, atd. |
| `site_market_config` | Marže, režimy cenění; časová platnost (zelený bonus není zde viz `asset_pv_array`). |
| `site_grid_connection` | Limity import/export, **block_export_on_negative_sell** (LP při záporném sell), no_export, rezervovaný výkon. |
| `site_override` | Manuální přepisy nad plánem (JSON + platnost). |
| `site_operating_mode` | Aktuální provozní režim na site (1 řádek/site). |
| `site_operating_mode_log` | Historie přepnutí režimů. |
| `site_heartbeat` | Poslední EMS heartbeat pro dashboard (ne pro Loxone watchdog). |
| `operating_mode_def` | Číselník režimů (baterie/síť/EV/TČ, hodnota pro Loxone). |
| `asset_inverter` | Střídač (výkony, endpoint, zda řiditelný). |
| `asset_battery` | Baterie vázaná na střídač (SoC limity, účinnosti, degradace). |
| `asset_pv_array` | FVE pole (Wp, orientace, curtailable vs ne; volitelně `green_bonus_*` pro dotované pole). |
| `asset_ev_charger` | Nabíječka EV (výkony, fáze, endpoint). |
| `asset_heat_pump` | TČ (výkon, COP ref, limity běhu, TUV parametry). |
| `asset_vehicle` | Vozidlo (kapacita, max AC výkon, default target SoC/deadline). |
| `market_interval_price` | Raw spot OTE (15min), bez marží. |
| `telemetry_inverter` | 1min telemetrie střídače (Timescale); volitelně `is_export_limited`, `pv_derating_flags` pro vyloučení slotu z učení delty. |
| `telemetry_ev_charger` | 1min telemetrie nabíječky (Timescale). |
| `telemetry_heat_pump` | 1min telemetrie TČ (Timescale). |
| `forecast_pv_run` | Metadata běhu predikce FVE. |
| `forecast_pv_interval` | Predikovaný výkon FVE po 15min (Timescale). |
| `forecast_accuracy` | Řádky přesnosti predikce vs telemetrie po 15min (per run); doplňuje `fn_fill_forecast_accuracy`. |
| `site_pv_forecast_calibration` | Per site: cutoff učení delty, policy škrcení, přepsání parametrů `fn_pv_forecast_delta_profile`. |
| `forecast_weather_interval` | Počasí 15min pro site (Timescale). |
| `forecast_correction_log` | Log korekcí forecastu vs skutečnost při rolling replanu. |
| `planning_run` | Jeden běh plánovače (daily/rolling/manual, stav, parametry solveru). |
| `planning_interval` | Výstup solveru po 15min (baterie, síť, EV, TČ, curtailment A). |
| `audit_interval` | Skutečnost vs plán po 15min (náklady, odchylky, bonus pole B). |
| `consumption_baseline_interval` | Bazální spotřeba actual/forecast 15min (Timescale). |
| `consumption_baseline_stats` | Historické průměry bazálu per DOW+hodina (EMA z telemetrie); vstup solveru. |
| `market_price_stats` | Historické průměry raw OTE ceny per DOW+hodina; predikce cen za horizont OTE (`fn_get_predicted_price`). |
| `tuv_usage_stats` | Průměrná změna teploty TUV zásobníku per DOW+hodina (telemetrie TČ); vstup TUV look-ahead ve solveru. |
| `ev_session` | Nabíjecí session na WB (deadline, energie, náklady). |
| `ev_arrival_stats` | Agregované počty příjezdů EV podle dne v týdnu a hodiny (Europe/Prague); plní se z detekce příjezdu v telemetrii. |
| `modbus_command` | Journal Modbus zápisů (pending → written → verified / mismatch / failed); retry a vazba na `planning_run`; u Deye exportu `deye_physical_mode` (PASSIVE/SELL/CHARGE). |
| `signal_def` | Katalog odchozích signálů (kód, typ hodnoty); seed `EXPORT_BAN_ACTIVE`. |
| `signal_route` | Mapování signál → cíl (`loxone_vi`, `http_rest`) per site + `endpoint_id` + volitelný `route_config_json` / `verify_config_json`. |
| `signal_outbound_journal` | Journal HTTP odeslání signálů (`queued``sent``verified` / retry / `abandoned`). |
| `signal_state` | Poslední požadovaná / odeslaná / ověřená hodnota na cíli (idempotence). |
| `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. |
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_delete_forecast_pv_prague_calendar_day`, `fn_pv_forecast_sync_reference_days`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_rebuild_consumption_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_ev_session_defaults` (kaskáda ev_weekly_requirement → forecast → defaulty), `fn_ev_session_planning_json` (EV session pro LP; nevyřazuje při needed=0), `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_modbus_device_state_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`.
---
## 6. Periodické úlohy backendu (APScheduler / smyčky)
Specifikace z `docs/02-architecture.md`, modulových docs a komentářů v `planning_engine.py`. **V gitu je zatím rozpracovaný backend** joby mají být v `backend/app/main.py` (zatím často chybí).
| Úloha | Frekvence | Poznámka |
|-------|-----------|----------|
| `telemetry_collector` | každých **60 s** | Smyčka polling Modbus (Deye, EV×2, TČ) viz `docs/04-modules/telemetry.md` |
| `price_importer` (scheduler) | **13:30 / 14:00 / 00:05** | Jeden globální zápis do `market_interval_price` za tick (ne cyklus per site); po importu obnova predikce záporných cen pro každou aktivní site. Viz `docs/04-modules/market-prices.md` |
| `forecast_service` | **14:30** + **06:00** denně | `docs/04-modules/forecast.md` |
| `run_daily_plan` | **15:00** denně | `backend/services/planning_engine.py` + `ems.fn_planning_horizon_end` (dynamický OTE horizont, terminal SoC) |
| `run_rolling_replan` | **každých 15 min** (`*/15`) | `planning_engine.py` přepočet od aktuálního slotu |
| `control_exporter` | **každých 15 min** (slot boundary) | `docs/04-modules/control.md` |
| `verify_modbus` | **každé 2 min** | Ověření `modbus_command` ve stavu `written` (posledních 10 min); viz `docs/04-modules/modbus-command-journal.md` |
| `signal_outbound_send` / `signal_outbound_verify` | **každých 15 s** | `services/signal_service.py` — odeslání fronty `signal_outbound_journal` a readback verify (Loxone / HTTP REST). |
| `audit_filler` / `fn_fill_audit_interval` | **každých 15 min** | `docs/02-architecture.md`, DB `fn_fill_audit_interval` |
| `forecast_accuracy` / `fn_fill_forecast_accuracy` | **každých 15 min** (min. 2,17,32,47) | Po audit filleru; doplní actual z telemetrie do `forecast_accuracy` |
| `fn_update_baseline_stats` | **00:30** denně | Aktualizace `consumption_baseline_stats` z telemetrie (30d lookback) |
| `fn_update_market_price_stats` | **14:45** denně | Po importu OTE a forecastu; `market_price_stats` (90d lookback) |
| `fn_update_tuv_usage_stats` | **00:45** denně | Po baseline jobu; `tuv_usage_stats` (30d lookback) |
---
## 7. Kde hledat co
| Chci… | Kam |
|-------|-----|
| Pochopit systém end-to-end | `docs/01-overview.md`, `docs/02-architecture.md` |
| Tabulky, vazby, jednotky | `docs/03-data-model.md` |
| OTE ceny, marže, efektivní cena | `docs/04-modules/market-prices.md`, `db/views/R__061_vw_site_effective_price.sql`, `backend/services/price_importer.py` |
| Multi-site UI (combobox), seznam aktivních lokalit | `GET /api/v1/me/sites` v `backend/app/main.py`, `frontend/src/context/SiteSelectionContext.tsx`, `useSiteStatus` (filtr `vw_site_status`) |
| FVE forecast, počasí | `docs/04-modules/forecast.md` |
| Bazální spotřeba | `docs/04-modules/consumption.md` |
| TČ, COP, TUV | `docs/04-modules/heat-pump.md`, `db/routines/R__005_fn_cop_estimate.sql` |
| Modbus, telemetrie, agregace | `docs/04-modules/telemetry.md` |
| Dashboard přehled 15min grafy slotů, SoC vs. živá telemetrie | `docs/04-modules/telemetry.md` (CA `telemetry_inverter_15m`, view `vw_telemetry_15m_7d`), `frontend/src/hooks/useDashboardData.ts`, `frontend/src/components/charts/SocTuvChart.tsx` |
| Modbus journal, verifikace, Discord | `docs/04-modules/modbus-command-journal.md` |
| Deye registry (FC 0x10, 108/109/141/142/178/143/145/340) | `docs/04-modules/modbus-registers.md` |
| Export setpointů, Loxone HTTP | `docs/04-modules/control.md`, `docs/loxone-integration.md` |
| LP solver, rolling replan, korekce FVE, dynamický OTE horizont | `docs/04-modules/planning.md`, `docs/04-modules/planning-extended-horizon.md`, `docs/planning-changelog.md`, `planning_engine.py` |
| Rozpočet nabíjecích slotů (Wh × ceny × forecast; plánováno) | `docs/04-modules/planning-charge-slot-budget.md` — náhrada v58 + pre-neg cushion |
| Záporný výkup, bod T, termika, bazén (home-01 strategie) | `docs/04-modules/planning-neg-sell-strategy.md` |
| Arbitráž baterie (mezi sloty ≠ buy/sell v jednom 15min) | `docs/04-modules/planning-arbitrage-accounting.md` |
| Provozní režimy AUTO / SELF_SUSTAIN / … | `docs/04-modules/operating-modes.md`, `db/migration/V004__operating_modes.sql`, `db/routines/R__044_fn_set_mode.sql` |
| EV, session, deadline charging | `docs/04-modules/ev-charging.md`, `db/migration/V006__vehicles.sql` |
| Curtailment A, zelený bonus B | `db/migration/V005__planning_curtailment.sql` |
| Rolling plán, forecast log | `db/migration/V007__rolling_replanning.sql` |
| Audit 15min | `db/routines/R__019_fn_fill_audit_interval.sql`, `docs/04-modules/telemetry.md` |
| Nové sloupce / tabulky | nový `db/migration/V00x__*.sql` + případně `db/routines` / `db/views` |
| JSONB read-model (`fn_*`, `fetch_json`) | `docs/02-architecture.md` sekce Read-model JSONB, `app/db_json.py` |
| Self-hosted deploy (Gitea, Caddy, `/opt/ems-deploy`) | `docs/deployment-self-hosted.md`, `deploy/deploy.sh` |
| Reset DB / restore z dumpu (Docker volume, Timescale) | `docs/database-reset-and-restore.md`, `scripts/import_ems_db.sh` |
| Nespecifikované chování | `docs/06-open-questions.md` (přidat otázku, neimpl. naslepo) |
| **MCP read-only SQL na EMS DB** | **`docs/07-mcp-postgres-ems.md`** — server ID **`user-postgres-ems`**, nástroj **`query`**, `{"sql":"…"}`. Pravidlo **`.cursor/rules/mcp-postgres-ems.mdc`**. |
| **Refaktor „Čistý plánovač“ (fáze, stav, nasazení v2)** | **`docs/refactor-clean-planner.md`**; verze enginu v1/v2 + env flagy: `docs/04-modules/planning.md` (sekce Verze enginu); changelog 2026-06-11 |
| **Čisté jádro plánovače v2** | `backend/services/planning/solver_v2.py`, testy `backend/tests/test_solver_v2.py`, eval `scripts/harness/solver_v2_eval.py` |
| **Delta-triáž neekonomického chování (agent skill)** | **`.claude/skills/ems-delta-triage/`** — realita vs plán vs shadow peer vs oracle, verdikt s Kč |
| **Vysvětlení plánu (agent skill)** | **`.cursor/skills/ems-plan-explain/`** — `fn_plan_explain_bundle`, sloty, proč nabíjí/exportuje |
| **Triáž bugů plánovače (agent skill)** | **`.cursor/skills/ems-planner-bug-triage/`** — Infeasible/relaxed solve, večerní export, neg den, BA81/KV1; MCP SQL v `reference.md` |
---
## Konvence (krátce)
- **Dokumentace 1:1 s implementací — POVINNÉ u každé změny.** Každý commit, který mění chování, nese i aktualizaci docs ve STEJNÉM commitu (nebo bezprostředně navazujícím): plánovač → `docs/planning-changelog.md` (formát: datum · problém · příčina/mechanismus · soubory · ověření) + dotčený `docs/04-modules/*.md`; nová zařízení/registry → modulový doc (vzor `modbus-registers-teltocharge.md`); deploy/CI → `docs/deployment-self-hosted.md`; nové tabulky/sloupce → `comment on` v migraci + zmínka v `docs/03-data-model.md` u větších celků; env flagy a defaulty → místo, kde jsou popsané (např. `planning.md` sekce Verze enginu). Zastaralé tvrzení v docs = bug se stejnou prioritou jako bug v kódu.
- Python: `snake_case`, type hints, Pydantic pro API modely.
- SQL: viz také odstavec **Formát SQL** u sekce SQL-first výše — **2 mezery** odsazení, **klíčová slova malými písmeny**, `snake_case` identifikátory, explicitní FK; Flyway pořadí `V###__` / repeatable `R__NNN_*.sql` (třímístný prefix = pořadí závislostí mezi fn/vw).
- **PG funkce: žádné overloady — název `ems.fn_*` je vždy unikátní** (nikdy dvě funkce stejného jména s jinými parametry). Díky tomu se `drop function if exists` i `comment on function` píší **VŽDY bez závorky s parametry** — odkaz přes signaturu se rozbije při každé změně parametrů (42883 shodil deploy 2026-06-12, R__018: comment mířil na starou signaturu po přidání `p_force`).
- Timescale **continuous aggregate** (CA): komentář k objektu CA je **`COMMENT ON VIEW`**, ne `COMMENT ON MATERIALIZED VIEW` (PG hlásí 42809). Viz `.cursor/rules/timescale-continuous-aggregate.mdc`.
- Výkon **W**, energie **Wh**, ceny **Kč/kWh**; čas v DB **`TIMESTAMPTZ` (UTC)**.
- NIKDY neupravuj existující V__ migrační soubory po jejich aplikaci na DB.
- Pokud je potřeba opravit chybu ve verzované migraci, vytvoř novou V{N+1} migraci.
- **Vývojová kadence (slabý server):** běžná práce na větvi **`dev`** (push = CI validace BEZ deploye); do `main` merge **1×/den v okně ~16:3017:00** nebo při milníku (ne těsně před 15:00 — daily plán; OTE importy 13:2514:00). Deploy zastavuje backend na ~10 min (vynechané rolling ticky kryje Loxone fallback). Hotfix smí na main okamžitě.
- Deploy: `flyway validate` před `migrate` ([`deploy/deploy.sh`](deploy/deploy.sh)). Lokálně `./scripts/flyway_validate_local.sh`; CI viz [`docs/deployment-self-hosted.md`](docs/deployment-self-hosted.md) a `scripts/ci_check_migration_immutability.sh`.