Files
ems/docs/03-data-model.md
Dusan Vojacek eb8dd0368f
Some checks failed
deploy / deploy (push) Failing after 1m42s
test / smoke-test (push) Successful in 2s
fix telemtrie na dahsbaordu (15min misto 1h)
2026-04-10 20:48:41 +02:00

453 lines
16 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.
# 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**
---
## 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.
```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 1112
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ě.
```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__vw_telemetry_15m_7d.sql`**, repeatable).
PostgREST role `ems_anon``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__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()
);
```