192 lines
18 KiB
Markdown
192 lines
18 KiB
Markdown
# 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 |
|
||
|
||
---
|
||
|
||
## 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í |
|
||
| `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í) |
|
||
|
||
---
|
||
|
||
## 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).
|
||
|
||
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).
|
||
|
||
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`.
|
||
|
||
### 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 `control_exporter.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.
|
||
|
||
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`. **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`.
|
||
|
||
16. **Rozšířený horizont plánování (96h):** denní plán pokrývá **96h** od začátku aktuálního 15min slotu. Sloty v prvních **36h** používají přesné efektivní ceny z `vw_site_effective_price` (OTE). Sloty **36–96h** doplňuje **predikovaná cena** z `market_price_stats` přes `fn_get_predicted_price` (prodejní strana hrubý faktor 0,85 vs. nákupní predikce). V účelové funkci LP se uplatní **váhy nejistoty** podle vzdálenosti od začátku okna: **1,0** (0–36h), **0,7** (36–72h), **0,4** (72–96h). Statistiky cen plní `fn_update_market_price_stats` (job 14:45), TUV delta `fn_update_tuv_usage_stats` (job 00:45). Detail: `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 **62–64** (č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ů 60–499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzické režimy střídače jsou tři:** **PASSIVE**, **SELL**, **CHARGE** (mapování z plánu / politik EMS v `control_exporter.get_deye_mode`). V **PASSIVE** a **SELL** jsou reg **108** / **109** obvykle na **maximum z DB** (**výjimka PRESERVE:** `lock_battery=True` → **0 / 0**). Omezování pod maximum jinak brání Deye reagovat na nepředvídatelnou spotřebu a přebytky FVE. **Řízení:** time points – blok **1** = začátek **aktuálního** 15min slotu + plán pro tento slot, blok **2** = začátek **následujícího** slotu + plán pro něj (`current_slot_hhmm` / `next_slot_hhmm`); bloky **3–6** neaktivní **2355** (ne 23:59 kvůli firmware), zápis **nejednou častěji než 1× denně** (Europe/Prague) + při změně podpisu (`deye_tou_inactive_signature`: `HHMM|min_soc|reserve_soc|tp_discharge_w`, V028 meta + V029 komentář); **reg 166+** u TP: **SELL** = `reserve_soc_percent`, **PASSIVE** / řádky **3–6** = `min_soc_percent`. **108** / **109** / **141** (0) / **142** (0 = selling first jen ve **SELL**, jinak 1) / **178** (pevně **32** ve **SELL**, **48** v **PASSIVE** a **CHARGE** – bez read-modify-write) / **143** (export limit W z DB) z **aktuálního** setpointu. **Reg 191** EMS **nezapisuje**. **Čas 62–64:** před zařazením do fronty **čtení** 62–64; zápis jen při driftu **> 60 s**, nebo **NULL** `deye_last_system_time_sync_at`, nebo uplynulých **24 h** od posledního **ověřeného** syncu (`deye_last_system_time_sync_at` / `deye_last_system_time_sync_minute` se doplňují až po **úspěšné toleranční verifikaci** v `_verify_deye_clock_command_run`, ne po samotném zápisu); při chybě čtení se čas zapisuje; reg **64** se zapisuje s **sekundami 0**; verifikace journalu pro souvislý blok 62–64 je **toleranční** (odchylka dekódovaného času až **120 s**); po 3 neúspěšných ověřeních **bez** přepnutí do SELF_SUSTAIN (jen Discord). **SELL:** `grid_setpoint_w` < −200. **CHARGE:** `battery_w` > 500 a `grid_setpoint_w` > 200. **PASSIVE:** ostatní (včetně `battery_w=None` u SELF_SUSTAIN → plné limity 108/109). Detail: `docs/04-modules/modbus-registers.md`, režimy: `docs/04-modules/operating-modes.md`.
|
||
|
||
19. **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 11–12 %, 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, 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). |
|
||
| `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`. |
|
||
| `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). |
|
||
| `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_latest_telemetry`, `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_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_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`.
|
||
|
||
---
|
||
|
||
## 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` (horizont **96 h**, váhy slotů 1,0 / 0,7 / 0,4) |
|
||
| `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` |
|
||
| `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__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__fn_cop_estimate.sql` |
|
||
| Modbus, telemetrie, agregace | `docs/04-modules/telemetry.md` |
|
||
| Modbus journal, verifikace, Discord | `docs/04-modules/modbus-command-journal.md` |
|
||
| Deye registry (FC 0x10, 108/109/141/142/178/143) | `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, horizont 96h | `docs/04-modules/planning.md`, `docs/04-modules/planning-extended-horizon.md`, `planning_engine.py` |
|
||
| Provozní režimy AUTO / SELF_SUSTAIN / … | `docs/04-modules/operating-modes.md`, `db/migration/V004__operating_modes.sql`, `R__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__fn_fill_audit_interval.sql`, `docs/04-modules/telemetry.md` |
|
||
| Nové sloupce / tabulky | nový `db/migration/V00x__*.sql` + případně `db/routines` / `db/views` |
|
||
| 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) |
|
||
|
||
---
|
||
|
||
## Konvence (krátce)
|
||
|
||
- Python: `snake_case`, type hints, Pydantic pro API modely.
|
||
- SQL: `snake_case`, explicitní FK; Flyway pořadí `V###__` / repeatable `R__`.
|
||
- 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.
|