23 KiB
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
-
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.
-
Všechny doménové záznamy vázat na
site_id(telemetrie, plány, audit, konfigurace aktiv, session, …). Výjimka:market_interval_priceje globální pro zdroj/trh; vazba na site je přes konfiguraci a view. -
Raw ceny ≠ efektivní ceny.
ems.market_interval_price= bez marží. Efektivní nákup/prodej jen přesems.vw_site_effective_price(join na platnousite_market_config). -
Loxone = exekutor + autonomní fallback, ne optimalizátor. Logika a plán v EMS. Watchdog v Loxone nesmí záviset na čtení DB (
site_heartbeatje jen pro EMS UI/diagnostiku). -
FVE pole B (
controllable = false, typicky ongrid GEN) – žádný curtailment. Curtailment jen pole A (Deye). Solver smí omezovat jenpv_a; pole B může mít zelený bonus naasset_pv_array(green_bonus_*), auditpv_b_production_wh/green_bonus_czk. -
Záporná prodejní cena →
grid_export == 0v LP (hard constraint). -
Záporná nákupní cena → omezit import na realistický horní strop (viz
solve_dispatchvplanning_engine.py– nesmí „nekonečný“ import). -
PuLP + HiGHS pro dispatch; žádný návrat k greedy
fn_plan_dayjako primárnímu řešení (SQL wrapper může zůstat pro uložení výsledků – dle docs). -
Zelený bonus je na
asset_pv_array(sloupcegreen_bonus_*), nikdy vsite_market_config. Výpočet přesfn_green_bonus_revenue(). Bonus se nepočítá v solveru – pouze v audit_filler (fn_fill_audit_interval). -
Deye Modbus: čtení i zápis (setpointy). RS485→Waveshare→TCP, knihovna
pymodbus. -
Přepínání provozního režimu přes DB API /
ems.fn_set_mode– držet konzistenci soperating_mode_defa Loxoneloxone_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.
- 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 jeSELECT … 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 vdb/routines//db/views/+ jedno volání z aplikace. - Jediné SQL v
backend/services/*.pyabackend/app/routers/*.py:SELECT 1/EXISTS;select ems.fn_*(…);SELECT … FROM ems.vw_*(read přes view); žádné jiné ad-hocSELECT/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_bundlev repeatabledb/routines/R__073_fn_health_site_jobs_mode_bundle.sql; FastAPI je vapp/main.py+ joby vapp/lifespan.py.
Provozní režimy (operating_mode)
- Pět hodnot
mode_codevems.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 vsolve_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_modea zapisují vwrite_inverter_setpoints. lock_battery=TrueuControlSetpoints(PRESERVE): registry 108/109 = 0 – Deye baterii nepoužívá. Výjimka oproti obecnému pravidlu max A ve PASSIVE/SELL.
-
forecast_pv_runaforecast_pv_intervalse NESMÍ mazat – historické běhy zůstávají v DB pro tracking přesnosti (forecast_accuracy,fn_fill_forecast_accuracy). -
Endpoint
GET …/forecast/pvvracíDISTINCT ON (interval_start, pv_array_id)seřazené podle nejnovějšíhoforecast_pv_run.created_at, aby UI nemělo duplikáty slotů; plná historie běhů zůstává v tabulkách. -
Příchod a odjezd EV detekuje
telemetry_collectorz telemetrie nabíječky: přechodavailable→preparing/charging(resp. jakýkoli stav ≠available) znamená příjezd; přechod naavailableuzavřeev_session. Tabulkaev_arrival_statsse při příjezdu doplňuje přesfn_update_ev_arrival_statsa nemá se mazat (dlouhodobá historická statistika). -
Bazální spotřeba =
load_power_wminus řízené zátěže (součet EV ztelemetry_ev_charger, TČ ztelemetry_heat_pump). Tabulkaconsumption_baseline_statsse plní denně (APScheduler 00:30) přesfn_update_baseline_stats. Solver načítá průměr bazálu zconsumption_baseline_stats(DOW + hodina v Europe/Prague), ne zconsumption_baseline_interval. -
Dynamický horizont plánování (jen OTE): konec okna z
ems.fn_planning_horizon_end(site_id, horizon_start)(minposledního OTE konce astart + 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 zvw_site_effective_price(žádná predikce v LP). Účelová funkce má terminal SoC shadow price:−(průměr buy v prvních 24 h slotů × 0,9 / 1000) × soc[T−1](Kč; SoC v Wh).market_price_stats/fn_get_predicted_pricezůstávají pro statistiky a budoucí rozšíření; detail historie:docs/04-modules/planning-extended-horizon.md. -
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_codez Pythonu (POST /api/v1/sites/{id}/mode, mismatch → SELF_SUSTAIN,fn_expire_modes) lze Discord zapnout přesDISCORD_WEBHOOK_URL. Detail:docs/04-modules/modbus-command-journal.md. -
Deye zápis registrů 60–499: pouze FC 0x10 (
write_registers), nikdy FC 0x06 pro tento rozsah;execute_modbus_commandssluč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 vcontrol_exporter.get_deye_mode). Reg 108 (charge A) se řídí záměrem solveru: max při nabíjení (bat_w > 0), 0 jinak (pass-through, self-consumption). Reg 109 (discharge A) vždy max z DB (výjimka PRESERVE:lock_battery=True→ 0 / 0). Ří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 (deye_zero_export_modez DB: 1 = to load / 2 = to CT v non-SELL; 0 = selling first ve SELL) / 178 (pevně 32 ve SELL, 48 v PASSIVE a CHARGE – bez read-modify-write) / 143 (export limit W z DB) / 145 (solar sell, vždy 1 = enabled) 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 NULLdeye_last_system_time_sync_at, nebo uplynulých 24 h od posledního syncu;deye_last_system_time_sync_at/deye_last_system_time_sync_minutepo úspěšném zápisu 62–64 a znovu po úspěšné toleranční verifikaci; při chybě čtení se čas zapisuje; reg 64 se zapisuje s sekundami 0; verify vždy čte 62–64 najednou — reg 64 nesmí do striktní větve; toleranční odchylka až 120 s; po 3 neúspěších u hodin bez SELF_SUSTAIN (jen Discord). SELL:battery_w< −500 agrid_setpoint_w< −200 (aktivní vybíjení baterie pro export). CHARGE:battery_w> 500 agrid_setpoint_w> 200. PASSIVE: ostatní (včetně pass-through s reg 108=0, self-consumption,battery_w=Noneu SELF_SUSTAIN). Čtyři typy slotů: Charge (108=max), Pass-through (108=0, PV→síť), Discharge-export (SELL, 142=0), Self-consumption (108=0, noc). Reg 109 vždy max kromě PRESERVE. Detail:docs/04-modules/modbus-registers.md, režimy:docs/04-modules/operating-modes.md. -
Baterie – export v LP: V
solve_dispatchbinárkaz_export[t]: pokudgrid_exportv daném slotu ≥ 1 W, platí koncovésoc[t] ≥ arb_base_wh(ekonomická rezerva z DB, ne časová řadaarb_floor_series). Bez exportu může plán jít kmin_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_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_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, 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_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.
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 |
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) | 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, planning_engine.py |
| 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 | Cursor MCP server postgres-ems, nástroj query. |
Konvence (krátce)
- Python:
snake_case, type hints, Pydantic pro API modely. - SQL:
snake_case, explicitní FK; Flyway pořadíV###__/ repeatableR__NNN_*.sql(třímístný prefix = pořadí závislostí mezi fn/vw). - Timescale continuous aggregate (CA): komentář k objektu CA je
COMMENT ON VIEW, neCOMMENT 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.
- Deploy:
flyway validatepředmigrate(deploy/deploy.sh). Lokálně./scripts/flyway_validate_local.sh; CI vizdocs/deployment-self-hosted.mdascripts/ci_check_migration_immutability.sh.