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

16 KiB
Raw Blame History

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.

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.

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.

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.

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

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

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ě.

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.

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, ...).

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.

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.

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.

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_anonSELECT 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.

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).

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.

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.

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.

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.

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.

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.

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()
);