458 lines
17 KiB
Markdown
458 lines
17 KiB
Markdown
# EMS Platform – Data Model
|
||
|
||
## Principy
|
||
|
||
- Vše je vztaženo ke `site` (lokalitě)
|
||
- Raw tržní data jsou sdílená, efektivní ceny se dopočítávají per site
|
||
- Telemetrie a plány mají vždy `site_id` + `interval_start`
|
||
- TimescaleDB hypertable pro časové série (telemetrie, ceny, plány)
|
||
- Primární časová granularita: **15 minut**
|
||
- Čtení a doménová logika z DB preferuj **`ems.fn_*`** a **`ems.vw_*`** (SQL-first; detail a výjimky v `CLAUDE.md` → sekce SQL-first).
|
||
|
||
---
|
||
|
||
## Konfigurace lokalit
|
||
|
||
### `site`
|
||
Základní entita. Jedna instalace = jeden objekt.
|
||
|
||
```sql
|
||
CREATE TABLE site (
|
||
id SERIAL PRIMARY KEY,
|
||
code TEXT UNIQUE NOT NULL, -- např. 'home-01'
|
||
name TEXT NOT NULL,
|
||
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
|
||
active BOOLEAN NOT NULL DEFAULT true,
|
||
notes TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
```
|
||
|
||
### `site_endpoint`
|
||
Komunikační endpointy – každá lokalita může mít více.
|
||
|
||
```sql
|
||
CREATE TABLE site_endpoint (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
endpoint_type TEXT NOT NULL, -- 'modbus_tcp', 'loxone_http', 'teltonika_api'
|
||
host TEXT NOT NULL,
|
||
port INT,
|
||
protocol TEXT, -- 'modbus_tcp', 'http', 'https'
|
||
auth_reference TEXT, -- odkaz na secret / env proměnnou
|
||
enabled BOOLEAN DEFAULT true,
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `site_market_config`
|
||
Obchodní konfigurace s maržemi. Platnost od-do umožňuje historické sledování změn.
|
||
|
||
```sql
|
||
CREATE TABLE site_market_config (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
purchase_pricing_mode TEXT NOT NULL DEFAULT 'spot', -- 'spot', 'fixed', 'hybrid'
|
||
sale_pricing_mode TEXT NOT NULL DEFAULT 'spot',
|
||
buy_margin_fixed_czk NUMERIC(10,4) DEFAULT 0, -- Kč/kWh
|
||
buy_margin_percent NUMERIC(6,4) DEFAULT 0,
|
||
sell_margin_fixed_czk NUMERIC(10,4) DEFAULT 0, -- Kč/kWh (záporná = srážka)
|
||
sell_margin_percent NUMERIC(6,4) DEFAULT 0,
|
||
currency TEXT NOT NULL DEFAULT 'CZK',
|
||
valid_from TIMESTAMPTZ NOT NULL,
|
||
valid_to TIMESTAMPTZ, -- NULL = aktuálně platný
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `site_grid_connection`
|
||
Síťová omezení lokality.
|
||
|
||
Migrace **V074** přidává **`block_export_on_negative_sell`** (boolean, default false): v LP při záporné efektivní prodejní ceně tvrdě **`grid_export == 0`**. Použít u lokalit typu KV1 (fixní nákup, bez neriťitelného přetoku pole B); u **home-01** obvykle nechat **false**, aby řešení zůstalo proveditelné při přebytku z pole B.
|
||
|
||
```sql
|
||
CREATE TABLE site_grid_connection (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id) UNIQUE,
|
||
max_import_power_w INT NOT NULL,
|
||
max_export_power_w INT NOT NULL DEFAULT 0,
|
||
no_export BOOLEAN DEFAULT false,
|
||
reserved_capacity_w INT DEFAULT 0,
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Aktiva
|
||
|
||
### `asset_inverter`
|
||
|
||
```sql
|
||
CREATE TABLE asset_inverter (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
code TEXT NOT NULL,
|
||
manufacturer TEXT, -- 'Deye'
|
||
model TEXT, -- 'SUN-20K-SG01LP1-EU'
|
||
endpoint_id INT REFERENCES site_endpoint(id),
|
||
max_charge_power_w INT,
|
||
max_discharge_power_w INT,
|
||
max_export_power_w INT,
|
||
controllable BOOLEAN DEFAULT true, -- false = ongridový autonomní
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `asset_battery`
|
||
|
||
```sql
|
||
CREATE TABLE asset_battery (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
inverter_id INT REFERENCES asset_inverter(id),
|
||
code TEXT NOT NULL,
|
||
usable_capacity_wh INT NOT NULL, -- 64000
|
||
min_soc_percent NUMERIC(5,2) DEFAULT 10, -- provozní spodní mez LP + clamp; u paralelních packů často 11–12
|
||
reserve_soc_percent NUMERIC(5,2) DEFAULT 20, -- ekonomická podlaha; MILP export (ge≥1 W) drží soc[t] ≥ tato rezerva (arb_base_wh)
|
||
max_soc_percent NUMERIC(5,2) DEFAULT 95,
|
||
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
|
||
);
|
||
```
|
||
|
||
### `asset_pv_array`
|
||
Každé FVE pole zvlášť – důležité pro predikci (azimut, sklon). **Zelený bonus** (dotace za vyrobenou elektřinu) se váže na **úroveň pole**, ne na celou lokalitu: které pole má bonus, jaká je sazba Kč/kWh a platnost, určují sloupce `green_bonus_*`. Výpočet příjmu za interval probíhá funkcí `ems.fn_green_bonus_revenue` z výroby v Wh; není součástí efektivní prodejní ceny ze sítě.
|
||
|
||
**Deye reg 340 (max solar power, W):** strop pro řiditelné DC pole A na hybridu počítá `ems.fn_inverter_pv_a_max_w(inverter_id)` jako **součet `nominal_power_wp`** řádků s `controllable = true` vázaných na daný invertor. Exportér (`exporter_monolith.write_inverter_setpoints`) tento cap použije jen u `asset_inverter.manufacturer` = Deye a **pouze pokud součet > 0**; při součtu 0 se reg 340 z EMS nezapisuje (nezasahuje do ručního nastavení v invertoru).
|
||
|
||
```sql
|
||
CREATE TABLE asset_pv_array (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
inverter_id INT REFERENCES asset_inverter(id),
|
||
code TEXT NOT NULL,
|
||
name TEXT,
|
||
nominal_power_wp INT NOT NULL, -- 10000
|
||
azimuth_deg NUMERIC(6,2), -- 0=S, 90=Z, -90=V
|
||
tilt_deg NUMERIC(5,2),
|
||
module_count INT,
|
||
shading_factor NUMERIC(4,3) DEFAULT 1.0,
|
||
controllable BOOLEAN DEFAULT false, -- ongridový = false
|
||
-- zelený bonus (NULL = pole bez bonusu)
|
||
green_bonus_czk_kwh NUMERIC(10,6), -- sazba Kč/kWh za vyrobenou kWh
|
||
green_bonus_valid_from DATE, -- platnost od (včetně)
|
||
green_bonus_valid_to DATE, -- platnost do (exclusive), NULL = dosud
|
||
green_bonus_meter_code TEXT, -- EAN / číslo zeleného elektroměru (audit)
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `asset_ev_charger`
|
||
EV nabíječky. Na první instalaci jsou 2× Teltonika.
|
||
|
||
```sql
|
||
CREATE TABLE asset_ev_charger (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
code TEXT NOT NULL,
|
||
manufacturer TEXT, -- 'Teltonika'
|
||
model TEXT, -- 'TeltoCharge'
|
||
endpoint_id INT REFERENCES site_endpoint(id),
|
||
max_power_w INT NOT NULL, -- 22000
|
||
min_power_w INT DEFAULT 1380, -- min pro jednofázové nabíjení
|
||
phases INT DEFAULT 3,
|
||
connector_count INT DEFAULT 1,
|
||
schedulable BOOLEAN DEFAULT true,
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `asset_flexible_device`
|
||
Generická tabulka pro ostatní flexibilní spotřebiče (TUV, tepelné čerpadlo, ...).
|
||
|
||
```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
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Tržní data
|
||
|
||
### `market_interval_price`
|
||
Raw spotové ceny OTE CZ. Bez vazby na lokalitu – sdílené.
|
||
Backend stahuje OTE **jednou** za naplánovaný import (data platí pro všechny site); efektivní ceny per site jen přes `site_market_config` a view.
|
||
TimescaleDB hypertable.
|
||
|
||
```sql
|
||
CREATE TABLE market_interval_price (
|
||
market_source TEXT NOT NULL DEFAULT 'OTE_CZ',
|
||
interval_start TIMESTAMPTZ NOT NULL,
|
||
interval_end TIMESTAMPTZ NOT NULL,
|
||
buy_raw_price_czk_kwh NUMERIC(10,6), -- nákupní raw cena Kč/kWh
|
||
sell_raw_price_czk_kwh NUMERIC(10,6), -- prodejní raw cena (ref. cena)
|
||
currency TEXT DEFAULT 'CZK',
|
||
imported_at TIMESTAMPTZ DEFAULT now(),
|
||
PRIMARY KEY (market_source, interval_start)
|
||
);
|
||
-- SELECT create_hypertable('market_interval_price', 'interval_start');
|
||
```
|
||
|
||
### View: `market_vw_site_effective_price`
|
||
Efektivní ceny per site – dopočítané z raw + marže. Neukládá se, počítá se za běhu.
|
||
|
||
```sql
|
||
CREATE VIEW market_vw_site_effective_price AS
|
||
SELECT
|
||
smc.site_id,
|
||
mip.interval_start,
|
||
mip.interval_end,
|
||
mip.buy_raw_price_czk_kwh,
|
||
mip.sell_raw_price_czk_kwh,
|
||
-- efektivní nákupní cena
|
||
mip.buy_raw_price_czk_kwh
|
||
+ smc.buy_margin_fixed_czk
|
||
+ (mip.buy_raw_price_czk_kwh * smc.buy_margin_percent / 100)
|
||
AS effective_buy_price_czk_kwh,
|
||
-- efektivní prodejní cena
|
||
mip.sell_raw_price_czk_kwh
|
||
+ smc.sell_margin_fixed_czk
|
||
+ (mip.sell_raw_price_czk_kwh * smc.sell_margin_percent / 100)
|
||
AS effective_sell_price_czk_kwh
|
||
FROM market_interval_price mip
|
||
CROSS JOIN site_market_config smc
|
||
WHERE smc.valid_to IS NULL -- aktuálně platná konfigurace
|
||
OR now() BETWEEN smc.valid_from AND smc.valid_to;
|
||
```
|
||
|
||
---
|
||
|
||
## Telemetrie
|
||
|
||
### `telemetry_inverter`
|
||
Raw měření ze střídače Deye (Modbus). 1min granularita, hypertable.
|
||
|
||
```sql
|
||
CREATE TABLE telemetry_inverter (
|
||
site_id INT REFERENCES site(id),
|
||
inverter_id INT REFERENCES asset_inverter(id),
|
||
measured_at TIMESTAMPTZ NOT NULL,
|
||
-- výroba
|
||
pv_power_w INT, -- celkový výkon FVE (oba stringy)
|
||
-- baterie
|
||
battery_soc_percent NUMERIC(5,2),
|
||
battery_power_w INT, -- kladné = nabíjení, záporné = vybíjení
|
||
battery_voltage_v NUMERIC(7,3),
|
||
-- síť
|
||
grid_power_w INT, -- kladné = import, záporné = export
|
||
grid_voltage_v NUMERIC(7,3),
|
||
-- spotřeba
|
||
load_power_w INT, -- celková spotřeba objektu
|
||
-- teploty
|
||
inverter_temp_c NUMERIC(5,2),
|
||
-- provozní stav
|
||
operating_mode TEXT, -- raw hodnota z Modbus registru
|
||
fault_code INT,
|
||
PRIMARY KEY (inverter_id, measured_at)
|
||
);
|
||
-- SELECT create_hypertable('telemetry_inverter', 'measured_at');
|
||
```
|
||
|
||
### Continuous aggregates a views (výkon střídače pro UI)
|
||
|
||
Z `telemetry_inverter` počítá TimescaleDB materializované agregáty (viz `db/migration/V011__indexes_and_aggregates.sql`, `V039__telemetry_inverter_15m_aggregate.sql`):
|
||
|
||
- **`telemetry_inverter_hourly`** – hodinové průměry + `LAST(battery_soc_percent, measured_at)`; čtení přes view **`vw_telemetry_hourly_7d`**.
|
||
- **`telemetry_inverter_15m`** – čtvrthodinové bucket odpovídající 15min slotům EMS; čtení přes **`vw_telemetry_15m_7d`** (definice v **`db/views/R__071_vw_telemetry_15m_7d.sql`**, repeatable).
|
||
|
||
PostgREST role `ems_anon` má `SELECT` na tyto views (ne na samotné CA); u view nad CA je `security_invoker = false`, stejně jako u `vw_telemetry_hourly_7d` (viz `db/views/R__072_z_postgrest_ems_anon_grants.sql`).
|
||
|
||
### `telemetry_ev_charger`
|
||
Stav EV nabíječek.
|
||
|
||
```sql
|
||
CREATE TABLE telemetry_ev_charger (
|
||
site_id INT REFERENCES site(id),
|
||
charger_id INT REFERENCES asset_ev_charger(id),
|
||
measured_at TIMESTAMPTZ NOT NULL,
|
||
connector_id INT DEFAULT 1,
|
||
status TEXT, -- 'available', 'charging', 'faulted', ...
|
||
power_w INT,
|
||
energy_kwh NUMERIC(10,3), -- kumulativní
|
||
current_a NUMERIC(7,3),
|
||
voltage_v NUMERIC(7,3),
|
||
session_id TEXT,
|
||
PRIMARY KEY (charger_id, connector_id, measured_at)
|
||
);
|
||
-- SELECT create_hypertable('telemetry_ev_charger', 'measured_at');
|
||
```
|
||
|
||
---
|
||
|
||
## Predikce výroby
|
||
|
||
### `forecast_pv_run`
|
||
Každý běh predikce je jeden záznam (kdy se spustil, jaký model, jaké pole).
|
||
|
||
```sql
|
||
CREATE TABLE forecast_pv_run (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
pv_array_id INT REFERENCES asset_pv_array(id), -- NULL = celá lokalita
|
||
forecast_source TEXT NOT NULL, -- 'open_meteo', 'solcast', 'manual'
|
||
model_params JSONB, -- parametry modelu
|
||
horizon_start TIMESTAMPTZ NOT NULL,
|
||
horizon_end TIMESTAMPTZ NOT NULL,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
status TEXT DEFAULT 'ok' -- 'ok', 'partial', 'failed'
|
||
);
|
||
```
|
||
|
||
### `forecast_pv_interval`
|
||
Samotná predikovaná data, 15min granularita.
|
||
|
||
```sql
|
||
CREATE TABLE forecast_pv_interval (
|
||
run_id INT REFERENCES forecast_pv_run(id),
|
||
pv_array_id INT REFERENCES asset_pv_array(id),
|
||
interval_start TIMESTAMPTZ NOT NULL,
|
||
power_w INT NOT NULL, -- predikovaný výkon
|
||
irradiance_wm2 NUMERIC(8,2), -- GHI ze weather service
|
||
temp_c NUMERIC(5,2),
|
||
PRIMARY KEY (run_id, pv_array_id, interval_start)
|
||
);
|
||
-- SELECT create_hypertable('forecast_pv_interval', 'interval_start');
|
||
```
|
||
|
||
---
|
||
|
||
## Plánování
|
||
|
||
### `planning_run`
|
||
Jeden plánovací běh per site.
|
||
|
||
```sql
|
||
CREATE TABLE planning_run (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
horizon_start TIMESTAMPTZ NOT NULL,
|
||
horizon_end TIMESTAMPTZ NOT NULL,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
status TEXT DEFAULT 'draft', -- 'draft', 'approved', 'active', 'superseded'
|
||
solver_params JSONB,
|
||
notes TEXT
|
||
);
|
||
```
|
||
|
||
### `planning_interval`
|
||
Výstup plánování – jeden řádek = jeden 15min slot.
|
||
|
||
```sql
|
||
CREATE TABLE planning_interval (
|
||
run_id INT REFERENCES planning_run(id),
|
||
interval_start TIMESTAMPTZ NOT NULL,
|
||
-- baterie
|
||
battery_setpoint_w INT, -- kladné = nabíjení, záporné = vybíjení
|
||
battery_soc_target_pct NUMERIC(5,2),
|
||
-- grid
|
||
grid_setpoint_w INT, -- kladné = import, záporné = export
|
||
-- EV (agregát za všechny nabíječky na site)
|
||
ev_charge_power_w INT,
|
||
-- TUV
|
||
tuv_enabled BOOLEAN,
|
||
-- ekonomika
|
||
expected_cost_czk NUMERIC(10,4),
|
||
effective_buy_price NUMERIC(10,6),
|
||
effective_sell_price NUMERIC(10,6),
|
||
-- + sloupce z migrací (curtailment, EV1/2, predicted price, vstupy solveru):
|
||
-- load_baseline_w, pv_a_forecast_raw_w, pv_b_forecast_raw_w,
|
||
-- pv_a_forecast_solver_w, pv_b_forecast_solver_w
|
||
PRIMARY KEY (run_id, interval_start)
|
||
);
|
||
```
|
||
|
||
Tabulka `baseline_load_forecast_accuracy` (migrace V027+) ukládá zpětně plánovaný bazál vs. skutečný bazál z auditu; plní `fn_fill_baseline_load_forecast_accuracy` po `fn_fill_audit_interval`.
|
||
|
||
---
|
||
|
||
## Audit
|
||
|
||
### `audit_interval`
|
||
Skutečnost vs plán, 15min granularita. Plněno zpětně po dostupnosti dat.
|
||
|
||
```sql
|
||
CREATE TABLE audit_interval (
|
||
site_id INT REFERENCES site(id),
|
||
interval_start TIMESTAMPTZ NOT NULL,
|
||
planning_run_id INT REFERENCES planning_run(id),
|
||
-- skutečnost (z telemetrie, průměr/agregát za 15min)
|
||
actual_pv_power_w INT,
|
||
actual_battery_power_w INT,
|
||
actual_grid_power_w INT,
|
||
actual_load_power_w INT,
|
||
actual_battery_soc NUMERIC(5,2),
|
||
-- odchylky
|
||
deviation_grid_w INT, -- actual - planned
|
||
actual_cost_czk NUMERIC(10,4),
|
||
pv_b_production_wh NUMERIC(10,3), -- výroba bonusových polí (Wh / interval), podklad pro bonus
|
||
green_bonus_czk NUMERIC(10,4), -- příjem zeleného bonusu (fn_green_bonus_revenue), mimo actual_cost_czk
|
||
PRIMARY KEY (site_id, interval_start)
|
||
);
|
||
-- SELECT create_hypertable('audit_interval', 'interval_start');
|
||
```
|
||
|
||
---
|
||
|
||
## Spotřeba
|
||
|
||
### `consumption_baseline_interval`
|
||
Bazální (neflexibilní) spotřeba – historická a predikovaná, 15min.
|
||
|
||
```sql
|
||
CREATE TABLE consumption_baseline_interval (
|
||
site_id INT REFERENCES site(id),
|
||
interval_start TIMESTAMPTZ NOT NULL,
|
||
data_type TEXT NOT NULL, -- 'actual', 'forecast'
|
||
power_w INT NOT NULL,
|
||
source TEXT, -- 'measured', 'model_v1', ...
|
||
PRIMARY KEY (site_id, data_type, interval_start)
|
||
);
|
||
```
|
||
|
||
### 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`.
|
||
|
||
---
|
||
|
||
## Override
|
||
|
||
### `site_override`
|
||
Manuální přepisy pro provozní stavy.
|
||
|
||
```sql
|
||
CREATE TABLE site_override (
|
||
id SERIAL PRIMARY KEY,
|
||
site_id INT REFERENCES site(id),
|
||
override_type TEXT NOT NULL, -- 'force_charge', 'force_discharge', 'block_export', 'manual_setpoint'
|
||
value_json JSONB, -- parametry přepisu
|
||
valid_from TIMESTAMPTZ NOT NULL,
|
||
valid_to TIMESTAMPTZ,
|
||
reason TEXT,
|
||
created_by TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
```
|