gpt5.5 - odladeni dokumentace dle kodu
This commit is contained in:
@@ -41,7 +41,7 @@ Systém přebírá rozhodovací logiku od Loxone a stává se „mozkem" – pl
|
||||
|
||||
| Vrstva | Technologie |
|
||||
|---|---|
|
||||
| DB | PostgreSQL 16 + TimescaleDB |
|
||||
| DB | PostgreSQL 18 + TimescaleDB |
|
||||
| API / BFF | PostgREST (automatické REST z DB schématu) |
|
||||
| Backend logika | Python (FastAPI) – plánovač, sběr dat, integrace |
|
||||
| Frontend | React + TypeScript |
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
┌─────────────▼───────────────────────────────┐
|
||||
│ PostgREST │
|
||||
│ Auto-REST API z PostgreSQL schématu ems │
|
||||
│ Read: views, tabulky │
|
||||
│ Write: insert/update přes API │
|
||||
│ Read-only: views, vybrané tabulky │
|
||||
│ Write operace jdou přes FastAPI │
|
||||
└─────────────┬───────────────────────────────┘
|
||||
│ SQL
|
||||
┌─────────────▼───────────────────────────────┐
|
||||
│ PostgreSQL 16 + TimescaleDB │
|
||||
│ PostgreSQL 18 + TimescaleDB │
|
||||
│ Schéma: ems │
|
||||
│ Funkce, views, hypertables │
|
||||
└─────────────┬───────────────────────────────┘
|
||||
@@ -26,13 +26,19 @@
|
||||
│ FastAPI (Python) │
|
||||
│ – Scheduled tasks (APScheduler) │
|
||||
│ – telemetry_collector (každých 60s) │
|
||||
│ – price_importer (13:30, 14:00, 00:05) │
|
||||
│ – heartbeat (každých 60s) │
|
||||
│ – price_importer (13:25, 13/14:12, │
|
||||
│ 13/14:45, 14:00, │
|
||||
│ 00:05) │
|
||||
│ – forecast_service (každé 2h, minute 05)│
|
||||
│ – planning_engine (denně 15:00) │
|
||||
│ – rolling_replan (každých 15min) │
|
||||
│ – control_exporter (každých 15min) │
|
||||
│ – audit_filler (každých 15min) │
|
||||
│ – forecast_accuracy (:02,:17,:32,:47) │
|
||||
│ – plan_actual_slot_guard (:05,:20,:35,:50) │
|
||||
│ – verify_modbus (každé 2 min) │
|
||||
│ – signal_outbound (každých 15s) │
|
||||
└──────┬──────────────────────────┬────────────┘
|
||||
│ Modbus TCP │ HTTP
|
||||
┌──────▼──────┐ ┌───────▼────────────┐
|
||||
@@ -59,7 +65,7 @@ FastAPI endpointy pro dashboard a konfiguraci preferují **jedno volání** `sel
|
||||
|
||||
| Komponenta | Technologie | Port | Popis |
|
||||
|---|---|---|---|
|
||||
| `db` | PostgreSQL 16 + TimescaleDB | 5432 | Datová vrstva |
|
||||
| `db` | PostgreSQL 18 + TimescaleDB | 5432 | Datová vrstva |
|
||||
| `postgrest` | PostgREST 12 | 3000 | Auto-REST API |
|
||||
| `backend` | Python 3.12 / FastAPI | 8000 | Logika, scheduled tasks |
|
||||
| `frontend` | React + Vite + TypeScript | 5173 (dev) / 80 (prod) | UI |
|
||||
@@ -88,7 +94,8 @@ ems-platform/
|
||||
R__019_fn_fill_audit_interval.sql
|
||||
R__073_fn_health_site_jobs_mode_bundle.sql
|
||||
(historicky) R__fn_plan_day.sql – primární plánování je PuLP v Pythonu
|
||||
R__fn_create_planning_run.sql
|
||||
R__037_fn_planning_run_commit.sql
|
||||
R__063_fn_load_planning_slots_full.sql
|
||||
views/
|
||||
R__061_vw_site_effective_price.sql
|
||||
R__058_vw_latest_telemetry.sql
|
||||
@@ -114,7 +121,7 @@ ems-platform/
|
||||
telemetry_collector.py
|
||||
price_importer.py
|
||||
forecast_service.py
|
||||
planning_engine.py ← volá ems.fn_create_planning_run()
|
||||
planning_engine.py ← PuLP solver, ukládá přes ems.fn_planning_run_commit()
|
||||
control_exporter.py
|
||||
audit_filler.py
|
||||
modbus/
|
||||
@@ -173,9 +180,10 @@ ems-platform/
|
||||
Zařízení → Waveshare → Modbus TCP → telemetry_collector → PostgreSQL
|
||||
```
|
||||
|
||||
### Denní plánování (15:00)
|
||||
### Denní plánování (15:00) a rolling replan
|
||||
```
|
||||
PostgreSQL (ceny + forecast) → fn_create_planning_run() → planning_interval
|
||||
PostgreSQL (ceny + forecast + telemetrie) → planning_engine (PuLP)
|
||||
→ fn_planning_run_commit() → planning_interval
|
||||
```
|
||||
|
||||
### Operátorské manuální akce (UI)
|
||||
@@ -200,6 +208,10 @@ Browser → PostgREST (čtení views/tabulek, filtr site_id dle výběru v UI)
|
||||
Browser → FastAPI (seznam lokalit /me/sites, triggery: replanning, import cen, …)
|
||||
```
|
||||
|
||||
PostgREST je v aktuální produkční konfiguraci určený pro čtení (`ems_anon` má
|
||||
`SELECT` na vybrané views/tabulky). Zápisy a operátorské akce se provádí přes
|
||||
FastAPI, které používá vlastní DB connection.
|
||||
|
||||
---
|
||||
|
||||
## Deployment: single-site (výchozí)
|
||||
|
||||
@@ -22,6 +22,8 @@ CREATE TABLE site (
|
||||
code TEXT UNIQUE NOT NULL, -- např. 'home-01'
|
||||
name TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
|
||||
latitude NUMERIC(9,6), -- Open-Meteo / pvlib
|
||||
longitude NUMERIC(9,6), -- Open-Meteo / pvlib
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
@@ -39,6 +41,7 @@ CREATE TABLE site_endpoint (
|
||||
host TEXT NOT NULL,
|
||||
port INT,
|
||||
protocol TEXT, -- 'modbus_tcp', 'http', 'https'
|
||||
unit_id INT, -- Modbus Unit ID pro modbus_tcp
|
||||
auth_reference TEXT, -- odkaz na secret / env proměnnou
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
notes TEXT
|
||||
@@ -119,6 +122,11 @@ CREATE TABLE asset_battery (
|
||||
charge_efficiency NUMERIC(5,4) DEFAULT 0.95,
|
||||
discharge_efficiency NUMERIC(5,4) DEFAULT 0.95,
|
||||
degradation_cost_czk_kwh NUMERIC(8,4) DEFAULT 0.5 -- náklad na cyklus
|
||||
-- pozdější migrace přidávají plánovací tunables:
|
||||
-- charge_slot_buffer, discharge_slot_buffer,
|
||||
-- planner_max_soc_percent, planner_discharge_floor_percent,
|
||||
-- planner_extreme_buy_threshold_czk_kwh,
|
||||
-- planner_terminal_soc_value_factor
|
||||
);
|
||||
```
|
||||
|
||||
@@ -135,7 +143,7 @@ CREATE TABLE asset_pv_array (
|
||||
code TEXT NOT NULL,
|
||||
name TEXT,
|
||||
nominal_power_wp INT NOT NULL, -- 10000
|
||||
azimuth_deg NUMERIC(6,2), -- 0=S, 90=Z, -90=V
|
||||
azimuth_deg NUMERIC(6,2), -- kompasově/pvlib: 0=N, 90=E, 180=S, 270=W
|
||||
tilt_deg NUMERIC(5,2),
|
||||
module_count INT,
|
||||
shading_factor NUMERIC(4,3) DEFAULT 1.0,
|
||||
@@ -169,22 +177,30 @@ CREATE TABLE asset_ev_charger (
|
||||
);
|
||||
```
|
||||
|
||||
### `asset_flexible_device`
|
||||
Generická tabulka pro ostatní flexibilní spotřebiče (TUV, tepelné čerpadlo, ...).
|
||||
### `asset_heat_pump`
|
||||
Tepelné čerpadlo / TUV. Aktuální implementace má samostatnou tabulku místo
|
||||
historického generického `asset_flexible_device`.
|
||||
|
||||
```sql
|
||||
CREATE TABLE asset_flexible_device (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT REFERENCES site(id),
|
||||
code TEXT NOT NULL,
|
||||
device_type TEXT NOT NULL, -- 'tuv', 'heat_pump', 'pool', ...
|
||||
control_mode TEXT DEFAULT 'loxone', -- jak se řídí
|
||||
max_power_w INT,
|
||||
min_power_w INT DEFAULT 0,
|
||||
interruptible BOOLEAN DEFAULT true,
|
||||
schedulable BOOLEAN DEFAULT true,
|
||||
priority INT DEFAULT 50, -- 0=nejvyšší priorita
|
||||
notes TEXT
|
||||
CREATE TABLE asset_heat_pump (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT REFERENCES site(id),
|
||||
code TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
endpoint_id INT REFERENCES site_endpoint(id),
|
||||
rated_heating_power_w INT NOT NULL,
|
||||
cop_rated NUMERIC(4,2),
|
||||
cop_temp_reference_c NUMERIC(5,2),
|
||||
min_run_duration_min INT NOT NULL DEFAULT 30,
|
||||
min_stop_duration_min INT NOT NULL DEFAULT 15,
|
||||
tuv_tank_volume_l INT,
|
||||
tuv_min_temp_c NUMERIC(5,2) NOT NULL DEFAULT 45,
|
||||
tuv_max_temp_c NUMERIC(5,2) NOT NULL DEFAULT 60,
|
||||
tuv_target_temp_c NUMERIC(5,2) NOT NULL DEFAULT 55,
|
||||
tuv_temp_sensor_ref TEXT,
|
||||
schedulable BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT
|
||||
);
|
||||
```
|
||||
|
||||
@@ -424,7 +440,9 @@ CREATE TABLE consumption_baseline_interval (
|
||||
```
|
||||
|
||||
### Flexibilní spotřebiče
|
||||
Flexibilní spotřeba se neukládá souhrnně – odvozuje se ze součtu `telemetry_ev_charger` + stavů `asset_flexible_device` per interval. Plánovaná flexibilní spotřeba je součástí `planning_interval`.
|
||||
Flexibilní spotřeba se neukládá souhrnně – odvozuje se ze součtu
|
||||
`telemetry_ev_charger` + `telemetry_heat_pump` / plánovaných setpointů.
|
||||
Plánovaná flexibilní spotřeba je součástí `planning_interval`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ Operativní predikce je v **`fn_get_baseline_forecast`** a v přímém dotazu v
|
||||
**Telemetrie:**
|
||||
- Stav ON/OFF (čteme z Loxone HTTP výstupu nebo Virtual Output stavu)
|
||||
- Teplota zásobníku (pokud je čidlo v Loxone – doporučeno)
|
||||
- Aktuální výkon: není přímo měřen, používáme `max_power_w` z `asset_flexible_device`
|
||||
- Aktuální výkon: zatím není reálně čten z TČ; placeholder telemetrie a plánování
|
||||
používají parametry z `asset_heat_pump`, hlavně `rated_heating_power_w`
|
||||
|
||||
**Plánování:**
|
||||
- TUV se ohřívá v době přebytku FVE nebo levného spotu
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
- Čte aktivní plán z DB pro daný 15min interval
|
||||
- Zkontroluje override záznamy
|
||||
- Zapíše setpointy do Deye přes Modbus TCP
|
||||
- Zapíše setpointy EV nabíječek přes Modbus TCP
|
||||
- Zapíše setpointy tepelného čerpadla přes Modbus TCP
|
||||
- EV nabíječky a tepelné čerpadlo zatím pouze vyhodnotí a zaloguje; konkrétní
|
||||
Modbus registry jsou TODO
|
||||
- Odešle potvrzovací setpointy do Loxone přes HTTP (Loxone jako exekutor fallback logiky)
|
||||
- Loguje každý write pro audit
|
||||
|
||||
@@ -19,10 +19,10 @@ DB (planning_interval + site_override)
|
||||
↓
|
||||
control_exporter.py (každých 15min nebo on-demand)
|
||||
├── Modbus write → Deye (baterie, grid limit)
|
||||
├── Modbus write → Teltonika EV nabíječka 1
|
||||
├── Modbus write → Teltonika EV nabíječka 2
|
||||
├── Modbus write → Samsung TČ
|
||||
└── HTTP POST → Loxone Virtual Inputs (informační setpointy)
|
||||
├── TODO/log → Teltonika EV nabíječka 1
|
||||
├── TODO/log → Teltonika EV nabíječka 2
|
||||
├── TODO/log → Samsung TČ
|
||||
└── HTTP GET → Loxone Virtual Inputs (informační setpointy)
|
||||
```
|
||||
|
||||
**Loxone role:** Loxone dostává setpointy jako informaci a jako fallback ochranu.
|
||||
@@ -34,7 +34,7 @@ Rozhodovací logika je v EMS, ne v Loxone.
|
||||
|
||||
| Trigger | Čas | Popis |
|
||||
|---|---|---|
|
||||
| Scheduled | každých 15min (xx:00, xx:15, xx:30, xx:45) | Standardní export na začátku intervalu |
|
||||
| Scheduled | každých 15min (`xx:14`, `xx:29`, `xx:44`, `xx:59`) | Export těsně před dalším slotem |
|
||||
| On-demand | po vytvoření nového plánu | Okamžitý export pokud plán překrývá aktuální čas |
|
||||
| On-demand | po vytvoření override | Okamžitá aplikace přepisu |
|
||||
|
||||
@@ -51,6 +51,8 @@ Ověření: logy backendu kolem pokusu **nebo** `select id,status,created_at fro
|
||||
## Logika exportu
|
||||
|
||||
```python
|
||||
# Zjednodušený historický náčrt. Aktuální implementace používá
|
||||
# ems.fn_planning_interval_at_offset(), ControlSetpoints a Deye journal.
|
||||
async def export_setpoints_for_interval(site_id: int, interval_start: datetime, db):
|
||||
"""
|
||||
Načte plánované setpointy pro daný interval, aplikuje overrides
|
||||
@@ -84,14 +86,11 @@ async def export_setpoints_for_interval(site_id: int, interval_start: datetime,
|
||||
|
||||
setpoints = apply_overrides(plan, overrides)
|
||||
|
||||
# 3. Zapsat do zařízení (paralelně)
|
||||
await asyncio.gather(
|
||||
write_inverter_setpoints(site_id, setpoints, db),
|
||||
write_ev_charger_setpoints(site_id, setpoints, db),
|
||||
write_heat_pump_setpoints(site_id, setpoints, db),
|
||||
write_loxone_setpoints(site_id, setpoints, db),
|
||||
return_exceptions=True
|
||||
)
|
||||
# 3. Zapsat Deye, zalogovat EV/TČ TODO, poslat Loxone
|
||||
await write_inverter_setpoints(site_id, setpoints, db)
|
||||
await write_ev_setpoints(site_id, setpoints, db) # TODO registry
|
||||
await write_heat_pump_setpoint(site_id, setpoints, db) # TODO registry
|
||||
await send_loxone_setpoints(site_id, setpoints, mode, db)
|
||||
|
||||
|
||||
def apply_overrides(plan, overrides) -> Setpoints:
|
||||
@@ -153,7 +152,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
||||
| **CHARGE** | `battery_w` > 0 **a** `grid_setpoint_w` > 0 |
|
||||
| **PASSIVE (ZERO)** | vše ostatní — reg. **108/109** dle `_deye_zero_export_amps_for_passive` (viz `operating-modes.md`) |
|
||||
|
||||
**PASSIVE** (AUTO, ZERO): výchozí **108** i **109** = maximum z DB; u exportu bez vybíjení **108 = 0**, u importu bez nabíjení **109 = 0** (`_deye_zero_export_amps_for_passive`). **TOU** z plánu. Reg. **142** = `deye_zero_export_mode`. Reg. **145** (solar sell): v kódu vždy **1** — význam přepínače a rozdíl vůči neřízeným FVE polím je v [`operating-modes.md`](operating-modes.md) (sekce *ZERO a zakázaný export*).
|
||||
**PASSIVE** (AUTO, ZERO): výchozí **108** i **109** = maximum z DB; u exportu bez vybíjení **108 = 0**, u importu bez nabíjení **109 = 0** (`_deye_zero_export_amps_for_passive`). **TOU** z plánu. Reg. **142** = `deye_zero_export_mode`. Reg. **145** (solar sell): **0** při `export_ban` mimo SELL, jinak **1** — význam přepínače a rozdíl vůči neřízeným FVE polím je v [`operating-modes.md`](operating-modes.md) (sekce *ZERO a zakázaný export*).
|
||||
|
||||
**SELF_SUSTAIN** zůstává **PASSIVE** v `get_deye_mode`; **108/109** jsou vždy **max z DB** (bez variant ZERO). Rozdíl je **`self_sustain_local_use=True`**: **TOU SOC** = **`min_soc_percent`**, `battery_w=None`.
|
||||
|
||||
@@ -165,7 +164,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
||||
| **109** (discharge A) | **0** | max / **0** (import, držet bat.) | **max z DB** | dle varianty |
|
||||
| **142** (limit control) | `deye_zero_export_mode` | `deye_zero_export_mode` | **0** (selling first) | `deye_zero_export_mode` |
|
||||
| **143** (export cap) | max z DB | max z DB | `min(max_site, max(200, \|grid_setpoint_w\|))` | max z DB |
|
||||
| **145** (solar sell) | 1 | 1 | 1 | 1 |
|
||||
| **145** (solar sell) | 1 / 0 při `export_ban` | 1 / 0 při `export_ban` | 1 | 1 / 0 při `export_ban` |
|
||||
| **178** (peak shaving) | 48 | 48 | **32** | 48 |
|
||||
|
||||
U **AUTO PASSIVE** závisí **108/109** na znaménkách plánu (viz `operating-modes.md`). **SELF_SUSTAIN** drží oba **max z DB**; **TOU SOC** ve všech PASSIVE větvích je **`min_soc_percent`** (viz `_deye_passive_tou_battery_soc_pct`). Liší se především **`battery_w`** a mapování **108/109**.
|
||||
@@ -184,6 +183,9 @@ Po zápisu na Modbus se hodnoty ověřují v `verify_modbus_commands` (`control_
|
||||
Při přechodu **SELF_SUSTAIN → AUTO** (`run_fn_set_mode_with_discord`) se na pozadí spustí **rolling replan**, aby aktivní plán odpovídal plné optimalizaci. Viz [`modbus-command-journal.md`](modbus-command-journal.md).
|
||||
|
||||
```python
|
||||
# Historický pseudokód. Aktuální Deye implementace používá journal
|
||||
# ems.modbus_command a FC 0x10 (`write_registers`) nad registry
|
||||
# 108/109/141/142/143/145/178/340 + TOU bloky.
|
||||
async def write_inverter_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
inverters = await db.fetch(
|
||||
"SELECT ai.*, se.host, se.port, se.unit_id "
|
||||
@@ -215,6 +217,10 @@ async def write_inverter_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
|
||||
## Zápis do Teltonika EV nabíječek (Modbus)
|
||||
|
||||
Aktuální implementace registry zatím nezapisuje. Funkce `write_ev_setpoints`
|
||||
načte schedulable nabíječky, spočítá proud podle `ev1/ev2_setpoint_w` a jen ho
|
||||
zaloguje jako `Modbus TODO`.
|
||||
|
||||
```python
|
||||
async def write_ev_charger_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
chargers = await db.fetch(
|
||||
@@ -250,6 +256,10 @@ async def write_ev_charger_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
|
||||
## Zápis do Samsung TČ (Modbus)
|
||||
|
||||
Aktuální implementace registry zatím nezapisuje. Funkce
|
||||
`write_heat_pump_setpoint` načte schedulable TČ a zaloguje požadované
|
||||
`heat_pump_enable` jako `Modbus TODO`.
|
||||
|
||||
```python
|
||||
async def write_heat_pump_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
heat_pumps = await db.fetch(
|
||||
@@ -297,20 +307,22 @@ async def write_loxone_setpoints(site_id: int, setpoints: Setpoints, db):
|
||||
# Loxone Virtual HTTP Input – každý setpoint = jeden HTTP GET/POST
|
||||
# Formát: /dev/sps/io/{VirtualInputName}/{value}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.get(f"{base_url}/EMS_BatterySetpoint/{setpoints.battery_setpoint_w}")
|
||||
await session.get(f"{base_url}/EMS_GridSetpoint/{setpoints.grid_setpoint_w or 0}")
|
||||
await session.get(f"{base_url}/EMS_EVChargeTotal/{setpoints.ev_charge_power_w or 0}")
|
||||
await session.get(f"{base_url}/EMS_HeatPumpEnable/{1 if setpoints.heat_pump_enabled else 0}")
|
||||
await session.get(f"{base_url}/EMS_Mode/{mode.loxone_mode_value}")
|
||||
await session.get(f"{base_url}/EMS_Battery_Setpoint_W/{battery_w}")
|
||||
await session.get(f"{base_url}/EMS_Grid_Setpoint_W/{grid_w}")
|
||||
await session.get(f"{base_url}/EMS_EV1_Power_W/{ev1_power_w}")
|
||||
await session.get(f"{base_url}/EMS_EV2_Power_W/{ev2_power_w}")
|
||||
await session.get(f"{base_url}/EMS_HeatPump_Enable/{heat_pump_enable}")
|
||||
```
|
||||
|
||||
> Virtual Input jména v Loxone (`EMS_BatterySetpoint` atd.) je nutné vytvořit při konfiguraci Loxone projektu.
|
||||
> Virtual Input jména v Loxone (`EMS_Battery_Setpoint_W`, `EMS_Grid_Setpoint_W`
|
||||
> atd.) je nutné vytvořit při konfiguraci Loxone projektu.
|
||||
|
||||
---
|
||||
|
||||
## Konfigurace (env proměnné)
|
||||
|
||||
```env
|
||||
CONTROL_EXPORT_LEAD_TIME_SEC=10 # kolik sekund před začátkem intervalu exportovat
|
||||
CONTROL_MODBUS_TIMEOUT_SEC=5
|
||||
LOXONE_USER=admin # nebo přes auth_reference v site_endpoint
|
||||
LOXONE_PASSWORD=secret
|
||||
@@ -331,9 +343,8 @@ Fallback: pokud per-site webhook není vyplněný, použije se env `DISCORD_WEBH
|
||||
|
||||
## Otevřené body
|
||||
|
||||
- [ ] Doplnit Modbus write registry Deye (charge/discharge/export limit)
|
||||
- [ ] Doplnit Modbus write registry Teltonika (current limit, enable)
|
||||
- [ ] Doplnit Modbus write registry Samsung TČ (enable, target temp)
|
||||
- [ ] Loxone Virtual Input jména – dohodnout a vytvořit v Loxone projektu
|
||||
- [ ] Loxone Virtual Input jména z tohoto dokumentu vytvořit v Loxone projektu
|
||||
- [ ] Strategie rozdělení EV výkonu mezi 2 nabíječky (rovnoměrně vs dle stavu session)
|
||||
- [ ] Co dělat při selhání zápisu do jednoho zařízení (rollback ostatních?)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
- Stahuje meteorologická data (irradiance, teplota) pro každé FVE pole zvlášť
|
||||
- Vypočítává predikovaný výkon v 15min intervalech
|
||||
- Ukládá výsledek per `pv_array_id` + `run_id`
|
||||
- Predikce se spouští denně a před každým plánovacím během
|
||||
- Predikce se spouští každé 2 hodiny v `:05` a ručně přes API. Plánovač používá
|
||||
poslední dostupné uložené forecasty; forecast nespouští implicitně před každým
|
||||
plánovacím během.
|
||||
|
||||
---
|
||||
|
||||
@@ -13,12 +15,15 @@
|
||||
|
||||
| Pole | Výkon | Azimut | Sklon | Střídač | Řízení |
|
||||
|---|---|---|---|---|---|
|
||||
| A | 10 kWp | TBD | TBD | Deye 20kW | řídíme |
|
||||
| B | 10 kWp | TBD | TBD | Ongridový | autonomní, **nepredikujeme odděleně** |
|
||||
| A | 10 kWp | 184° | 22° | Deye 20kW | řídíme |
|
||||
| B | 10 kWp | 184° | 35° | Ongridový | autonomní, predikujeme jako samostatné pole |
|
||||
|
||||
> **Předpoklad:** Pole B (ongridový) je zapojeno do GEN portu Deye. Jeho výkon se projeví v `pv_power_w` telemetrie jako součást celkového výkonu. Pro plánování modelujeme jen pole A. Pole B bereme jako šum / bonus který se projeví v auditu.
|
||||
> **Aktuální implementace:** Forecast služba počítá všechna FVE pole lokality,
|
||||
> která mají vyplněný `azimuth_deg` a `tilt_deg`; plánovač pracuje odděleně s
|
||||
> `pv_a_forecast_w` i `pv_b_forecast_w`.
|
||||
|
||||
> Azimuty a sklony je nutné doplnit při konfiguraci lokality do `asset_pv_array`.
|
||||
> Azimut je uložen v kompasové / pvlib konvenci: `0=N`, `90=E`, `180=S`,
|
||||
> `270=W`.
|
||||
|
||||
---
|
||||
|
||||
@@ -36,10 +41,9 @@
|
||||
GET https://api.open-meteo.com/v1/forecast
|
||||
?latitude={lat}
|
||||
&longitude={lon}
|
||||
&hourly=shortwave_radiation,temperature_2m
|
||||
&minutely_15=shortwave_radiation,temperature_2m
|
||||
&timezone=Europe/Prague
|
||||
&forecast_days=3
|
||||
&minutely_15=direct_normal_irradiance,diffuse_radiation,shortwave_radiation,temperature_2m
|
||||
&timezone=auto
|
||||
&forecast_days=7
|
||||
```
|
||||
|
||||
**Záložní / budoucí: Solcast**
|
||||
@@ -51,34 +55,28 @@ GET https://api.open-meteo.com/v1/forecast
|
||||
|
||||
## Výpočet výkonu z irradiance
|
||||
|
||||
Jednoduchý fyzikální model (dostatečný pro plánování):
|
||||
Implementace používá `pvlib` a model POA irradiance `haydavies`:
|
||||
|
||||
```python
|
||||
def calculate_pv_power(
|
||||
irradiance_wm2: float, # GHI ze weather service
|
||||
temp_c: float,
|
||||
nominal_power_wp: int,
|
||||
azimuth_deg: float,
|
||||
tilt_deg: float,
|
||||
shading_factor: float = 1.0,
|
||||
temp_coeff: float = -0.004 # typicky -0.4%/°C pro křemík
|
||||
) -> int:
|
||||
# 1. Korekce na teplotu panelu
|
||||
panel_temp = temp_c + 25 # zjednodušený NOCT model
|
||||
temp_correction = 1 + temp_coeff * (panel_temp - 25)
|
||||
poa_global = pvlib.irradiance.get_total_irradiance(
|
||||
surface_tilt=tilt_deg,
|
||||
surface_azimuth=azimuth_deg, # 0=N, 90=E, 180=S, 270=W
|
||||
solar_zenith=solar_pos["apparent_zenith"],
|
||||
solar_azimuth=solar_pos["azimuth"],
|
||||
dni=dni,
|
||||
ghi=ghi,
|
||||
dhi=dhi,
|
||||
dni_extra=dni_extra,
|
||||
model="haydavies",
|
||||
)["poa_global"].fillna(0).clip(lower=0)
|
||||
|
||||
# 2. Korekce na azimut a sklon (zjednodušená, bez přesného GHI→POA)
|
||||
# Přesnější model: pvlib knihovna (doporučeno pro produkci)
|
||||
orientation_factor = cos_angle_of_incidence(azimuth_deg, tilt_deg)
|
||||
|
||||
# 3. Výsledný výkon
|
||||
power_w = (irradiance_wm2 / 1000) * nominal_power_wp * temp_correction * orientation_factor * shading_factor
|
||||
|
||||
return max(0, int(power_w))
|
||||
area_m2 = nominal_power_wp / (1000.0 * 0.20)
|
||||
power_w = (poa_global * area_m2 * 0.20 * shading_factor).clip(
|
||||
lower=0,
|
||||
upper=nominal_power_wp * 1.1,
|
||||
)
|
||||
```
|
||||
|
||||
> **Doporučení pro implementaci:** Použít knihovnu `pvlib` (Python) pro přesný POA irradiance výpočet z GHI + azimut + sklon. Je to standardní nástroj, dobře dokumentovaný.
|
||||
|
||||
---
|
||||
|
||||
## Kdo spouští predikci
|
||||
@@ -107,15 +105,15 @@ def calculate_pv_power(
|
||||
## Logika běhu predikce
|
||||
|
||||
```python
|
||||
def run_forecast(site_id: int, horizon_days: int = 2):
|
||||
def run_forecast(site_id: int):
|
||||
site = db.get_site(site_id)
|
||||
arrays = db.get_pv_arrays(site_id, controllable=True)
|
||||
arrays = db.get_pv_arrays_with_azimuth_and_tilt(site_id)
|
||||
|
||||
for array in arrays:
|
||||
# 1. Stáhnout meteorologická data
|
||||
weather = open_meteo_client.fetch(
|
||||
lat=site.lat, lon=site.lon,
|
||||
start=today, end=today + horizon_days
|
||||
forecast_days=clamp(OPEN_METEO_FORECAST_DAYS, 2, 16)
|
||||
)
|
||||
|
||||
# 2. Vytvořit forecast_pv_run
|
||||
@@ -147,8 +145,7 @@ def run_forecast(site_id: int, horizon_days: int = 2):
|
||||
temp_c=slot.temperature_2m
|
||||
))
|
||||
|
||||
db.upsert_forecast_intervals(intervals)
|
||||
db.update_forecast_run_status(run.id, "ok")
|
||||
db.insert_forecast_intervals(intervals)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -204,23 +201,20 @@ Hromadně: **`ems.fn_pv_forecast_sync_reference_days(site_id, p_days_local date[
|
||||
```env
|
||||
OPEN_METEO_API_URL=https://api.open-meteo.com/v1/forecast
|
||||
OPEN_METEO_FORECAST_DAYS=7
|
||||
FORECAST_MAX_AGE_HOURS=2 # plánovač odmítne starší predikci
|
||||
FORECAST_RETRY_COUNT=3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
- Alert pokud forecast pro dnešní den + zítřek není k dispozici do 15:00
|
||||
- Endpoint `GET /health/forecast?site_id=1&date=YYYY-MM-DD` → čerstvost a počet intervalů
|
||||
- Zatím není samostatný `/health/forecast` endpoint.
|
||||
- Stav se kontroluje přes logy běhu `scheduled_forecast_refresh`, přes forecast API
|
||||
a přes obecné health endpointy.
|
||||
- Log každého běhu (délka horizontu, počet intervalů, trvání, zdroj)
|
||||
|
||||
---
|
||||
|
||||
## Otevřené body
|
||||
|
||||
- [ ] Doplnit přesný azimut a sklon obou FVE polí při instalaci
|
||||
- [ ] Rozhodnout: pvlib pro přesnější POA výpočet vs jednoduchý model – doporučujeme pvlib od začátku
|
||||
- [ ] Pole B (ongridový) – zda vůbec modelovat nebo ignorovat v plánu a jen sledovat v auditu
|
||||
- [ ] Ověřit přesný azimut a sklon obou FVE polí proti skutečné instalaci
|
||||
- [ ] Solcast jako alternativa v budoucnu – `forecast_source` to umožňuje bez DB změn
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Co modul dělá
|
||||
|
||||
- Čte data ze střídače Deye, EV nabíječek Teltonika a tepelného čerpadla Samsung přes Modbus TCP
|
||||
- Čte data ze střídače Deye přes Modbus TCP
|
||||
- EV nabíječky Teltonika a tepelné čerpadlo Samsung mají zatím placeholder
|
||||
vzorky; konkrétní registry jsou TODO
|
||||
- Ukládá surová měření do DB (1min granularita)
|
||||
- Detekuje výpadky komunikace a loguje chyby
|
||||
- Agreguje 1min data na 15min průměry pro spotřebu, audit a plánování
|
||||
@@ -18,15 +20,15 @@ Samostatná Python služba. Běží jako smyčka, nezávislá na FastAPI.
|
||||
| Zařízení | Interval | Důvod |
|
||||
|---|---|---|
|
||||
| Deye střídač | 60 s | 1min granularita telemetrie |
|
||||
| Teltonika EV nabíječka 1 | 60 s | |
|
||||
| Teltonika EV nabíječka 2 | 60 s | |
|
||||
| Samsung tepelné čerpadlo | 60 s | |
|
||||
| Teltonika EV nabíječka 1 | 60 s | zatím placeholder `available`, 0 W |
|
||||
| Teltonika EV nabíječka 2 | 60 s | zatím placeholder `available`, 0 W |
|
||||
| Samsung tepelné čerpadlo | 60 s | zatím placeholder hodnoty |
|
||||
|
||||
### Chování při chybě
|
||||
|
||||
- Chyba komunikace: záznam se nezapíše, chyba se loguje
|
||||
- 3 po sobě jdoucí chyby = alert (log WARNING)
|
||||
- 10 po sobě jdoucích chyb = log ERROR + pokus o reconnect
|
||||
- Kód zatím nedrží počítadlo po sobě jdoucích chyb podle zařízení; chyby se logují
|
||||
při jednotlivých poll pokusech
|
||||
- Data se neinterpolují – chybějící minuty zůstanou prázdné (audit to pozná)
|
||||
|
||||
---
|
||||
@@ -43,7 +45,7 @@ Komunikace: Modbus TCP, Unit ID dle DIP přepínače na střídači (typicky 1).
|
||||
| 514 (0x0202) | uint16 | Dnešní nabití baterie | Wh | `batt_charge_today_wh` |
|
||||
| 515 (0x0203) | uint16 | Dnešní vybití baterie | Wh | `batt_discharge_today_wh` |
|
||||
| 588 (0x024C) | uint16 | Battery SoC | % | `battery_soc_percent` |
|
||||
| 590 (0x024E) | int16 | Tok výkonu baterie | W | signed: **+ vybíjení, − nabíjení** |
|
||||
| 590 (0x024E) | int16 | Tok výkonu baterie | W | signed z Deye; v DB `battery_power_w` platí **+ nabíjení, − vybíjení** |
|
||||
| 625 (0x0271) | int16 | Výkon sítě | W | signed: **+ import, − export** |
|
||||
| 653 (0x028D) | uint16 | Celková spotřeba | W | `load_power_w` |
|
||||
| 667 (0x029B) | int16 | Výkon GEN portu (FVE pole B) | W (signed) | `gen_port_power_w`; záporné při zpětném toku / bez výroby — **číst signed** |
|
||||
@@ -60,6 +62,13 @@ Komunikace: Modbus TCP, Unit ID dle DIP přepínače na střídači (typicky 1).
|
||||
|
||||
**Zápis setpointů (plánování → Deye):**
|
||||
|
||||
Aktuální řízení Deye je popsané v [`control.md`](control.md) a
|
||||
[`modbus-registers.md`](modbus-registers.md). Nepoužívá starý `write_register`
|
||||
model, ale journal `ems.modbus_command` a FC 0x10 (`write_registers`) pro
|
||||
registry 108/109/141/142/143/145/178/340 + TOU bloky.
|
||||
|
||||
Historická orientační mapa níže neplatí jako implementační kontrakt:
|
||||
|
||||
| Registr (hex) | Typ | Popis | Hodnota |
|
||||
|---|---|---|---|
|
||||
| 0x00F3 | Write Single | Battery charge power limit | W |
|
||||
@@ -114,14 +123,15 @@ Komunikace: Modbus TCP přes Waveshare.
|
||||
|
||||
## Kód telemetrie (Python)
|
||||
|
||||
Implementace: `backend/services/telemetry_collector.py` — `poll_inverter()` používá konstanty `DEYE_REG_*` a třídu `ModbusDevice`; hlavní smyčka je `run_telemetry_loop` / `run_telemetry_loop_wrapper`.
|
||||
Implementace: `backend/services/telemetry_collector.py` — `poll_inverter()` používá konstanty `DEYE_REG_*` a sdíleného Modbus klienta z `services.modbus_client`; hlavní smyčka je `run_telemetry_loop` / `run_telemetry_loop_wrapper`.
|
||||
|
||||
---
|
||||
|
||||
## Agregace 1min → 15min
|
||||
|
||||
Prováděna PostgreSQL funkcí `ems.fn_fill_audit_interval()` a `ems.fn_fill_baseline_consumption()`.
|
||||
Spouštěna každých 15 minut jako scheduled task (Python APScheduler nebo pg_cron).
|
||||
Prováděna PostgreSQL funkcí `ems.fn_fill_audit_interval()` a navazujícími
|
||||
funkcemi pro baseline/accuracy. Spouští ji Python APScheduler: audit filler v
|
||||
minutách `:01,:16,:31,:46`, forecast accuracy v `:02,:17,:32,:47`.
|
||||
|
||||
```sql
|
||||
-- Příklad agregace telemetrie na 15min průměr
|
||||
@@ -161,12 +171,13 @@ Nad `ems.telemetry_inverter` běží dva **continuous aggregate** (TimescaleDB);
|
||||
|
||||
```env
|
||||
TELEMETRY_POLL_INTERVAL_SEC=60
|
||||
TELEMETRY_ERROR_WARN_THRESHOLD=3 # počet chyb před WARNING logem
|
||||
TELEMETRY_ERROR_RECONNECT_THRESHOLD=10
|
||||
MODBUS_CONNECT_TIMEOUT_SEC=5
|
||||
MODBUS_READ_TIMEOUT_SEC=3
|
||||
```
|
||||
|
||||
`TELEMETRY_POLL_INTERVAL_SEC` a chybové prahy zatím nejsou v kódu používány;
|
||||
smyčka běží každých 60 s přímo v `run_telemetry_loop_wrapper`.
|
||||
|
||||
---
|
||||
|
||||
## Otevřené body
|
||||
|
||||
@@ -45,12 +45,12 @@ Věci bez kterých nelze bezpečně napojit fyzická zařízení, spustit smyslu
|
||||
|
||||
| Popis | Kde | Kdo |
|
||||
|-------|-----|-----|
|
||||
| Doplnit **GPS** (`latitude`, `longitude`) pro lokalitu `home-01` – vstup Open-Meteo. | `db/migration/V003__seed_site_home01.sql` ř. 11–17 (`INSERT` + komentáře TODO); `docs/06-open-questions.md` ř. 15–16 | majitel (souřadnice) → programátor (úprava seedu/SQL) |
|
||||
| Ověřit **GPS** (`latitude`, `longitude`) pro lokalitu `home-01` proti skutečné instalaci – v seedu už jsou vyplněné hodnoty, ale forecast na nich stojí. | `db/migration/V003__seed_site_home01.sql` (`INSERT INTO ems.site`) | majitel (ověření souřadnic) → programátor |
|
||||
| Doplnit **skutečné IP** Waveshare (Deye), obou Teltonika WB, Samsung TČ a **Loxone**; ověřit **Modbus Unit ID** u zařízení. | `db/migration/V003__seed_site_home01.sql` ř. 27–30, 33–36, 39–41, 44–46, 49–52 (TODO komentáře); `docs/04-modules/telemetry.md` ř. 215 (ověření Unit ID) | majitel / instalatér (síť) → programátor (seed nebo `site_endpoint` v DB) |
|
||||
| Doplnit **azimut a sklon** FVE polí A a B pro přesný výpočet predikce. | `db/migration/V003__seed_site_home01.sql` ř. 125–132, 140–146; `docs/06-open-questions.md` ř. 13–14; `docs/04-modules/forecast.md` ř. 16–17 (tabulka TBD), 177 | majitel / projektant FVE → programátor |
|
||||
| Ověřit **azimut a sklon** FVE polí A a B proti skutečné instalaci. Seed aktuálně obsahuje `pv-a` 184°/22° a `pv-b` 184°/35° v kompasové konvenci `0=N, 90=E, 180=S, 270=W`. | `db/migration/V003__seed_site_home01.sql` (`asset_pv_array` seed); `docs/04-modules/forecast.md` | majitel / projektant FVE → programátor |
|
||||
| Doplnit **model TČ**, **jmenovitý topný výkon (W)**, **COP rated**, **objem zásobníku TUV**, **odkaz na čidlo TUV** v seedu (`asset_heat_pump` má povinné numerické sloupce – bez platných hodnot nelze konzistentně plánovat / migrovat). | `db/migration/V003__seed_site_home01.sql` ř. 182–200 | majitel (datasheet) → programátor |
|
||||
| **Rozhodnout Teltonika: OCPP 1.6 vs REST API** před implementací EV řízení a sběru. | `docs/06-open-questions.md` ř. 9–10; `docs/04-modules/consumption.md` ř. 184 | majitel + programátor |
|
||||
| **Doplnit přesné Modbus registry** (čtení i zápis) pro Deye, Teltonika, Samsung – bez mapy registrů nejde napsat funkční `telemetry_collector` / `control_exporter`. | `docs/04-modules/telemetry.md` ř. 63, 76–105 (tabulky TBD), 212–214; `docs/04-modules/heat-pump.md` ř. 79–85, 102; `docs/04-modules/control.md` ř. 249–251; pseudokód `TBD_*_REGISTER` ř. 166–171, 192–197; `docs/loxone-integration.md` ř. 259–261 | majitel dodá PDF/šablony → programátor; část ověření s **Loxone programátor** |
|
||||
| **Doplnit přesné Modbus registry** pro Teltonika a Samsung a držet je jako TODO v kódu. Deye čtení a hlavní Deye řízení už jsou implementované přes registry popsané v `modbus-registers.md`; stále ověřit fyzické hodnoty pro konkrétní instalaci. | `docs/04-modules/telemetry.md`; `docs/04-modules/heat-pump.md`; `docs/04-modules/control.md`; `docs/loxone-integration.md`; `backend/services/telemetry_collector.py`; `backend/services/control/exporter_monolith.py` | majitel dodá PDF/šablony → programátor; část ověření s **Loxone programátor** |
|
||||
| Ověřit **Modbus registr Output Power Limit** (curtailment pole A) na Deye SUN-20K. | `docs/04-modules/planning.md` ř. 422 | programátor (+ dokumentace od majitele) |
|
||||
| Doplnit **skutečnou sazbu zeleného bonusu** do `asset_pv_array.green_bonus_czk_kwh` pro `pv-b` (aktuální placeholder: **7.135** Kč/kWh – ověřit ze smlouvy s EG.D). | `db/migration/V017__green_bonus.sql` (seed `pv-b`) | majitel (smlouva) → programátor |
|
||||
| Doplnit **`green_bonus_meter_code`** (EAN zeleného elektroměru) pro `pv-b` v `asset_pv_array`. | `db/migration/V017__green_bonus.sql` / přímá úprava DB | majitel → programátor |
|
||||
|
||||
@@ -10,9 +10,9 @@ Tento soubor slouží jako živý seznam věcí které je potřeba rozhodnout p
|
||||
|
||||
- [ ] **Kurz EUR/CZK** – Fixní hodnota v konfiguraci nebo denní stahování z ČNB API? Ovlivňuje `price_importer.py`.
|
||||
|
||||
- [ ] **Azimut a sklon FVE polí** – Doplnit přesné hodnoty pro home-01 (pole A). Nutné pro `forecast_service.py`.
|
||||
- [ ] **Azimut a sklon FVE polí** – Ověřit hodnoty v seedu pro home-01 (`pv-a` 184°/22°, `pv-b` 184°/35°). Nutné pro `forecast_service.py`; konvence je kompasově/pvlib `0=N, 90=E, 180=S, 270=W`.
|
||||
|
||||
- [ ] **GPS souřadnice lokality home-01** – Nutné pro Open-Meteo API (lat/lon).
|
||||
- [ ] **GPS souřadnice lokality home-01** – V seedu jsou vyplněné; ověřit proti skutečné instalaci, protože jsou vstupem pro Open-Meteo API.
|
||||
|
||||
---
|
||||
|
||||
@@ -20,7 +20,7 @@ Tento soubor slouží jako živý seznam věcí které je potřeba rozhodnout p
|
||||
|
||||
- [ ] **Dvě úrovně min SoC v DB** – Dnes jedno `min_soc_percent` (provozní podlaha pro LP i TOU PASSIVE). Budoucí oddělení „tvrdé BMS minimum“ vs „plánovací minimum“ by vyžadovalo nový sloupec nebo politiku per site.
|
||||
|
||||
- [ ] **TUV výkon** – Je TUV výkon měřitelný zvlášť nebo jen ON/OFF? Pokud jen ON/OFF, použijeme `asset_flexible_device.max_power_w` jako aproximaci.
|
||||
- [ ] **TUV výkon** – Je TUV výkon měřitelný zvlášť nebo jen ON/OFF? Pokud jen ON/OFF, použijeme `asset_heat_pump.rated_heating_power_w` jako aproximaci.
|
||||
|
||||
- [ ] **Pole B (ongridový)** – Zahrnout do auditu jako "neřízená výroba"? Nebo ignorovat úplně? Komplikuje audit ale zpřesňuje ho.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user