# 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 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ě. ```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` 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__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() ); ```