Initial commit

Made-with: Cursor
This commit is contained in:
Dusan Vojacek
2026-03-20 13:27:37 +01:00
commit 8b4af663d8
77 changed files with 13337 additions and 0 deletions

430
docs/03-data-model.md Normal file
View File

@@ -0,0 +1,430 @@
# 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,
reserve_soc_percent NUMERIC(5,2) DEFAULT 20, -- rezerva pro výpadek
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).
```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
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é.
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');
```
### `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),
PRIMARY KEY (run_id, interval_start)
);
```
---
## 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),
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()
);
```