Initial commit
Made-with: Cursor
This commit is contained in:
608
db/migration/V001__init_schema.sql
Normal file
608
db/migration/V001__init_schema.sql
Normal file
@@ -0,0 +1,608 @@
|
||||
-- =============================================================
|
||||
-- V001__init_schema.sql
|
||||
-- EMS Platform – inicializace schématu a všech tabulek
|
||||
-- =============================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS ems;
|
||||
|
||||
-- ============================================================
|
||||
-- LOKALITY
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.site (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
|
||||
latitude NUMERIC(9,6),
|
||||
longitude NUMERIC(9,6),
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site IS 'Lokalita / jeden objekt v systému EMS.';
|
||||
COMMENT ON COLUMN ems.site.id IS 'Primární klíč lokality.';
|
||||
COMMENT ON COLUMN ems.site.code IS 'Krátký unikátní kód lokality. Příklad: home-01.';
|
||||
COMMENT ON COLUMN ems.site.name IS 'Lidsky čitelný název lokality.';
|
||||
COMMENT ON COLUMN ems.site.timezone IS 'Časová zóna IANA. Výchozí Europe/Prague.';
|
||||
COMMENT ON COLUMN ems.site.latitude IS 'Zeměpisná šířka – vstup pro weather API a výpočty irradiance.';
|
||||
COMMENT ON COLUMN ems.site.longitude IS 'Zeměpisná délka – vstup pro weather API a výpočty irradiance.';
|
||||
COMMENT ON COLUMN ems.site.active IS 'Pokud false, lokalita se přeskočí při plánování a sběru dat.';
|
||||
COMMENT ON COLUMN ems.site.notes IS 'Volné poznámky.';
|
||||
COMMENT ON COLUMN ems.site.created_at IS 'Čas vytvoření záznamu.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.site_endpoint (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
endpoint_type TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INT,
|
||||
protocol TEXT,
|
||||
unit_id INT,
|
||||
auth_reference TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_endpoint IS 'Komunikační endpointy lokality. Jedna lokalita může mít více endpointů různých typů.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.id IS 'Primární klíč endpointu.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.endpoint_type IS 'Typ endpointu: modbus_tcp, loxone_http, http_api.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.host IS 'IP adresa nebo hostname cílového zařízení (Waveshare, Loxone).';
|
||||
COMMENT ON COLUMN ems.site_endpoint.port IS 'TCP port. Modbus TCP typicky 502, Loxone 80.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.protocol IS 'Protokol: modbus_tcp, http, https.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.unit_id IS 'Modbus Unit ID (slave address). Relevantní pro modbus_tcp.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.auth_reference IS 'Název env proměnné nebo secret s přihlašovacími údaji.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.enabled IS 'Pokud false, endpoint se nepoužívá.';
|
||||
COMMENT ON COLUMN ems.site_endpoint.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.site_market_config (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
purchase_pricing_mode TEXT NOT NULL DEFAULT 'spot',
|
||||
sale_pricing_mode TEXT NOT NULL DEFAULT 'spot',
|
||||
buy_margin_fixed_czk NUMERIC(10,4) NOT NULL DEFAULT 0,
|
||||
buy_margin_percent NUMERIC(6,4) NOT NULL DEFAULT 0,
|
||||
sell_margin_fixed_czk NUMERIC(10,4) NOT NULL DEFAULT 0,
|
||||
sell_margin_percent NUMERIC(6,4) NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT 'CZK',
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_market_config IS 'Obchodní konfigurace lokality s maržemi. valid_from/valid_to umožňuje historii změn.';
|
||||
COMMENT ON COLUMN ems.site_market_config.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.site_market_config.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.site_market_config.purchase_pricing_mode IS 'Režim nákupní ceny: spot, fixed, hybrid.';
|
||||
COMMENT ON COLUMN ems.site_market_config.sale_pricing_mode IS 'Režim prodejní ceny: spot, fixed, hybrid.';
|
||||
COMMENT ON COLUMN ems.site_market_config.buy_margin_fixed_czk IS 'Fixní nákupní marže Kč/kWh přičítaná k raw ceně.';
|
||||
COMMENT ON COLUMN ems.site_market_config.buy_margin_percent IS 'Procentní nákupní marže aplikovaná na raw cenu.';
|
||||
COMMENT ON COLUMN ems.site_market_config.sell_margin_fixed_czk IS 'Fixní prodejní marže Kč/kWh. Záporná = srážka z prodejní ceny.';
|
||||
COMMENT ON COLUMN ems.site_market_config.sell_margin_percent IS 'Procentní prodejní marže.';
|
||||
COMMENT ON COLUMN ems.site_market_config.currency IS 'Měna konfigurace.';
|
||||
COMMENT ON COLUMN ems.site_market_config.valid_from IS 'Začátek platnosti konfigurace.';
|
||||
COMMENT ON COLUMN ems.site_market_config.valid_to IS 'Konec platnosti. NULL = aktuálně platný záznam.';
|
||||
COMMENT ON COLUMN ems.site_market_config.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.site_grid_connection (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id) UNIQUE,
|
||||
max_import_power_w INT NOT NULL,
|
||||
max_export_power_w INT NOT NULL DEFAULT 0,
|
||||
no_export BOOLEAN NOT NULL DEFAULT false,
|
||||
reserved_capacity_w INT NOT NULL DEFAULT 0,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_grid_connection IS 'Síťová omezení připojení lokality k distribuční síti.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.site_id IS 'Vazba na lokalitu. Každá lokalita má nejvýše jeden záznam.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.max_import_power_w IS 'Maximální povolený odběr ze sítě v W dle jističe/smlouvy.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.max_export_power_w IS 'Maximální povolený export do sítě v W. 0 = export zakázán.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.no_export IS 'Pokud true, export do sítě je zakázán bez ohledu na max_export_power_w.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.reserved_capacity_w IS 'Výkon rezervovaný pro interní potřeby, odečítá se z dostupné kapacity.';
|
||||
COMMENT ON COLUMN ems.site_grid_connection.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.asset_inverter (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
code TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
endpoint_id INT REFERENCES ems.site_endpoint(id),
|
||||
max_charge_power_w INT,
|
||||
max_discharge_power_w INT,
|
||||
max_export_power_w INT,
|
||||
controllable BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_inverter IS 'Střídač / hybridní měnič na lokalitě.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.code IS 'Kód aktiva, unikátní v rámci lokality.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.manufacturer IS 'Výrobce. Příklad: Deye.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.model IS 'Model. Příklad: SUN-20K-SG01LP1-EU.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.endpoint_id IS 'Modbus TCP endpoint přes Waveshare.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.max_charge_power_w IS 'Maximální nabíjecí výkon baterie v W.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.max_discharge_power_w IS 'Maximální vybíjecí výkon baterie v W.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.max_export_power_w IS 'Maximální výkon exportu do sítě v W.';
|
||||
COMMENT ON COLUMN ems.asset_inverter.controllable IS 'Pokud false, střídač není řízen EMS (ongridový na GEN portu).';
|
||||
COMMENT ON COLUMN ems.asset_inverter.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.asset_battery (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
inverter_id INT NOT NULL REFERENCES ems.asset_inverter(id),
|
||||
code TEXT NOT NULL,
|
||||
usable_capacity_wh INT NOT NULL,
|
||||
min_soc_percent NUMERIC(5,2) NOT NULL DEFAULT 10,
|
||||
reserve_soc_percent NUMERIC(5,2) NOT NULL DEFAULT 20,
|
||||
max_soc_percent NUMERIC(5,2) NOT NULL DEFAULT 95,
|
||||
charge_efficiency NUMERIC(5,4) NOT NULL DEFAULT 0.95,
|
||||
discharge_efficiency NUMERIC(5,4) NOT NULL DEFAULT 0.95,
|
||||
degradation_cost_czk_kwh NUMERIC(8,4) NOT NULL DEFAULT 0.5
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_battery IS 'Bateriový systém připojený ke střídači.';
|
||||
COMMENT ON COLUMN ems.asset_battery.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.asset_battery.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.asset_battery.inverter_id IS 'Střídač ke kterému je baterie připojena.';
|
||||
COMMENT ON COLUMN ems.asset_battery.code IS 'Kód aktiva.';
|
||||
COMMENT ON COLUMN ems.asset_battery.usable_capacity_wh IS 'Použitelná kapacita baterie v Wh bez rezerv výrobce.';
|
||||
COMMENT ON COLUMN ems.asset_battery.min_soc_percent IS 'Minimální SoC v % – absolutní spodní limit, nikdy nepřekročit.';
|
||||
COMMENT ON COLUMN ems.asset_battery.reserve_soc_percent IS 'Rezervní SoC v % – zachován pro výpadky sítě, nevyužívat v běžném plánu.';
|
||||
COMMENT ON COLUMN ems.asset_battery.max_soc_percent IS 'Maximální SoC v % – horní limit pro denní provoz.';
|
||||
COMMENT ON COLUMN ems.asset_battery.charge_efficiency IS 'Účinnost nabíjení (0–1). Typicky 0.95.';
|
||||
COMMENT ON COLUMN ems.asset_battery.discharge_efficiency IS 'Účinnost vybíjení (0–1). Typicky 0.95.';
|
||||
COMMENT ON COLUMN ems.asset_battery.degradation_cost_czk_kwh IS 'Odhadovaný náklad degradace v Kč/kWh cyklu. Vstupuje do optimalizace jako cena za použití baterie.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.asset_pv_array (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
inverter_id INT REFERENCES ems.asset_inverter(id),
|
||||
code TEXT NOT NULL,
|
||||
name TEXT,
|
||||
nominal_power_wp INT NOT NULL,
|
||||
azimuth_deg NUMERIC(6,2),
|
||||
tilt_deg NUMERIC(5,2),
|
||||
module_count INT,
|
||||
shading_factor NUMERIC(4,3) NOT NULL DEFAULT 1.0,
|
||||
controllable BOOLEAN NOT NULL DEFAULT false,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_pv_array IS 'FVE pole. Každé pole jako samostatný záznam – klíčové pro přesnou predikci výroby.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.inverter_id IS 'Střídač ke kterému je pole připojeno.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.code IS 'Kód pole, unikátní v rámci lokality. Příklad: pv-a, pv-b.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.name IS 'Popis pole. Příklad: Jižní střecha.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.nominal_power_wp IS 'Nominální výkon pole v Wp (součet výkonů panelů).';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.azimuth_deg IS 'Azimut panelů ve stupních. 0=jih, 90=západ, -90=východ.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.tilt_deg IS 'Sklon panelů od horizontály ve stupních.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.module_count IS 'Počet panelů v poli.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.shading_factor IS 'Koeficient stínění (0–1). 1.0 = bez stínění.';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.controllable IS 'Pokud false, pole není řízeno EMS (ongridový autonomní střídač na GEN portu).';
|
||||
COMMENT ON COLUMN ems.asset_pv_array.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.asset_ev_charger (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
code TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
endpoint_id INT REFERENCES ems.site_endpoint(id),
|
||||
max_power_w INT NOT NULL,
|
||||
min_power_w INT NOT NULL DEFAULT 1380,
|
||||
phases INT NOT NULL DEFAULT 3,
|
||||
connector_count INT NOT NULL DEFAULT 1,
|
||||
schedulable BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_ev_charger IS 'EV nabíjecí stanice. Komunikace přes Modbus TCP (Waveshare převodník).';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.code IS 'Kód aktiva, unikátní v rámci lokality.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.manufacturer IS 'Výrobce nabíječky. Příklad: Teltonika.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.model IS 'Model nabíječky. Příklad: TeltoCharge 22kW.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.endpoint_id IS 'Modbus TCP endpoint přes Waveshare převodník.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.max_power_w IS 'Maximální výkon nabíječky v W.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.min_power_w IS 'Minimální výkon nabíjení v W. 1380 W = 6A jednofázové minimum IEC 61851.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.phases IS 'Počet fází nabíjení.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.connector_count IS 'Počet konektorů na nabíječce.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.schedulable IS 'Pokud true, nabíječka je zapojená do plánování EMS.';
|
||||
COMMENT ON COLUMN ems.asset_ev_charger.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.asset_heat_pump (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
code TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
endpoint_id INT REFERENCES ems.site_endpoint(id),
|
||||
rated_heating_power_w INT NOT NULL,
|
||||
cop_rated NUMERIC(4,2),
|
||||
cop_temp_reference_c NUMERIC(5,2),
|
||||
min_run_duration_min INT NOT NULL DEFAULT 30,
|
||||
min_stop_duration_min INT NOT NULL DEFAULT 15,
|
||||
tuv_tank_volume_l INT,
|
||||
tuv_min_temp_c NUMERIC(5,2) NOT NULL DEFAULT 45,
|
||||
tuv_max_temp_c NUMERIC(5,2) NOT NULL DEFAULT 60,
|
||||
tuv_target_temp_c NUMERIC(5,2) NOT NULL DEFAULT 55,
|
||||
tuv_temp_sensor_ref TEXT,
|
||||
schedulable BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_heat_pump IS 'Tepelné čerpadlo s Modbus řízením (Samsung). Řízeno na základě COP, venkovní teploty a stavu zásobníku TUV.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.code IS 'Kód aktiva, unikátní v rámci lokality.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.manufacturer IS 'Výrobce tepelného čerpadla. Příklad: Samsung.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.model IS 'Model tepelného čerpadla.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.endpoint_id IS 'Modbus TCP endpoint přes Waveshare převodník.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.rated_heating_power_w IS 'Jmenovitý topný výkon v W při referenčních podmínkách.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.cop_rated IS 'Jmenovitý COP při referenční teplotě cop_temp_reference_c.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.cop_temp_reference_c IS 'Referenční venkovní teplota pro jmenovitý COP. Typicky 7°C (norma A7/W35).';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.min_run_duration_min IS 'Minimální nepřerušená doba běhu v minutách. Chrání kompresor před krátkými cykly.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.min_stop_duration_min IS 'Minimální doba stání po vypnutí v minutách. Ochrana kompresoru při restartu.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.tuv_tank_volume_l IS 'Objem zásobníku TUV v litrech. Slouží k výpočtu doby ohřevu.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.tuv_min_temp_c IS 'Minimální teplota TUV zásobníku v °C. Pod touto hodnotou se ohřev spustí bez ohledu na cenu.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.tuv_max_temp_c IS 'Maximální teplota TUV zásobníku v °C. Nad touto hodnotou se ohřev zastaví.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.tuv_target_temp_c IS 'Cílová teplota TUV pro normální plánovaný provoz.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.tuv_temp_sensor_ref IS 'Název nebo kód čidla teploty zásobníku (Modbus registr nebo Loxone Virtual Output).';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.schedulable IS 'Pokud true, tepelné čerpadlo je zapojeno do plánování EMS.';
|
||||
COMMENT ON COLUMN ems.asset_heat_pump.notes IS 'Volné poznámky.';
|
||||
|
||||
-- ============================================================
|
||||
-- TRŽNÍ DATA
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.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) NOT NULL,
|
||||
sell_raw_price_czk_kwh NUMERIC(10,6) NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'CZK',
|
||||
imported_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (market_source, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.market_interval_price IS 'Raw spotové ceny elektřiny OTE CZ. Sdílené pro všechny lokality, bez marží. Granularita 15 minut.';
|
||||
COMMENT ON COLUMN ems.market_interval_price.market_source IS 'Zdroj tržních dat. Příklad: OTE_CZ.';
|
||||
COMMENT ON COLUMN ems.market_interval_price.interval_start IS 'Začátek 15minutového intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.market_interval_price.interval_end IS 'Konec 15minutového intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.market_interval_price.buy_raw_price_czk_kwh IS 'Raw nákupní cena v Kč/kWh bez marží.';
|
||||
COMMENT ON COLUMN ems.market_interval_price.sell_raw_price_czk_kwh IS 'Raw prodejní referenční cena v Kč/kWh bez marží. Pro OTE CZ = DAM cena.';
|
||||
COMMENT ON COLUMN ems.market_interval_price.currency IS 'Měna cen záznamu.';
|
||||
COMMENT ON COLUMN ems.market_interval_price.imported_at IS 'Čas importu záznamu do DB. Slouží pro audit importů.';
|
||||
|
||||
-- ============================================================
|
||||
-- TELEMETRIE
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.telemetry_inverter (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
inverter_id INT NOT NULL REFERENCES ems.asset_inverter(id),
|
||||
measured_at TIMESTAMPTZ NOT NULL,
|
||||
pv_power_w INT,
|
||||
battery_soc_percent NUMERIC(5,2),
|
||||
battery_power_w INT,
|
||||
battery_voltage_v NUMERIC(7,3),
|
||||
grid_power_w INT,
|
||||
grid_voltage_v NUMERIC(7,3),
|
||||
load_power_w INT,
|
||||
inverter_temp_c NUMERIC(5,2),
|
||||
operating_mode TEXT,
|
||||
fault_code INT,
|
||||
PRIMARY KEY (inverter_id, measured_at)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.telemetry_inverter IS 'Telemetrie ze střídače Deye čtená přes Modbus TCP. 1min granularita. TimescaleDB hypertable.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.inverter_id IS 'Vazba na střídač.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.measured_at IS 'Čas měření (UTC).';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.pv_power_w IS 'Celkový okamžitý výkon FVE v W (součet všech stringů čtených tímto střídačem, včetně GEN portu).';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.battery_soc_percent IS 'Aktuální stav nabití baterie v %.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.battery_power_w IS 'Výkon baterie v W. Kladné = nabíjení, záporné = vybíjení.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.battery_voltage_v IS 'Napětí bateriového systému v V.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.grid_power_w IS 'Výkon přenosu se sítí v W. Kladné = import ze sítě, záporné = export do sítě.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.grid_voltage_v IS 'Napětí sítě v V.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.load_power_w IS 'Celková spotřeba objektu v W (vše za AC výstupem střídače, včetně EV a TUV).';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.inverter_temp_c IS 'Teplota střídače v °C.';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.operating_mode IS 'Provozní režim dle Modbus registru (raw hodnota pro ladění).';
|
||||
COMMENT ON COLUMN ems.telemetry_inverter.fault_code IS 'Kód chyby z Modbus registru. 0 = bez chyby.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.telemetry_ev_charger (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
charger_id INT NOT NULL REFERENCES ems.asset_ev_charger(id),
|
||||
measured_at TIMESTAMPTZ NOT NULL,
|
||||
connector_id INT NOT NULL DEFAULT 1,
|
||||
status TEXT,
|
||||
power_w INT,
|
||||
energy_kwh NUMERIC(10,3),
|
||||
current_a NUMERIC(7,3),
|
||||
voltage_v NUMERIC(7,3),
|
||||
session_id TEXT,
|
||||
error_code TEXT,
|
||||
PRIMARY KEY (charger_id, connector_id, measured_at)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.telemetry_ev_charger IS 'Telemetrie EV nabíječky Teltonika čtená přes Modbus TCP (Waveshare). 1min granularita. TimescaleDB hypertable.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.charger_id IS 'Vazba na EV nabíječku.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.measured_at IS 'Čas měření (UTC).';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.connector_id IS 'Číslo konektoru (1-based). Teltonika TeltoCharge má 1 konektor.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.status IS 'Stav konektoru dle OCPP: available, preparing, charging, suspended_ev, suspended_evse, finishing, faulted.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.power_w IS 'Aktuální nabíjecí výkon v W.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.energy_kwh IS 'Kumulativní energie aktuální session v kWh. Resetuje se při nové session.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.current_a IS 'Nabíjecí proud v A.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.voltage_v IS 'Napětí v bodě připojení v V.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.session_id IS 'Identifikátor aktuální nabíjecí session z Modbus registru.';
|
||||
COMMENT ON COLUMN ems.telemetry_ev_charger.error_code IS 'Kód chyby z Modbus registru nabíječky.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.telemetry_heat_pump (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
heat_pump_id INT NOT NULL REFERENCES ems.asset_heat_pump(id),
|
||||
measured_at TIMESTAMPTZ NOT NULL,
|
||||
outdoor_temp_c NUMERIC(5,2),
|
||||
water_inlet_temp_c NUMERIC(5,2),
|
||||
water_outlet_temp_c NUMERIC(5,2),
|
||||
tuv_tank_temp_c NUMERIC(5,2),
|
||||
power_w INT,
|
||||
operating_mode TEXT,
|
||||
cop_actual NUMERIC(4,2),
|
||||
defrost_active BOOLEAN,
|
||||
alarm_code INT,
|
||||
PRIMARY KEY (heat_pump_id, measured_at)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.telemetry_heat_pump IS 'Telemetrie tepelného čerpadla Samsung čtená přes Modbus TCP (Waveshare). 1min granularita. TimescaleDB hypertable.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.heat_pump_id IS 'Vazba na tepelné čerpadlo.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.measured_at IS 'Čas měření (UTC).';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.outdoor_temp_c IS 'Venkovní teplota naměřená čerpadlem v °C. Klíčový vstup pro výpočet COP a rozhodnutí o ohřevu.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.water_inlet_temp_c IS 'Teplota vody na vstupu do výměníku čerpadla v °C.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.water_outlet_temp_c IS 'Teplota vody na výstupu z čerpadla v °C.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.tuv_tank_temp_c IS 'Teplota TUV zásobníku v °C. Klíčový vstup pro rozhodnutí kdy ohřívat.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.power_w IS 'Aktuální příkon čerpadla v W.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.operating_mode IS 'Provozní režim: heating, cooling, dhw (domestic hot water), standby.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.cop_actual IS 'Aktuálně vypočtený COP pokud je dostupný z Modbus registru.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.defrost_active IS 'Příznak aktivního odmrazovacího cyklu. Při defrostu je skutečný příkon vyšší.';
|
||||
COMMENT ON COLUMN ems.telemetry_heat_pump.alarm_code IS 'Kód alarmu z Modbus registru. 0 = bez alarmu.';
|
||||
|
||||
-- ============================================================
|
||||
-- PREDIKCE
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.forecast_pv_run (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
pv_array_id INT REFERENCES ems.asset_pv_array(id),
|
||||
forecast_source TEXT NOT NULL DEFAULT 'open_meteo',
|
||||
model_params JSONB,
|
||||
horizon_start TIMESTAMPTZ NOT NULL,
|
||||
horizon_end TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
status TEXT NOT NULL DEFAULT 'ok'
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.forecast_pv_run IS 'Metadata jednoho běhu predikce výroby FVE.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.pv_array_id IS 'Konkrétní FVE pole. NULL = agregovaná predikce celé lokality.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.forecast_source IS 'Zdroj meteorologických dat: open_meteo, solcast, manual.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.model_params IS 'Parametry predikčního modelu použité při tomto běhu (JSON).';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.horizon_start IS 'Začátek predikčního horizontu.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.horizon_end IS 'Konec predikčního horizontu.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.created_at IS 'Čas spuštění predikce.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_run.status IS 'Stav běhu: ok, partial, failed.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.forecast_pv_interval (
|
||||
run_id INT NOT NULL REFERENCES ems.forecast_pv_run(id),
|
||||
pv_array_id INT NOT NULL REFERENCES ems.asset_pv_array(id),
|
||||
interval_start TIMESTAMPTZ NOT NULL,
|
||||
power_w INT NOT NULL,
|
||||
irradiance_wm2 NUMERIC(8,2),
|
||||
temp_c NUMERIC(5,2),
|
||||
PRIMARY KEY (run_id, pv_array_id, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.forecast_pv_interval IS 'Predikovaný výkon FVE po 15min intervalech. TimescaleDB hypertable.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.run_id IS 'Vazba na běh predikce.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.pv_array_id IS 'Konkrétní FVE pole.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.interval_start IS 'Začátek 15min intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.power_w IS 'Predikovaný výkon FVE pole v W.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.irradiance_wm2 IS 'GHI irradiance ze weather service v W/m² použitá při výpočtu.';
|
||||
COMMENT ON COLUMN ems.forecast_pv_interval.temp_c IS 'Predikovaná venkovní teplota v °C použitá při výpočtu.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.forecast_weather_interval (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
forecast_source TEXT NOT NULL DEFAULT 'open_meteo',
|
||||
interval_start TIMESTAMPTZ NOT NULL,
|
||||
outdoor_temp_c NUMERIC(5,2),
|
||||
irradiance_wm2 NUMERIC(8,2),
|
||||
cloud_cover_pct NUMERIC(5,2),
|
||||
wind_speed_ms NUMERIC(6,2),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (site_id, forecast_source, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.forecast_weather_interval IS 'Predikce počasí per lokalita, 15min granularita. Sdílený vstup pro FVE predikci i COP odhad tepelného čerpadla.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.forecast_source IS 'Zdroj predikce počasí.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.interval_start IS 'Začátek 15min intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.outdoor_temp_c IS 'Predikovaná venkovní teplota v °C. Klíčový vstup pro COP odhad tepelného čerpadla.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.irradiance_wm2 IS 'Predikovaná irradiance GHI v W/m².';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.cloud_cover_pct IS 'Predikovaná oblačnost v %.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.wind_speed_ms IS 'Predikovaná rychlost větru v m/s.';
|
||||
COMMENT ON COLUMN ems.forecast_weather_interval.created_at IS 'Čas importu predikce do DB.';
|
||||
|
||||
-- ============================================================
|
||||
-- PLÁNOVÁNÍ
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.planning_run (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
horizon_start TIMESTAMPTZ NOT NULL,
|
||||
horizon_end TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
solver_params JSONB,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.planning_run IS 'Jeden plánovací běh pro konkrétní lokalitu a horizont.';
|
||||
COMMENT ON COLUMN ems.planning_run.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.planning_run.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.planning_run.horizon_start IS 'Začátek plánovaného horizontu (typicky dnešní půlnoc).';
|
||||
COMMENT ON COLUMN ems.planning_run.horizon_end IS 'Konec plánovaného horizontu (typicky zítřejší půlnoc nebo +48h).';
|
||||
COMMENT ON COLUMN ems.planning_run.created_at IS 'Čas vytvoření plánu.';
|
||||
COMMENT ON COLUMN ems.planning_run.status IS 'Stav plánu: draft (sestavován), approved (schválen), active (aktuálně platný), superseded (nahrazen novějším).';
|
||||
COMMENT ON COLUMN ems.planning_run.solver_params IS 'Parametry optimalizačního solveru použité při tomto běhu (JSON).';
|
||||
COMMENT ON COLUMN ems.planning_run.notes IS 'Volné poznámky k plánu.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE ems.planning_interval (
|
||||
run_id INT NOT NULL REFERENCES ems.planning_run(id),
|
||||
interval_start TIMESTAMPTZ NOT NULL,
|
||||
battery_setpoint_w INT,
|
||||
battery_soc_target_pct NUMERIC(5,2),
|
||||
grid_setpoint_w INT,
|
||||
ev_charge_power_w INT,
|
||||
heat_pump_enabled BOOLEAN,
|
||||
heat_pump_setpoint_w INT,
|
||||
expected_cost_czk NUMERIC(10,4),
|
||||
effective_buy_price NUMERIC(10,6),
|
||||
effective_sell_price NUMERIC(10,6),
|
||||
PRIMARY KEY (run_id, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.planning_interval IS 'Výstup optimalizace – jeden řádek = jeden 15min slot plánu.';
|
||||
COMMENT ON COLUMN ems.planning_interval.run_id IS 'Vazba na plánovací běh.';
|
||||
COMMENT ON COLUMN ems.planning_interval.interval_start IS 'Začátek 15min intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.planning_interval.battery_setpoint_w IS 'Plánovaný výkon baterie v W. Kladné = nabíjení, záporné = vybíjení.';
|
||||
COMMENT ON COLUMN ems.planning_interval.battery_soc_target_pct IS 'Cílový SoC baterie na konci intervalu v %.';
|
||||
COMMENT ON COLUMN ems.planning_interval.grid_setpoint_w IS 'Plánovaný výkon se sítí v W. Kladné = import, záporné = export.';
|
||||
COMMENT ON COLUMN ems.planning_interval.ev_charge_power_w IS 'Plánovaný agregovaný výkon nabíjení EV v W (součet všech nabíječek na site).';
|
||||
COMMENT ON COLUMN ems.planning_interval.heat_pump_enabled IS 'Zda má tepelné čerpadlo v tomto intervalu běžet.';
|
||||
COMMENT ON COLUMN ems.planning_interval.heat_pump_setpoint_w IS 'Plánovaný výkon tepelného čerpadla v W.';
|
||||
COMMENT ON COLUMN ems.planning_interval.expected_cost_czk IS 'Očekávané náklady intervalu v Kč. Záporné = příjem z prodeje.';
|
||||
COMMENT ON COLUMN ems.planning_interval.effective_buy_price IS 'Efektivní nákupní cena použitá při plánování v Kč/kWh.';
|
||||
COMMENT ON COLUMN ems.planning_interval.effective_sell_price IS 'Efektivní prodejní cena použitá při plánování v Kč/kWh.';
|
||||
|
||||
-- ============================================================
|
||||
-- AUDIT
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.audit_interval (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
interval_start TIMESTAMPTZ NOT NULL,
|
||||
planning_run_id INT REFERENCES ems.planning_run(id),
|
||||
actual_pv_power_w INT,
|
||||
actual_battery_power_w INT,
|
||||
actual_grid_power_w INT,
|
||||
actual_load_power_w INT,
|
||||
actual_battery_soc_pct NUMERIC(5,2),
|
||||
actual_ev_power_w INT,
|
||||
actual_heat_pump_power_w INT,
|
||||
actual_cost_czk NUMERIC(10,4),
|
||||
deviation_grid_w INT,
|
||||
deviation_cost_czk NUMERIC(10,4),
|
||||
PRIMARY KEY (site_id, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.audit_interval IS 'Skutečnost vs plán po 15min intervalech. Plněno zpětně funkcí ems.fn_fill_audit_interval().';
|
||||
COMMENT ON COLUMN ems.audit_interval.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.audit_interval.interval_start IS 'Začátek 15min intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.audit_interval.planning_run_id IS 'Plán který byl v tomto intervalu aktivní.';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_pv_power_w IS 'Skutečný výkon FVE v W (průměr za 15min z telemetrie).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_battery_power_w IS 'Skutečný výkon baterie v W (průměr za 15min).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_grid_power_w IS 'Skutečný výkon přenosu se sítí v W (průměr za 15min).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_load_power_w IS 'Skutečná celková spotřeba v W (průměr za 15min).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_battery_soc_pct IS 'Skutečný SoC baterie na konci intervalu v %.';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_ev_power_w IS 'Skutečný výkon nabíjení EV v W (suma všech nabíječek, průměr za 15min).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_heat_pump_power_w IS 'Skutečný příkon tepelného čerpadla v W (průměr za 15min).';
|
||||
COMMENT ON COLUMN ems.audit_interval.actual_cost_czk IS 'Skutečné náklady intervalu v Kč vypočtené ze skutečného grid_power a efektivní ceny.';
|
||||
COMMENT ON COLUMN ems.audit_interval.deviation_grid_w IS 'Odchylka skutečný - plánovaný výkon sítě v W.';
|
||||
COMMENT ON COLUMN ems.audit_interval.deviation_cost_czk IS 'Odchylka skutečných nákladů od plánovaných v Kč.';
|
||||
|
||||
-- ============================================================
|
||||
-- SPOTŘEBA
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.consumption_baseline_interval (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
interval_start TIMESTAMPTZ NOT NULL,
|
||||
data_type TEXT NOT NULL,
|
||||
power_w INT NOT NULL,
|
||||
source TEXT,
|
||||
PRIMARY KEY (site_id, data_type, interval_start)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.consumption_baseline_interval IS 'Bazální (neflexibilní) spotřeba po 15min intervalech. Historická skutečnost i predikce.';
|
||||
COMMENT ON COLUMN ems.consumption_baseline_interval.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.consumption_baseline_interval.interval_start IS 'Začátek 15min intervalu (UTC).';
|
||||
COMMENT ON COLUMN ems.consumption_baseline_interval.data_type IS 'Typ záznamu: actual (historická skutečnost), forecast (predikce pro plánování).';
|
||||
COMMENT ON COLUMN ems.consumption_baseline_interval.power_w IS 'Bazální spotřeba v W = celková spotřeba mínus měřitelná flexibilní zařízení.';
|
||||
COMMENT ON COLUMN ems.consumption_baseline_interval.source IS 'Metoda výpočtu: measured (z telemetrie), model_v1 (statistický model).';
|
||||
|
||||
-- ============================================================
|
||||
-- OVERRIDE
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.site_override (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
override_type TEXT NOT NULL,
|
||||
value_json JSONB,
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
reason TEXT,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_override IS 'Manuální přepisy provozních stavů. Mají přednost před automatickým plánem.';
|
||||
COMMENT ON COLUMN ems.site_override.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.site_override.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.site_override.override_type IS 'Typ přepisu: force_charge, force_discharge, block_export, manual_setpoint, block_heat_pump.';
|
||||
COMMENT ON COLUMN ems.site_override.value_json IS 'Parametry přepisu jako JSON. Obsah závisí na override_type.';
|
||||
COMMENT ON COLUMN ems.site_override.valid_from IS 'Začátek platnosti přepisu.';
|
||||
COMMENT ON COLUMN ems.site_override.valid_to IS 'Konec platnosti. NULL = platí do odvolání.';
|
||||
COMMENT ON COLUMN ems.site_override.reason IS 'Důvod přepisu pro auditní účely.';
|
||||
COMMENT ON COLUMN ems.site_override.created_by IS 'Uživatel nebo systémová komponenta která přepis vytvořila.';
|
||||
COMMENT ON COLUMN ems.site_override.created_at IS 'Čas vytvoření záznamu.';
|
||||
81
db/migration/V002__timescale_hypertables.sql
Normal file
81
db/migration/V002__timescale_hypertables.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- =============================================================
|
||||
-- V002__timescale_hypertables.sql
|
||||
-- EMS Platform – vytvoření TimescaleDB hypertable pro časové série
|
||||
-- Spouštět po V001 a po instalaci TimescaleDB extension
|
||||
-- =============================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
|
||||
-- Telemetrie střídače – 1min záznamy, partitioning po 1 týdnu
|
||||
SELECT create_hypertable(
|
||||
'ems.telemetry_inverter',
|
||||
'measured_at',
|
||||
chunk_time_interval => INTERVAL '1 week',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Telemetrie EV nabíječek
|
||||
SELECT create_hypertable(
|
||||
'ems.telemetry_ev_charger',
|
||||
'measured_at',
|
||||
chunk_time_interval => INTERVAL '1 week',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Telemetrie tepelného čerpadla
|
||||
SELECT create_hypertable(
|
||||
'ems.telemetry_heat_pump',
|
||||
'measured_at',
|
||||
chunk_time_interval => INTERVAL '1 week',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Spotové ceny – 15min záznamy, partitioning po 1 měsíci
|
||||
SELECT create_hypertable(
|
||||
'ems.market_interval_price',
|
||||
'interval_start',
|
||||
chunk_time_interval => INTERVAL '1 month',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- FVE predikce – 15min záznamy
|
||||
SELECT create_hypertable(
|
||||
'ems.forecast_pv_interval',
|
||||
'interval_start',
|
||||
chunk_time_interval => INTERVAL '1 month',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Predikce počasí
|
||||
SELECT create_hypertable(
|
||||
'ems.forecast_weather_interval',
|
||||
'interval_start',
|
||||
chunk_time_interval => INTERVAL '1 month',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Audit
|
||||
SELECT create_hypertable(
|
||||
'ems.audit_interval',
|
||||
'interval_start',
|
||||
chunk_time_interval => INTERVAL '1 month',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Bazální spotřeba
|
||||
SELECT create_hypertable(
|
||||
'ems.consumption_baseline_interval',
|
||||
'interval_start',
|
||||
chunk_time_interval => INTERVAL '1 month',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Kompresní politiky pro staré chunky
|
||||
-- Telemetrie starší 30 dní komprimovat (čtení stačí)
|
||||
-- ============================================================
|
||||
|
||||
SELECT add_compression_policy('ems.telemetry_inverter', INTERVAL '30 days', if_not_exists => TRUE);
|
||||
SELECT add_compression_policy('ems.telemetry_ev_charger', INTERVAL '30 days', if_not_exists => TRUE);
|
||||
SELECT add_compression_policy('ems.telemetry_heat_pump', INTERVAL '30 days', if_not_exists => TRUE);
|
||||
SELECT add_compression_policy('ems.market_interval_price', INTERVAL '90 days', if_not_exists => TRUE);
|
||||
203
db/migration/V003__seed_site_home01.sql
Normal file
203
db/migration/V003__seed_site_home01.sql
Normal file
@@ -0,0 +1,203 @@
|
||||
-- =============================================================
|
||||
-- V003__seed_site_home01.sql
|
||||
-- EMS Platform – seed data první lokality home-01
|
||||
-- Doplnit: latitude, longitude, IP adresy, azimuty FVE polí
|
||||
-- =============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- LOKALITA
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.site (code, name, timezone, latitude, longitude, active, notes)
|
||||
VALUES (
|
||||
'home-01',
|
||||
'Hlavní objekt',
|
||||
'Europe/Prague',
|
||||
NULL, -- TODO: doplnit GPS
|
||||
NULL, -- TODO: doplnit GPS
|
||||
true,
|
||||
'První instalace. Deye 20kW + 64kWh baterie + 2x Teltonika EV + Samsung TČ.'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- ENDPOINTY
|
||||
-- ============================================================
|
||||
|
||||
-- Deye střídač přes Waveshare RS485→TCP
|
||||
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
|
||||
SELECT id, 'modbus_tcp', '192.168.1.100', 502, 'modbus_tcp', 1, true, 'Waveshare WS-ETH pro Deye SUN-20K. Unit ID dle DIP přepínače.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
-- TODO: doplnit skutečnou IP adresy Waveshare
|
||||
|
||||
-- Teltonika EV nabíječka 1 přes Waveshare
|
||||
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
|
||||
SELECT id, 'modbus_tcp', '192.168.1.101', 502, 'modbus_tcp', 1, true, 'Waveshare pro Teltonika TeltoCharge #1.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
-- TODO: doplnit IP a unit_id
|
||||
|
||||
-- Teltonika EV nabíječka 2 přes Waveshare
|
||||
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
|
||||
SELECT id, 'modbus_tcp', '192.168.1.102', 502, 'modbus_tcp', 1, true, 'Waveshare pro Teltonika TeltoCharge #2.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
|
||||
-- Samsung tepelné čerpadlo přes Waveshare
|
||||
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
|
||||
SELECT id, 'modbus_tcp', '192.168.1.103', 502, 'modbus_tcp', 1, true, 'Waveshare pro Samsung tepelné čerpadlo.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
|
||||
-- Loxone Miniserver
|
||||
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
|
||||
SELECT id, 'loxone_http', '192.168.1.10', 80, 'http', NULL, true, 'Loxone Miniserver – příjem setpointů přes Virtual HTTP Inputs.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
-- TODO: doplnit IP Loxone
|
||||
|
||||
-- ============================================================
|
||||
-- SÍŤOVÉ PŘIPOJENÍ
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.site_grid_connection (site_id, max_import_power_w, max_export_power_w, no_export, reserved_capacity_w, notes)
|
||||
SELECT id, 22000, 20000, false, 0, 'Třífázová přípojka. Limity upřesnit dle smlouvy s distributorem.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- TRŽNÍ KONFIGURACE
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.site_market_config (
|
||||
site_id, purchase_pricing_mode, sale_pricing_mode,
|
||||
buy_margin_fixed_czk, buy_margin_percent,
|
||||
sell_margin_fixed_czk, sell_margin_percent,
|
||||
currency, valid_from, valid_to, notes
|
||||
)
|
||||
SELECT
|
||||
id, 'spot', 'spot',
|
||||
0.050, -- 5 haléřů/kWh nákupní marže (distribuce, poplatky)
|
||||
0,
|
||||
-0.020, -- 2 haléře/kWh srážka z prodejní ceny
|
||||
0,
|
||||
'CZK', now(), NULL, 'Výchozí konfigurace. Upřesnit dle skutečné smlouvy.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA – STŘÍDAČ
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.asset_inverter (site_id, code, manufacturer, model, endpoint_id, max_charge_power_w, max_discharge_power_w, max_export_power_w, controllable, notes)
|
||||
SELECT
|
||||
s.id, 'deye-main', 'Deye', 'SUN-20K-SG01LP1-EU',
|
||||
ep.id,
|
||||
20000, 20000, 20000,
|
||||
true,
|
||||
'Hlavní hybridní střídač 20kW LV. RS485 Modbus RTU přes Waveshare.'
|
||||
FROM ems.site s
|
||||
JOIN ems.site_endpoint ep ON ep.site_id = s.id AND ep.notes LIKE '%Deye%'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
-- Ongridový střídač na GEN portu (autonomní, neřídíme)
|
||||
INSERT INTO ems.asset_inverter (site_id, code, manufacturer, model, endpoint_id, max_export_power_w, controllable, notes)
|
||||
SELECT
|
||||
id, 'ongrid-gen', NULL, NULL, NULL, 10000, false,
|
||||
'Ongridový střídač zapojený do GEN portu Deye. Autonomní provoz, EMS neřídí.'
|
||||
FROM ems.site WHERE code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA – BATERIE
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.asset_battery (site_id, inverter_id, code, usable_capacity_wh, min_soc_percent, reserve_soc_percent, max_soc_percent, charge_efficiency, discharge_efficiency, degradation_cost_czk_kwh)
|
||||
SELECT
|
||||
s.id, inv.id, 'bat-main',
|
||||
64000, -- 64 kWh
|
||||
10, -- min SoC 10 %
|
||||
20, -- rezerva 20 % pro výpadek sítě
|
||||
95, -- max SoC 95 %
|
||||
0.95, 0.95,
|
||||
0.50 -- Kč/kWh degradace, upřesnit dle záruky výrobce
|
||||
FROM ems.site s
|
||||
JOIN ems.asset_inverter inv ON inv.site_id = s.id AND inv.code = 'deye-main'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA – FVE POLE
|
||||
-- ============================================================
|
||||
|
||||
-- Pole A – řízené Deye střídačem
|
||||
INSERT INTO ems.asset_pv_array (site_id, inverter_id, code, name, nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, controllable, notes)
|
||||
SELECT
|
||||
s.id, inv.id, 'pv-a', 'FVE pole A',
|
||||
10000, -- 10 kWp
|
||||
NULL, -- TODO: doplnit azimut (0=jih)
|
||||
NULL, -- TODO: doplnit sklon (stupně)
|
||||
NULL,
|
||||
1.0,
|
||||
true,
|
||||
'Hlavní FVE pole řízené Deye střídačem. Doplnit azimut a sklon.'
|
||||
FROM ems.site s
|
||||
JOIN ems.asset_inverter inv ON inv.site_id = s.id AND inv.code = 'deye-main'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
-- Pole B – ongridový, autonomní
|
||||
INSERT INTO ems.asset_pv_array (site_id, inverter_id, code, name, nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, controllable, notes)
|
||||
SELECT
|
||||
s.id, inv.id, 'pv-b', 'FVE pole B (ongrid)',
|
||||
10000,
|
||||
NULL, -- TODO: doplnit azimut
|
||||
NULL, -- TODO: doplnit sklon
|
||||
NULL,
|
||||
1.0,
|
||||
false,
|
||||
'Ongridový střídač na GEN portu Deye. EMS neřídí, výkon se projeví v telemetrii Deye jako součást pv_power_w.'
|
||||
FROM ems.site s
|
||||
JOIN ems.asset_inverter inv ON inv.site_id = s.id AND inv.code = 'ongrid-gen'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA – EV NABÍJEČKY
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.asset_ev_charger (site_id, code, manufacturer, model, endpoint_id, max_power_w, min_power_w, phases, connector_count, schedulable, notes)
|
||||
SELECT
|
||||
s.id, 'ev-charger-1', 'Teltonika', 'TeltoCharge 22kW',
|
||||
ep.id,
|
||||
22000, 1380, 3, 1, true,
|
||||
'EV nabíječka č. 1. Modbus TCP přes Waveshare. Ověřit Modbus registry z dokumentace Teltonika.'
|
||||
FROM ems.site s
|
||||
JOIN ems.site_endpoint ep ON ep.site_id = s.id AND ep.notes LIKE '%TeltoCharge #1%'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
INSERT INTO ems.asset_ev_charger (site_id, code, manufacturer, model, endpoint_id, max_power_w, min_power_w, phases, connector_count, schedulable, notes)
|
||||
SELECT
|
||||
s.id, 'ev-charger-2', 'Teltonika', 'TeltoCharge 22kW',
|
||||
ep.id,
|
||||
22000, 1380, 3, 1, true,
|
||||
'EV nabíječka č. 2. Modbus TCP přes Waveshare.'
|
||||
FROM ems.site s
|
||||
JOIN ems.site_endpoint ep ON ep.site_id = s.id AND ep.notes LIKE '%TeltoCharge #2%'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
-- ============================================================
|
||||
-- AKTIVA – TEPELNÉ ČERPADLO
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.asset_heat_pump (
|
||||
site_id, code, manufacturer, model, endpoint_id,
|
||||
rated_heating_power_w, cop_rated, cop_temp_reference_c,
|
||||
min_run_duration_min, min_stop_duration_min,
|
||||
tuv_tank_volume_l, tuv_min_temp_c, tuv_max_temp_c, tuv_target_temp_c,
|
||||
tuv_temp_sensor_ref, schedulable, notes
|
||||
)
|
||||
SELECT
|
||||
s.id, 'hp-samsung', 'Samsung', NULL, -- TODO: doplnit model
|
||||
ep.id,
|
||||
NULL, -- TODO: doplnit jmenovitý výkon W
|
||||
NULL, -- TODO: doplnit COP rated
|
||||
7.0, -- referenční teplota A7/W35
|
||||
30, 15,
|
||||
NULL, -- TODO: doplnit objem zásobníku
|
||||
45, 60, 55,
|
||||
NULL, -- TODO: doplnit odkaz na teplotní čidlo
|
||||
true,
|
||||
'Samsung tepelné čerpadlo s Modbus modulem. Řídit dle COP a venkovní teploty (výhodné kolem poledne v chladných měsících).'
|
||||
FROM ems.site s
|
||||
JOIN ems.site_endpoint ep ON ep.site_id = s.id AND ep.notes LIKE '%Samsung%'
|
||||
WHERE s.code = 'home-01';
|
||||
117
db/migration/V004__operating_modes.sql
Normal file
117
db/migration/V004__operating_modes.sql
Normal file
@@ -0,0 +1,117 @@
|
||||
-- =============================================================
|
||||
-- V004__operating_modes.sql
|
||||
-- EMS Platform – provozní režimy lokalit
|
||||
-- =============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- Číselník provozních režimů
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.operating_mode_def (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
ev_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
heat_pump_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
battery_mode TEXT NOT NULL, -- 'plan', 'self_sustain', 'charge_max', 'hold', 'none'
|
||||
grid_mode TEXT NOT NULL, -- 'plan', 'import_ok', 'no_import', 'no_export', 'none'
|
||||
loxone_mode_value INT NOT NULL, -- integer posílaný do Loxone Virtual Input
|
||||
is_autonomous BOOLEAN NOT NULL DEFAULT false, -- Loxone umí sám bez EMS setpointů
|
||||
sort_order INT NOT NULL DEFAULT 50
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.operating_mode_def IS 'Číselník provozních režimů systému. Každý režim definuje chování baterie, sítě a flexibilních zařízení.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.code IS 'Unikátní kód režimu. Příklad: AUTO, SELF_SUSTAIN.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.name IS 'Lidsky čitelný název režimu pro UI.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.description IS 'Popis chování v daném režimu.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.ev_enabled IS 'Zda je povoleno nabíjení EV v tomto režimu.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.heat_pump_enabled IS 'Zda je tepelné čerpadlo povoleno v tomto režimu.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.battery_mode IS 'Chování baterie: plan=dle EMS plánu, self_sustain=vybíjí do domu, charge_max=max nabíjení, hold=drží SoC, none=žádné akce.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.grid_mode IS 'Chování vůči síti: plan=dle EMS, import_ok=import povolen, no_import=bez importu, no_export=bez exportu, none=žádné akce.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.loxone_mode_value IS 'Celočíselná hodnota posílaná do Loxone Virtual Input EMS_Mode. Loxone interně přepíná stavový stroj.';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.is_autonomous IS 'Pokud true, Loxone zvládne tento režim bez průběžných setpointů od EMS (fallback bezpečný).';
|
||||
COMMENT ON COLUMN ems.operating_mode_def.sort_order IS 'Pořadí zobrazení v UI.';
|
||||
|
||||
INSERT INTO ems.operating_mode_def
|
||||
(code, name, description, ev_enabled, heat_pump_enabled, battery_mode, grid_mode, loxone_mode_value, is_autonomous, sort_order)
|
||||
VALUES
|
||||
('AUTO', 'Automatický', 'EMS řídí vše podle optimalizovaného plánu. Setpointy posílány každých 15 min.',
|
||||
true, true, 'plan', 'plan', 1, false, 10),
|
||||
|
||||
('SELF_SUSTAIN', 'Soběstačný', 'Fallback bez EMS. FVE + baterie pokrývají spotřebu. Žádný import, žádný export, EV a TČ odstaveny.',
|
||||
false, false, 'self_sustain', 'no_export', 2, true, 20),
|
||||
|
||||
('CHARGE_CHEAP', 'Nabíjení levnou cenou', 'Manuální: max nabíjení baterie ze sítě. Použít při ručně identifikované levné ceně nebo akci.',
|
||||
false, false, 'charge_max', 'import_ok', 3, false, 30),
|
||||
|
||||
('PRESERVE', 'Ochrana baterie', 'Dovolená / servis. Baterie drží aktuální SoC, žádné akce. EV a TČ odstaveny.',
|
||||
false, false, 'hold', 'import_ok', 4, true, 40),
|
||||
|
||||
('MANUAL', 'Manuální', 'Technický přepis. EMS ani Loxone logika neřídí střídač. Pouze pro servisní práce.',
|
||||
false, false, 'none', 'none', 0, true, 50);
|
||||
|
||||
-- ============================================================
|
||||
-- Aktivní provozní režim per lokalita
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.site_operating_mode (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id) PRIMARY KEY,
|
||||
mode_code TEXT NOT NULL REFERENCES ems.operating_mode_def(code),
|
||||
activated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
activated_by TEXT,
|
||||
valid_until TIMESTAMPTZ, -- NULL = platí do ručního přepnutí
|
||||
previous_mode TEXT REFERENCES ems.operating_mode_def(code),
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_operating_mode IS 'Aktuálně aktivní provozní režim per lokalita. Jeden řádek na lokalitu (upsert při přepnutí).';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.site_id IS 'Vazba na lokalitu. PK – jedna lokalita má vždy právě jeden aktivní režim.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.mode_code IS 'Aktuálně aktivní režim. FK na operating_mode_def.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.activated_at IS 'Čas přepnutí do tohoto režimu.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.activated_by IS 'Kdo nebo co přepnulo režim: user:jan, system:watchdog, api, loxone.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.valid_until IS 'Automatické vypršení režimu. NULL = platí do ručního přepnutí.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.previous_mode IS 'Předchozí režim – pro rychlý návrat zpět.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode.notes IS 'Volný komentář k přepnutí.';
|
||||
|
||||
-- ============================================================
|
||||
-- Historie přepnutí režimů (audit log)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.site_operating_mode_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
mode_code TEXT NOT NULL REFERENCES ems.operating_mode_def(code),
|
||||
activated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deactivated_at TIMESTAMPTZ,
|
||||
activated_by TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_operating_mode_log IS 'Auditní log všech přepnutí provozních režimů per lokalita.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.id IS 'Primární klíč.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.site_id IS 'Vazba na lokalitu.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.mode_code IS 'Aktivovaný režim.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.activated_at IS 'Čas aktivace režimu.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.deactivated_at IS 'Čas deaktivace (přepnutí na jiný režim). NULL = stále aktivní.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.activated_by IS 'Zdroj přepnutí: user:jan, system:watchdog, system:ems_start.';
|
||||
COMMENT ON COLUMN ems.site_operating_mode_log.notes IS 'Volný komentář.';
|
||||
|
||||
-- ============================================================
|
||||
-- Heartbeat tabulka – pouze informační, pro EMS vlastní diagnostiku
|
||||
-- POZOR: Loxone watchdog NEČTE tuto tabulku.
|
||||
-- Loxone sleduje HTTP pulzy přímo (nezávisle na DB).
|
||||
-- Tato tabulka slouží jen pro EMS dashboard a alerting.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.site_heartbeat (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id) PRIMARY KEY,
|
||||
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
ems_version TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'ok'
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.site_heartbeat IS 'Informační záznam posledního úspěšného heartbeat pulzu EMS per lokalita. Slouží pouze pro EMS dashboard a alerting – Loxone watchdog tuto tabulku nečte a nezávisí na ní. Fallback logika je implementována čistě v Loxone (viz docs/loxone-integration.md).';
|
||||
COMMENT ON COLUMN ems.site_heartbeat.site_id IS 'Vazba na lokalitu. PK.';
|
||||
COMMENT ON COLUMN ems.site_heartbeat.last_seen IS 'Čas kdy EMS backend naposledy úspěšně odeslal pulz do Loxone.';
|
||||
COMMENT ON COLUMN ems.site_heartbeat.ems_version IS 'Verze EMS backendu pro diagnostiku.';
|
||||
COMMENT ON COLUMN ems.site_heartbeat.status IS 'Stav EMS backendu: ok, degraded (částečný výpadek), error (kritická chyba).';
|
||||
51
db/migration/V005__planning_curtailment.sql
Normal file
51
db/migration/V005__planning_curtailment.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
-- =============================================================
|
||||
-- V005__planning_curtailment.sql
|
||||
-- EMS Platform – rozšíření plánování o curtailment FVE pole A
|
||||
-- a zelený bonus pole B
|
||||
-- =============================================================
|
||||
|
||||
-- Přidat curtailment výsledek do planning_interval
|
||||
ALTER TABLE ems.planning_interval
|
||||
ADD COLUMN IF NOT EXISTS pv_a_curtailed_w INT NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN ems.planning_interval.pv_a_curtailed_w IS
|
||||
'Plánované omezení výroby FVE pole A v W rozhodnuté LP solverem. '
|
||||
'0 = žádné omezení výroby. Hodnota > 0 znamená že Deye dostane příkaz '
|
||||
'omezit Output Power Limit na (pv_a_forecast_w - pv_a_curtailed_w).';
|
||||
|
||||
-- Přidat zelený bonus do audit_interval pro správnou ekonomiku
|
||||
ALTER TABLE ems.audit_interval
|
||||
ADD COLUMN IF NOT EXISTS pv_b_production_wh NUMERIC(10,3),
|
||||
ADD COLUMN IF NOT EXISTS green_bonus_czk NUMERIC(10,4);
|
||||
|
||||
COMMENT ON COLUMN ems.audit_interval.pv_b_production_wh IS
|
||||
'Skutečná výroba FVE pole B v Wh za 15min interval. '
|
||||
'Odvozena z telemetrie: celkový pv_power_w minus výroba pole A (pokud měřena odděleně). '
|
||||
'Slouží pro výpočet nároku na zelený bonus.';
|
||||
|
||||
COMMENT ON COLUMN ems.audit_interval.green_bonus_czk IS
|
||||
'Příjem ze zeleného bonusu za výrobu pole B v Kč. '
|
||||
'Vypočteno jako pv_b_production_wh / 1000 * green_bonus_czk_kwh z site_market_config. '
|
||||
'Zahrnovat do celkových nákladů/příjmů lokality.';
|
||||
|
||||
-- Rozšíření site_market_config o zelený bonus
|
||||
ALTER TABLE ems.site_market_config
|
||||
ADD COLUMN IF NOT EXISTS green_bonus_czk_kwh NUMERIC(8,4) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS green_bonus_asset_code TEXT;
|
||||
|
||||
COMMENT ON COLUMN ems.site_market_config.green_bonus_czk_kwh IS
|
||||
'Výše zeleného bonusu (dotace) v Kč/kWh za vyrobenou elektřinu z FVE pole s dotací. '
|
||||
'Bonus se vztahuje vždy na výrobu bez ohledu na cenu nebo způsob využití energie.';
|
||||
|
||||
COMMENT ON COLUMN ems.site_market_config.green_bonus_asset_code IS
|
||||
'Kód FVE pole (asset_pv_array.code) na které se zelený bonus vztahuje. '
|
||||
'Příklad: pv-b. NULL = bonus se nevztahuje na žádné konkrétní pole.';
|
||||
|
||||
-- Seed: doplnit zelený bonus pro home-01
|
||||
-- (hodnota bonusu bude upřesněna dle smlouvy s OTE/ERU)
|
||||
UPDATE ems.site_market_config
|
||||
SET
|
||||
green_bonus_czk_kwh = 1.20, -- TODO: doplnit skutečnou výši bonusu ze smlouvy
|
||||
green_bonus_asset_code = 'pv-b'
|
||||
WHERE site_id = (SELECT id FROM ems.site WHERE code = 'home-01')
|
||||
AND valid_to IS NULL;
|
||||
119
db/migration/V006__vehicles.sql
Normal file
119
db/migration/V006__vehicles.sql
Normal file
@@ -0,0 +1,119 @@
|
||||
-- =============================================================
|
||||
-- V006__vehicles.sql
|
||||
-- EMS Platform – vozidla a EV session tracking
|
||||
-- =============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- Vozidla
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.asset_vehicle (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
code TEXT NOT NULL,
|
||||
name TEXT,
|
||||
make TEXT, -- 'Tesla', 'Renault'
|
||||
model TEXT, -- 'Model Y', 'Zoe R135'
|
||||
battery_capacity_kwh NUMERIC(6,2) NOT NULL, -- kapacita trakční baterie
|
||||
max_charge_power_w INT NOT NULL, -- max přijímaný AC výkon vozidla
|
||||
default_charger_id INT REFERENCES ems.asset_ev_charger(id),
|
||||
api_type TEXT NOT NULL DEFAULT 'none', -- 'tesla', 'none'
|
||||
api_reference TEXT, -- klíč do env/secrets pro API
|
||||
default_target_soc_pct NUMERIC(5,2) NOT NULL DEFAULT 80,
|
||||
default_deadline_hour INT NOT NULL DEFAULT 7, -- hodina rána
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
UNIQUE (site_id, code)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.asset_vehicle IS 'Vozidla registrovaná v EMS. Každé vozidlo má výchozí cílový SoC a deadline pro plánování nabíjení.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.battery_capacity_kwh IS 'Celková kapacita trakční baterie vozidla v kWh. Tesla Model Y ~75 kWh, Renault Zoe R135 ~52 kWh.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.max_charge_power_w IS 'Maximální výkon který vozidlo přijme přes AC nabíjení. Může být nižší než výkon WB. Tesla MY ~11 000 W, Zoe R135 ~7 400 W.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.api_type IS 'Typ API pro čtení stavu vozidla. tesla = Tesla API (Tessie nebo přímé), none = žádné API.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.api_reference IS 'Název env proměnné nebo klíče v secrets kde je uložen API token/přihlášení. Např. TESLA_MY_TOKEN.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.default_target_soc_pct IS 'Výchozí cílový SoC pro deadline charging. Uživatel může přepsat v UI před odjezdem.';
|
||||
COMMENT ON COLUMN ems.asset_vehicle.default_deadline_hour IS 'Výchozí hodina rána do které musí být dosažen target SoC. 7 = 07:00.';
|
||||
|
||||
-- ============================================================
|
||||
-- EV session tracking
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE ems.ev_session (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
charger_id INT NOT NULL REFERENCES ems.asset_ev_charger(id),
|
||||
vehicle_id INT REFERENCES ems.asset_vehicle(id), -- NULL pokud neznámé vozidlo
|
||||
session_start TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
session_end TIMESTAMPTZ,
|
||||
-- Stav při připojení (pokud znám)
|
||||
soc_at_connect_pct NUMERIC(5,2),
|
||||
-- Deadline požadavek
|
||||
target_soc_pct NUMERIC(5,2),
|
||||
target_deadline TIMESTAMPTZ,
|
||||
-- Průběžný stav (aktualizován z telemetrie)
|
||||
energy_delivered_wh NUMERIC(12,3) NOT NULL DEFAULT 0,
|
||||
last_power_w INT,
|
||||
-- Výsledek při odpojení
|
||||
soc_at_disconnect_pct NUMERIC(5,2),
|
||||
total_cost_czk NUMERIC(10,4),
|
||||
deadline_met BOOLEAN, -- byl deadline splněn?
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.ev_session IS 'Záznamy jednotlivých nabíjecích sessions EV. Session začíná připojením (status != available na WB) a končí odpojením.';
|
||||
COMMENT ON COLUMN ems.ev_session.vehicle_id IS 'Identifikace vozidla. Může být NULL pokud EMS neví které auto je připojeno (Zoe nemá API).';
|
||||
COMMENT ON COLUMN ems.ev_session.soc_at_connect_pct IS 'SoC baterie vozidla při připojení. Pro Tesla čteno z API, pro Zoe NULL (neznáme).';
|
||||
COMMENT ON COLUMN ems.ev_session.target_soc_pct IS 'Cílový SoC pro tuto session. Výchozí z asset_vehicle.default_target_soc_pct, uživatel může změnit v UI.';
|
||||
COMMENT ON COLUMN ems.ev_session.target_deadline IS 'Deadline pro dosažení target_soc_pct. Výchozí = dnešní/zítřejší default_deadline_hour.';
|
||||
COMMENT ON COLUMN ems.ev_session.energy_delivered_wh IS 'Kumulativní energie dodaná v této session v Wh. Čteno z WB Modbus (kWh counter). Slouží i pro odhad SoC u Zoe.';
|
||||
COMMENT ON COLUMN ems.ev_session.deadline_met IS 'True pokud byl target_soc_pct dosažen před target_deadline. NULL pokud session ještě probíhá.';
|
||||
|
||||
-- Hypertable pro session log (sessions jsou časová série)
|
||||
-- Poznámka: ev_session není hypertable – sessions jsou krátké záznamy, ne telemetrie.
|
||||
-- Index pro rychlé dotazy na aktivní session
|
||||
CREATE INDEX idx_ev_session_active
|
||||
ON ems.ev_session (charger_id, session_end)
|
||||
WHERE session_end IS NULL;
|
||||
|
||||
CREATE INDEX idx_ev_session_site_time
|
||||
ON ems.ev_session (site_id, session_start DESC);
|
||||
|
||||
-- ============================================================
|
||||
-- Seed – vozidla home-01
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO ems.asset_vehicle
|
||||
(site_id, code, name, make, model,
|
||||
battery_capacity_kwh, max_charge_power_w,
|
||||
default_charger_id, api_type,
|
||||
default_target_soc_pct, default_deadline_hour)
|
||||
SELECT
|
||||
s.id,
|
||||
'tesla-my', 'Tesla Model Y', 'Tesla', 'Model Y',
|
||||
75.0,
|
||||
11000, -- Tesla MY AC max ~11kW (3fáze × 16A × 230V)
|
||||
ch1.id,
|
||||
'none', -- Tesla API fáze 2, zatím none
|
||||
80, -- 80% výchozí target – Tesla má velkou baterii
|
||||
7 -- do 7:00 ráno
|
||||
FROM ems.site s
|
||||
JOIN ems.asset_ev_charger ch1 ON ch1.site_id = s.id AND ch1.code = 'ev-charger-1'
|
||||
WHERE s.code = 'home-01';
|
||||
|
||||
INSERT INTO ems.asset_vehicle
|
||||
(site_id, code, name, make, model,
|
||||
battery_capacity_kwh, max_charge_power_w,
|
||||
default_charger_id, api_type,
|
||||
default_target_soc_pct, default_deadline_hour)
|
||||
SELECT
|
||||
s.id,
|
||||
'zoe-r135', 'Renault Zoe R135', 'Renault', 'Zoe R135',
|
||||
52.0,
|
||||
7400, -- Zoe R135 max 7.4kW AC (jednofáze 32A nebo 3fáze nižší)
|
||||
ch2.id,
|
||||
'none', -- Zoe nemá API
|
||||
90, -- 90% výchozí target – Zoe má menší baterii, kritičtější
|
||||
7
|
||||
FROM ems.site s
|
||||
JOIN ems.asset_ev_charger ch2 ON ch2.site_id = s.id AND ch2.code = 'ev-charger-2'
|
||||
WHERE s.code = 'home-01';
|
||||
91
db/migration/V007__rolling_replanning.sql
Normal file
91
db/migration/V007__rolling_replanning.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- =============================================================
|
||||
-- V007__rolling_replanning.sql
|
||||
-- EMS Platform – rozšíření planning_run o rolling horizon typ
|
||||
-- a forecast korekční faktory
|
||||
-- =============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- Rozšíření planning_run o typ a kontext replanningu
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE ems.planning_run
|
||||
ADD COLUMN IF NOT EXISTS run_type TEXT NOT NULL DEFAULT 'daily',
|
||||
ADD COLUMN IF NOT EXISTS triggered_by TEXT,
|
||||
ADD COLUMN IF NOT EXISTS replan_from TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS soc_at_replan_wh NUMERIC(10,2),
|
||||
ADD COLUMN IF NOT EXISTS solver_duration_ms INT,
|
||||
ADD COLUMN IF NOT EXISTS forecast_correction_factor NUMERIC(6,4);
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.run_type IS
|
||||
'Typ plánovacího běhu:
|
||||
daily = hlavní denní plán (15:00, horizont 36h)
|
||||
rolling = průběžný replan každých 15min
|
||||
manual = spuštěno ručně z UI nebo API';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.triggered_by IS
|
||||
'Co spustilo tento plánovací běh:
|
||||
scheduler:daily, scheduler:rolling, user:jan, api, override_change';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.replan_from IS
|
||||
'Od kterého slotu byl plán přepočítán (pro rolling). NULL pro daily plán.
|
||||
Sloty před replan_from jsou převzaty z předchozího aktivního plánu.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.soc_at_replan_wh IS
|
||||
'Skutečný SoC baterie v Wh v okamžiku replanningu (z telemetrie).
|
||||
Vstupní podmínka pro solver – zpřesňuje počáteční SoC oproti dennímu plánu.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.solver_duration_ms IS
|
||||
'Čas výpočtu LP solveru v milisekundách. Pro monitoring výkonu.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_run.forecast_correction_factor IS
|
||||
'Korekční faktor aplikovaný na FVE forecast při tomto replanningu.
|
||||
Vypočten z poměru skutečné vs. předpovídané výroby za posledních 60 minut.
|
||||
1.0 = žádná korekce, 0.8 = skutečnost byla 80% forecastu.';
|
||||
|
||||
-- ============================================================
|
||||
-- Rozšíření planning_interval o per-EV setpointy
|
||||
-- (nahrazuje jeden agregovaný ev_charge_power_w)
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE ems.planning_interval
|
||||
ADD COLUMN IF NOT EXISTS ev1_setpoint_w INT,
|
||||
ADD COLUMN IF NOT EXISTS ev2_setpoint_w INT,
|
||||
ADD COLUMN IF NOT EXISTS ev1_via_bat_w INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS ev2_via_bat_w INT NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN ems.planning_interval.ev1_setpoint_w IS
|
||||
'Plánovaný celkový výkon nabíjení EV nabíječky 1 (Tesla) v W. NULL = auto nepřipojeno.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_interval.ev2_setpoint_w IS
|
||||
'Plánovaný celkový výkon nabíjení EV nabíječky 2 (Zoe) v W. NULL = auto nepřipojeno.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_interval.ev1_via_bat_w IS
|
||||
'Část výkonu EV1 která jde přes baterii (round-trip ztráta). 0 = přímé napájení.';
|
||||
|
||||
COMMENT ON COLUMN ems.planning_interval.ev2_via_bat_w IS
|
||||
'Část výkonu EV2 která jde přes baterii (round-trip ztráta). 0 = přímé napájení.';
|
||||
|
||||
-- ============================================================
|
||||
-- Tabulka forecast korekcí – pro analýzu přesnosti
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ems.forecast_correction_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
logged_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
window_start TIMESTAMPTZ NOT NULL, -- začátek okna pro výpočet faktoru
|
||||
window_end TIMESTAMPTZ NOT NULL,
|
||||
actual_pv_wh NUMERIC(12,3), -- skutečná výroba za okno
|
||||
forecast_pv_wh NUMERIC(12,3), -- předpovídaná výroba za okno
|
||||
correction_factor NUMERIC(6,4), -- actual / forecast
|
||||
applied_to_run_id INT REFERENCES ems.planning_run(id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.forecast_correction_log IS
|
||||
'Log výpočtu korekčních faktorů FVE forecastu. Každý rolling replan zaznamená
|
||||
poměr skutečné vs. předpovídané výroby za posledních 60 minut. Slouží pro
|
||||
analýzu přesnosti forecastu a ladění modelu.';
|
||||
|
||||
COMMENT ON COLUMN ems.forecast_correction_log.correction_factor IS
|
||||
'Poměr actual_pv_wh / forecast_pv_wh. Hodnoty typicky 0.5–1.5.
|
||||
Extrémní hodnoty (oblačnost, porucha) jsou odfiltrovány v kódu (clamp 0.5–1.5).';
|
||||
137
db/routines/R__fn_cop_estimate.sql
Normal file
137
db/routines/R__fn_cop_estimate.sql
Normal file
@@ -0,0 +1,137 @@
|
||||
-- =============================================================
|
||||
-- R__fn_cop_estimate.sql
|
||||
-- EMS Platform – odhad COP tepelného čerpadla dle venkovní teploty
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_cop_estimate(
|
||||
p_heat_pump_id INT,
|
||||
p_outdoor_temp_c NUMERIC
|
||||
)
|
||||
RETURNS NUMERIC(4,2)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cop_rated NUMERIC;
|
||||
v_cop_ref_temp NUMERIC;
|
||||
v_cop_estimated NUMERIC;
|
||||
BEGIN
|
||||
-- Načíst referenční COP a teplotu z konfigurace čerpadla
|
||||
SELECT cop_rated, cop_temp_reference_c
|
||||
INTO v_cop_rated, v_cop_ref_temp
|
||||
FROM ems.asset_heat_pump
|
||||
WHERE id = p_heat_pump_id;
|
||||
|
||||
IF v_cop_rated IS NULL OR v_cop_ref_temp IS NULL THEN
|
||||
-- Fallback: obecný odhad pro vzduch-voda TČ bez konfigurace
|
||||
-- Zdroj: přibližná lineární závislost COP na venkovní teplotě
|
||||
-- COP ≈ 2.0 při -10°C, COP ≈ 4.5 při +15°C
|
||||
v_cop_estimated := 2.0 + (p_outdoor_temp_c + 10.0) * (4.5 - 2.0) / 25.0;
|
||||
ELSE
|
||||
-- Lineární interpolace od referenčního bodu
|
||||
-- COP klesá přibližně o 0.10 na každý stupeň poklesu venkovní teploty
|
||||
-- Toto je zjednodušený model – zpřesnit dle skutečných dat z tepelky
|
||||
v_cop_estimated := v_cop_rated + (p_outdoor_temp_c - v_cop_ref_temp) * 0.10;
|
||||
END IF;
|
||||
|
||||
-- Omezit na rozumné hodnoty (COP vzduch-voda reálně 1.5–6.0)
|
||||
v_cop_estimated := GREATEST(1.5, LEAST(6.0, v_cop_estimated));
|
||||
|
||||
RETURN ROUND(v_cop_estimated, 2);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_cop_estimate(INT, NUMERIC) IS
|
||||
'Odhadne COP tepelného čerpadla pro danou venkovní teplotu.
|
||||
Používá lineární model od referenčního bodu (cop_rated při cop_temp_reference_c).
|
||||
Výstup slouží k rozhodnutí zda je výhodné spustit TČ v daném intervalu.
|
||||
Přesnost modelu zlepšit kalibrací na historická data (cop_actual z telemetrie).
|
||||
Výsledek omezen na rozsah 1.5–6.0.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_heat_pump_cost_per_kwh_heat(
|
||||
p_heat_pump_id INT,
|
||||
p_outdoor_temp_c NUMERIC,
|
||||
p_buy_price_czk_kwh NUMERIC
|
||||
)
|
||||
RETURNS NUMERIC(8,4)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
-- Cena za 1 kWh tepla = cena elektřiny / COP
|
||||
-- Čím vyšší COP, tím levnější teplo
|
||||
SELECT ROUND(
|
||||
p_buy_price_czk_kwh / NULLIF(ems.fn_cop_estimate(p_heat_pump_id, p_outdoor_temp_c), 0),
|
||||
4
|
||||
);
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_heat_pump_cost_per_kwh_heat(INT, NUMERIC, NUMERIC) IS
|
||||
'Vypočte efektivní cenu za 1 kWh dodaného tepla v Kč.
|
||||
Vstup: ID tepelného čerpadla, venkovní teplota, nákupní cena elektřiny.
|
||||
Výstup slouží k porovnání výhodnosti ohřevu v různých časových intervalech.
|
||||
Nižší hodnota = výhodnější čas pro provoz TČ.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_heat_pump_should_run(
|
||||
p_heat_pump_id INT,
|
||||
p_interval_start TIMESTAMPTZ,
|
||||
p_outdoor_temp_c NUMERIC,
|
||||
p_tuv_tank_temp_c NUMERIC,
|
||||
p_buy_price_czk_kwh NUMERIC,
|
||||
p_max_cost_threshold NUMERIC DEFAULT 3.0 -- Kč/kWh tepla – maximální akceptovatelná cena
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_hp ems.asset_heat_pump%ROWTYPE;
|
||||
v_cost_per_kwh NUMERIC;
|
||||
v_override BOOLEAN;
|
||||
BEGIN
|
||||
SELECT * INTO v_hp FROM ems.asset_heat_pump WHERE id = p_heat_pump_id;
|
||||
|
||||
-- Kontrola override (blokování TČ)
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM ems.site_override
|
||||
WHERE site_id = v_hp.site_id
|
||||
AND override_type = 'block_heat_pump'
|
||||
AND valid_from <= p_interval_start
|
||||
AND (valid_to IS NULL OR valid_to > p_interval_start)
|
||||
) INTO v_override;
|
||||
|
||||
IF v_override THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
-- Povinný ohřev: teplota pod minimem
|
||||
IF p_tuv_tank_temp_c IS NOT NULL AND p_tuv_tank_temp_c < v_hp.tuv_min_temp_c THEN
|
||||
RETURN true;
|
||||
END IF;
|
||||
|
||||
-- Zásobník je plný
|
||||
IF p_tuv_tank_temp_c IS NOT NULL AND p_tuv_tank_temp_c >= v_hp.tuv_max_temp_c THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
-- Ekonomické rozhodnutí: spustit pokud cena tepla je pod prahem
|
||||
v_cost_per_kwh := ems.fn_heat_pump_cost_per_kwh_heat(
|
||||
p_heat_pump_id, p_outdoor_temp_c, p_buy_price_czk_kwh
|
||||
);
|
||||
|
||||
RETURN v_cost_per_kwh <= p_max_cost_threshold;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_heat_pump_should_run(INT, TIMESTAMPTZ, NUMERIC, NUMERIC, NUMERIC, NUMERIC) IS
|
||||
'Rozhodne zda má tepelné čerpadlo v daném intervalu běžet.
|
||||
Logika priorit:
|
||||
1. Pokud existuje override block_heat_pump → false.
|
||||
2. Pokud teplota zásobníku pod tuv_min_temp_c → true (povinný ohřev).
|
||||
3. Pokud zásobník nad tuv_max_temp_c → false.
|
||||
4. Jinak ekonomické rozhodnutí: spustit pokud cena tepla ≤ p_max_cost_threshold Kč/kWh.
|
||||
Výhodné časy jsou přes poledne v chladných měsících (vyšší venkovní teplota = lepší COP).';
|
||||
61
db/routines/R__fn_effective_price.sql
Normal file
61
db/routines/R__fn_effective_price.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- =============================================================
|
||||
-- R__fn_effective_price.sql
|
||||
-- EMS Platform – funkce pro výpočet efektivní ceny per site
|
||||
-- Repeatable migration – nasazuje se při každé změně
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_effective_buy_price(
|
||||
p_site_id INT,
|
||||
p_interval_start TIMESTAMPTZ
|
||||
)
|
||||
RETURNS NUMERIC(10,6)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT
|
||||
mip.buy_raw_price_czk_kwh
|
||||
+ smc.buy_margin_fixed_czk
|
||||
+ (mip.buy_raw_price_czk_kwh * smc.buy_margin_percent / 100.0)
|
||||
FROM ems.market_interval_price mip
|
||||
CROSS JOIN ems.site_market_config smc
|
||||
WHERE mip.market_source = 'OTE_CZ'
|
||||
AND mip.interval_start = p_interval_start
|
||||
AND smc.site_id = p_site_id
|
||||
AND smc.valid_from <= p_interval_start
|
||||
AND (smc.valid_to IS NULL OR smc.valid_to > p_interval_start)
|
||||
ORDER BY smc.valid_from DESC
|
||||
LIMIT 1;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_effective_buy_price(INT, TIMESTAMPTZ) IS
|
||||
'Vrátí efektivní nákupní cenu elektřiny v Kč/kWh pro danou lokalitu a 15min interval.
|
||||
Přičítá fixní a procentní nákupní marži dle aktuálně platné site_market_config.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_effective_sell_price(
|
||||
p_site_id INT,
|
||||
p_interval_start TIMESTAMPTZ
|
||||
)
|
||||
RETURNS NUMERIC(10,6)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT
|
||||
mip.sell_raw_price_czk_kwh
|
||||
+ smc.sell_margin_fixed_czk
|
||||
+ (mip.sell_raw_price_czk_kwh * smc.sell_margin_percent / 100.0)
|
||||
FROM ems.market_interval_price mip
|
||||
CROSS JOIN ems.site_market_config smc
|
||||
WHERE mip.market_source = 'OTE_CZ'
|
||||
AND mip.interval_start = p_interval_start
|
||||
AND smc.site_id = p_site_id
|
||||
AND smc.valid_from <= p_interval_start
|
||||
AND (smc.valid_to IS NULL OR smc.valid_to > p_interval_start)
|
||||
ORDER BY smc.valid_from DESC
|
||||
LIMIT 1;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_effective_sell_price(INT, TIMESTAMPTZ) IS
|
||||
'Vrátí efektivní prodejní cenu elektřiny v Kč/kWh pro danou lokalitu a 15min interval.
|
||||
Aplikuje fixní a procentní prodejní marži (záporná marže = srážka z prodejní ceny).';
|
||||
171
db/routines/R__fn_fill_audit_interval.sql
Normal file
171
db/routines/R__fn_fill_audit_interval.sql
Normal file
@@ -0,0 +1,171 @@
|
||||
-- =============================================================
|
||||
-- R__fn_fill_audit_interval.sql
|
||||
-- EMS Platform – plnění audit_interval ze skutečné telemetrie
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_fill_audit_interval(
|
||||
p_site_id INT,
|
||||
p_interval_start TIMESTAMPTZ
|
||||
)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_interval_end TIMESTAMPTZ := p_interval_start + INTERVAL '15 minutes';
|
||||
v_run_id INT;
|
||||
v_avg_pv_power_w INT;
|
||||
v_avg_battery_power_w INT;
|
||||
v_avg_grid_power_w INT;
|
||||
v_avg_load_power_w INT;
|
||||
v_last_soc NUMERIC(5,2);
|
||||
v_sum_ev_power_w INT;
|
||||
v_avg_hp_power_w INT;
|
||||
v_plan ems.planning_interval%ROWTYPE;
|
||||
v_buy_price NUMERIC;
|
||||
v_sell_price NUMERIC;
|
||||
v_actual_cost NUMERIC := NULL;
|
||||
BEGIN
|
||||
-- Najít aktivní plán pro tento interval
|
||||
SELECT pi.* INTO v_plan
|
||||
FROM ems.planning_interval pi
|
||||
JOIN ems.planning_run pr ON pr.id = pi.run_id
|
||||
WHERE pr.site_id = p_site_id
|
||||
AND pi.interval_start = p_interval_start
|
||||
AND pr.status IN ('active', 'superseded')
|
||||
ORDER BY pr.created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
v_run_id := v_plan.run_id;
|
||||
|
||||
-- Agregovat telemetrii střídače (průměr za 15min; agregace bez GROUP BY vrací vždy 1 řádek)
|
||||
SELECT
|
||||
AVG(pv_power_w)::INT,
|
||||
AVG(battery_power_w)::INT,
|
||||
AVG(grid_power_w)::INT,
|
||||
AVG(load_power_w)::INT,
|
||||
LAST(battery_soc_percent, measured_at)
|
||||
INTO
|
||||
v_avg_pv_power_w,
|
||||
v_avg_battery_power_w,
|
||||
v_avg_grid_power_w,
|
||||
v_avg_load_power_w,
|
||||
v_last_soc
|
||||
FROM ems.telemetry_inverter
|
||||
WHERE site_id = p_site_id
|
||||
AND measured_at >= p_interval_start
|
||||
AND measured_at < v_interval_end;
|
||||
|
||||
-- Agregovat EV nabíječky (součet průměrů po charger_id)
|
||||
SELECT COALESCE(SUM(avg_power), 0)::INT
|
||||
INTO v_sum_ev_power_w
|
||||
FROM (
|
||||
SELECT AVG(power_w) AS avg_power
|
||||
FROM ems.telemetry_ev_charger
|
||||
WHERE site_id = p_site_id
|
||||
AND measured_at >= p_interval_start
|
||||
AND measured_at < v_interval_end
|
||||
GROUP BY charger_id
|
||||
) sub;
|
||||
|
||||
-- Agregovat tepelné čerpadlo
|
||||
SELECT AVG(power_w)::INT
|
||||
INTO v_avg_hp_power_w
|
||||
FROM ems.telemetry_heat_pump
|
||||
WHERE site_id = p_site_id
|
||||
AND measured_at >= p_interval_start
|
||||
AND measured_at < v_interval_end;
|
||||
|
||||
-- Efektivní cena pro výpočet skutečných nákladů
|
||||
v_buy_price := ems.fn_effective_buy_price(p_site_id, p_interval_start);
|
||||
v_sell_price := ems.fn_effective_sell_price(p_site_id, p_interval_start);
|
||||
|
||||
-- Skutečné náklady (kladný grid = nákup, záporný = prodej)
|
||||
IF v_avg_grid_power_w IS NOT NULL THEN
|
||||
v_actual_cost := (v_avg_grid_power_w::NUMERIC / 1000.0 / 4.0)
|
||||
* CASE WHEN v_avg_grid_power_w >= 0
|
||||
THEN COALESCE(v_buy_price, 0)
|
||||
ELSE COALESCE(v_sell_price, 0) END;
|
||||
END IF;
|
||||
|
||||
-- Upsert do audit_interval
|
||||
INSERT INTO ems.audit_interval (
|
||||
site_id, interval_start, planning_run_id,
|
||||
actual_pv_power_w, actual_battery_power_w,
|
||||
actual_grid_power_w, actual_load_power_w,
|
||||
actual_battery_soc_pct,
|
||||
actual_ev_power_w,
|
||||
actual_heat_pump_power_w,
|
||||
actual_cost_czk,
|
||||
deviation_grid_w,
|
||||
deviation_cost_czk
|
||||
) VALUES (
|
||||
p_site_id, p_interval_start, v_run_id,
|
||||
v_avg_pv_power_w,
|
||||
v_avg_battery_power_w,
|
||||
v_avg_grid_power_w,
|
||||
v_avg_load_power_w,
|
||||
v_last_soc,
|
||||
v_sum_ev_power_w,
|
||||
v_avg_hp_power_w,
|
||||
ROUND(v_actual_cost, 4),
|
||||
CASE WHEN v_plan.run_id IS NOT NULL
|
||||
THEN v_avg_grid_power_w - v_plan.grid_setpoint_w
|
||||
ELSE NULL END,
|
||||
CASE WHEN v_plan.run_id IS NOT NULL
|
||||
THEN ROUND(v_actual_cost - COALESCE(v_plan.expected_cost_czk, 0), 4)
|
||||
ELSE NULL END
|
||||
)
|
||||
ON CONFLICT (site_id, interval_start) DO UPDATE SET
|
||||
planning_run_id = EXCLUDED.planning_run_id,
|
||||
actual_pv_power_w = EXCLUDED.actual_pv_power_w,
|
||||
actual_battery_power_w = EXCLUDED.actual_battery_power_w,
|
||||
actual_grid_power_w = EXCLUDED.actual_grid_power_w,
|
||||
actual_load_power_w = EXCLUDED.actual_load_power_w,
|
||||
actual_battery_soc_pct = EXCLUDED.actual_battery_soc_pct,
|
||||
actual_ev_power_w = EXCLUDED.actual_ev_power_w,
|
||||
actual_heat_pump_power_w = EXCLUDED.actual_heat_pump_power_w,
|
||||
actual_cost_czk = EXCLUDED.actual_cost_czk,
|
||||
deviation_grid_w = EXCLUDED.deviation_grid_w,
|
||||
deviation_cost_czk = EXCLUDED.deviation_cost_czk;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_fill_audit_interval(INT, TIMESTAMPTZ) IS
|
||||
'Naplní nebo aktualizuje jeden řádek v audit_interval pro danou lokalitu a 15min interval.
|
||||
Agreguje průměry z telemetrie (střídač, EV, TČ), porovná se skutečným plánem a spočítá odchylky.
|
||||
Volat každých 15 minut pro interval který právě skončil.';
|
||||
|
||||
-- ============================================================
|
||||
-- Hromadné plnění auditu za historické období
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_fill_audit_range(
|
||||
p_site_id INT,
|
||||
p_from TIMESTAMPTZ,
|
||||
p_to TIMESTAMPTZ
|
||||
)
|
||||
RETURNS INT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_slot TIMESTAMPTZ;
|
||||
v_count INT := 0;
|
||||
BEGIN
|
||||
v_slot := date_trunc('hour', p_from)
|
||||
+ INTERVAL '15 min' * FLOOR(EXTRACT(MINUTE FROM p_from) / 15);
|
||||
|
||||
WHILE v_slot < p_to LOOP
|
||||
PERFORM ems.fn_fill_audit_interval(p_site_id, v_slot);
|
||||
v_slot := v_slot + INTERVAL '15 minutes';
|
||||
v_count := v_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_fill_audit_range(INT, TIMESTAMPTZ, TIMESTAMPTZ) IS
|
||||
'Hromadně naplní audit_interval pro celé historické období.
|
||||
Volá fn_fill_audit_interval pro každý 15min slot v rozsahu p_from–p_to.
|
||||
Vrátí počet zpracovaných intervalů. Použít pro backfill po výpadku nebo prvním nasazení.';
|
||||
160
db/routines/R__fn_set_mode.sql
Normal file
160
db/routines/R__fn_set_mode.sql
Normal file
@@ -0,0 +1,160 @@
|
||||
-- =============================================================
|
||||
-- R__fn_set_mode.sql
|
||||
-- EMS Platform – přepínání provozních režimů
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_set_mode(
|
||||
p_site_id INT,
|
||||
p_mode_code TEXT,
|
||||
p_activated_by TEXT DEFAULT 'system',
|
||||
p_valid_until TIMESTAMPTZ DEFAULT NULL,
|
||||
p_notes TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_current_mode TEXT;
|
||||
v_mode_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- Ověřit že režim existuje
|
||||
SELECT EXISTS(SELECT 1 FROM ems.operating_mode_def WHERE code = p_mode_code)
|
||||
INTO v_mode_exists;
|
||||
|
||||
IF NOT v_mode_exists THEN
|
||||
RAISE EXCEPTION 'Neznámý provozní režim: %', p_mode_code;
|
||||
END IF;
|
||||
|
||||
-- Zjistit aktuální režim (pro log a previous_mode)
|
||||
SELECT mode_code INTO v_current_mode
|
||||
FROM ems.site_operating_mode
|
||||
WHERE site_id = p_site_id;
|
||||
|
||||
-- Pokud se režim nemění, nic nedělat
|
||||
IF v_current_mode = p_mode_code THEN
|
||||
RETURN p_mode_code;
|
||||
END IF;
|
||||
|
||||
-- Uzavřít předchozí záznam v logu
|
||||
UPDATE ems.site_operating_mode_log
|
||||
SET deactivated_at = now()
|
||||
WHERE site_id = p_site_id
|
||||
AND deactivated_at IS NULL;
|
||||
|
||||
-- Upsert aktivního režimu
|
||||
INSERT INTO ems.site_operating_mode
|
||||
(site_id, mode_code, activated_at, activated_by, valid_until, previous_mode, notes)
|
||||
VALUES
|
||||
(p_site_id, p_mode_code, now(), p_activated_by, p_valid_until, v_current_mode, p_notes)
|
||||
ON CONFLICT (site_id) DO UPDATE SET
|
||||
mode_code = EXCLUDED.mode_code,
|
||||
activated_at = EXCLUDED.activated_at,
|
||||
activated_by = EXCLUDED.activated_by,
|
||||
valid_until = EXCLUDED.valid_until,
|
||||
previous_mode = EXCLUDED.previous_mode,
|
||||
notes = EXCLUDED.notes;
|
||||
|
||||
-- Přidat záznam do logu
|
||||
INSERT INTO ems.site_operating_mode_log
|
||||
(site_id, mode_code, activated_at, activated_by, notes)
|
||||
VALUES
|
||||
(p_site_id, p_mode_code, now(), p_activated_by, p_notes);
|
||||
|
||||
RETURN p_mode_code;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_set_mode(INT, TEXT, TEXT, TIMESTAMPTZ, TEXT) IS
|
||||
'Přepne provozní režim lokality. Atomicky aktualizuje site_operating_mode a zapíše do audit logu.
|
||||
Ignoruje přepnutí na stejný režim. Vyhodí výjimku pro neznámý kód režimu.
|
||||
Příklad: SELECT ems.fn_set_mode(1, ''SELF_SUSTAIN'', ''user:jan'', NULL, ''Odjezd na dovolenou'');';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_restore_previous_mode(
|
||||
p_site_id INT,
|
||||
p_activated_by TEXT DEFAULT 'system'
|
||||
)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_previous TEXT;
|
||||
BEGIN
|
||||
SELECT previous_mode INTO v_previous
|
||||
FROM ems.site_operating_mode
|
||||
WHERE site_id = p_site_id;
|
||||
|
||||
IF v_previous IS NULL THEN
|
||||
-- Fallback na AUTO pokud není předchozí režim
|
||||
v_previous := 'AUTO';
|
||||
END IF;
|
||||
|
||||
RETURN ems.fn_set_mode(p_site_id, v_previous, p_activated_by, NULL, 'Obnova předchozího režimu');
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_restore_previous_mode(INT, TEXT) IS
|
||||
'Přepne lokalitu zpět na předchozí provozní režim (uložený v previous_mode).
|
||||
Pokud předchozí režim neexistuje, přepne na AUTO. Používat po skončení dočasného přepisu.';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_expire_modes()
|
||||
RETURNS INT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_count INT := 0;
|
||||
v_rec RECORD;
|
||||
BEGIN
|
||||
-- Najít lokality kde vypršel valid_until a přepnout na AUTO
|
||||
FOR v_rec IN
|
||||
SELECT site_id, previous_mode
|
||||
FROM ems.site_operating_mode
|
||||
WHERE valid_until IS NOT NULL
|
||||
AND valid_until <= now()
|
||||
AND mode_code <> 'AUTO'
|
||||
LOOP
|
||||
PERFORM ems.fn_set_mode(
|
||||
v_rec.site_id,
|
||||
COALESCE(v_rec.previous_mode, 'AUTO'),
|
||||
'system:expiry',
|
||||
NULL,
|
||||
'Automatické vypršení dočasného režimu'
|
||||
);
|
||||
v_count := v_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_expire_modes() IS
|
||||
'Zkontroluje všechny lokality s dočasným režimem (valid_until IS NOT NULL) a přepne zpět ty s prosahlým časem.
|
||||
Volat každou minutu jako scheduled task. Vrátí počet přepnutých lokalit.';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_update_heartbeat(
|
||||
p_site_id INT,
|
||||
p_status TEXT DEFAULT 'ok',
|
||||
p_ems_version TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS VOID
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
INSERT INTO ems.site_heartbeat (site_id, last_seen, status, ems_version)
|
||||
VALUES (p_site_id, now(), p_status, p_ems_version)
|
||||
ON CONFLICT (site_id) DO UPDATE SET
|
||||
last_seen = now(),
|
||||
status = EXCLUDED.status,
|
||||
ems_version = COALESCE(EXCLUDED.ems_version, ems.site_heartbeat.ems_version);
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION ems.fn_update_heartbeat(INT, TEXT, TEXT) IS
|
||||
'Aktualizuje informační heartbeat záznam EMS pro danou lokalitu.
|
||||
Volat každou minutu z backend service po úspěšném odeslání pulzu do Loxone.
|
||||
Slouží pouze pro EMS dashboard – Loxone watchdog nezávisí na této tabulce,
|
||||
sleduje HTTP pulzy přímo a nezávisle na dostupnosti DB.';
|
||||
69
db/views/R__vw_audit_summary.sql
Normal file
69
db/views/R__vw_audit_summary.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- =============================================================
|
||||
-- R__vw_audit_summary.sql
|
||||
-- EMS Platform – přehledové views pro audit a dashboard
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
-- Denní souhrn per lokalita
|
||||
CREATE OR REPLACE VIEW ems.vw_audit_daily AS
|
||||
SELECT
|
||||
site_id,
|
||||
date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague') AS day_local,
|
||||
COUNT(*) AS interval_count,
|
||||
-- Energie (kWh = W × 15min / 60 = W / 4 / 1000)
|
||||
ROUND(SUM(GREATEST(actual_grid_power_w, 0))::NUMERIC / 4000, 3) AS import_kwh,
|
||||
ROUND(SUM(ABS(LEAST(actual_grid_power_w, 0)))::NUMERIC / 4000, 3) AS export_kwh,
|
||||
ROUND(SUM(GREATEST(actual_pv_power_w, 0))::NUMERIC / 4000, 3) AS pv_kwh,
|
||||
ROUND(SUM(GREATEST(actual_load_power_w, 0))::NUMERIC / 4000, 3) AS load_kwh,
|
||||
ROUND(SUM(GREATEST(actual_ev_power_w, 0))::NUMERIC / 4000, 3) AS ev_kwh,
|
||||
ROUND(SUM(GREATEST(actual_heat_pump_power_w, 0))::NUMERIC / 4000, 3) AS hp_kwh,
|
||||
-- Náklady
|
||||
ROUND(SUM(actual_cost_czk), 2) AS actual_cost_czk,
|
||||
ROUND(SUM(deviation_cost_czk), 2) AS total_deviation_czk,
|
||||
-- Počet intervalů s velkými odchylkami (>1kW)
|
||||
COUNT(*) FILTER (WHERE ABS(deviation_grid_w) > 1000) AS high_deviation_count
|
||||
FROM ems.audit_interval
|
||||
GROUP BY site_id, date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague');
|
||||
|
||||
COMMENT ON VIEW ems.vw_audit_daily IS
|
||||
'Denní souhrn auditu per lokalita. Energie v kWh, náklady v Kč.
|
||||
Používat pro dashboard denního přehledu a reporty.';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
-- Týdenní souhrn
|
||||
CREATE OR REPLACE VIEW ems.vw_audit_weekly AS
|
||||
SELECT
|
||||
site_id,
|
||||
date_trunc('week', interval_start AT TIME ZONE 'Europe/Prague') AS week_local,
|
||||
ROUND(SUM(GREATEST(actual_grid_power_w, 0))::NUMERIC / 4000, 1) AS import_kwh,
|
||||
ROUND(SUM(ABS(LEAST(actual_grid_power_w, 0)))::NUMERIC / 4000, 1) AS export_kwh,
|
||||
ROUND(SUM(GREATEST(actual_pv_power_w, 0))::NUMERIC / 4000, 1) AS pv_kwh,
|
||||
ROUND(SUM(actual_cost_czk), 0) AS actual_cost_czk
|
||||
FROM ems.audit_interval
|
||||
GROUP BY site_id, date_trunc('week', interval_start AT TIME ZONE 'Europe/Prague');
|
||||
|
||||
COMMENT ON VIEW ems.vw_audit_weekly IS
|
||||
'Týdenní souhrn auditu per lokalita.';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
-- Aktuální den – hourly breakdown pro dashboard graf
|
||||
CREATE OR REPLACE VIEW ems.vw_audit_today_hourly AS
|
||||
SELECT
|
||||
site_id,
|
||||
date_trunc('hour', interval_start AT TIME ZONE 'Europe/Prague') AS hour_local,
|
||||
ROUND(AVG(actual_pv_power_w)::NUMERIC / 1000, 2) AS avg_pv_kw,
|
||||
ROUND(AVG(actual_battery_power_w)::NUMERIC / 1000, 2) AS avg_battery_kw,
|
||||
ROUND(AVG(actual_grid_power_w)::NUMERIC / 1000, 2) AS avg_grid_kw,
|
||||
ROUND(AVG(actual_load_power_w)::NUMERIC / 1000, 2) AS avg_load_kw,
|
||||
ROUND(AVG(actual_battery_soc_pct), 1) AS avg_soc_pct,
|
||||
ROUND(SUM(actual_cost_czk), 2) AS cost_czk
|
||||
FROM ems.audit_interval
|
||||
WHERE interval_start >= date_trunc('day', now() AT TIME ZONE 'Europe/Prague')
|
||||
AND interval_start < date_trunc('day', now() AT TIME ZONE 'Europe/Prague') + INTERVAL '1 day'
|
||||
GROUP BY site_id, date_trunc('hour', interval_start AT TIME ZONE 'Europe/Prague')
|
||||
ORDER BY hour_local;
|
||||
|
||||
COMMENT ON VIEW ems.vw_audit_today_hourly IS
|
||||
'Hodinový přehled dnešního dne pro dashboard graf výkonů a nákladů.';
|
||||
77
db/views/R__vw_latest_telemetry.sql
Normal file
77
db/views/R__vw_latest_telemetry.sql
Normal file
@@ -0,0 +1,77 @@
|
||||
-- =============================================================
|
||||
-- R__vw_latest_telemetry.sql
|
||||
-- EMS Platform – aktuální stav všech zařízení per lokalita
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_latest_inverter AS
|
||||
SELECT DISTINCT ON (t.inverter_id)
|
||||
t.site_id,
|
||||
t.inverter_id,
|
||||
inv.code AS inverter_code,
|
||||
t.measured_at,
|
||||
t.pv_power_w,
|
||||
t.battery_soc_percent,
|
||||
t.battery_power_w,
|
||||
t.grid_power_w,
|
||||
t.load_power_w,
|
||||
t.inverter_temp_c,
|
||||
t.operating_mode,
|
||||
t.fault_code,
|
||||
now() - t.measured_at AS data_age
|
||||
FROM ems.telemetry_inverter t
|
||||
JOIN ems.asset_inverter inv ON inv.id = t.inverter_id
|
||||
ORDER BY t.inverter_id, t.measured_at DESC;
|
||||
|
||||
COMMENT ON VIEW ems.vw_latest_inverter IS
|
||||
'Nejnovější telemetrická data pro každý střídač. Slouží pro real-time dashboard a health check.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_latest_ev_charger AS
|
||||
SELECT DISTINCT ON (t.charger_id, t.connector_id)
|
||||
t.site_id,
|
||||
t.charger_id,
|
||||
ch.code AS charger_code,
|
||||
t.connector_id,
|
||||
t.measured_at,
|
||||
t.status,
|
||||
t.power_w,
|
||||
t.energy_kwh,
|
||||
t.current_a,
|
||||
t.session_id,
|
||||
t.error_code,
|
||||
now() - t.measured_at AS data_age
|
||||
FROM ems.telemetry_ev_charger t
|
||||
JOIN ems.asset_ev_charger ch ON ch.id = t.charger_id
|
||||
ORDER BY t.charger_id, t.connector_id, t.measured_at DESC;
|
||||
|
||||
COMMENT ON VIEW ems.vw_latest_ev_charger IS
|
||||
'Nejnovější telemetrická data pro každý konektor EV nabíječky. Slouží pro dashboard a řízení nabíjení.';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_latest_heat_pump AS
|
||||
SELECT DISTINCT ON (t.heat_pump_id)
|
||||
t.site_id,
|
||||
t.heat_pump_id,
|
||||
hp.code AS heat_pump_code,
|
||||
t.measured_at,
|
||||
t.outdoor_temp_c,
|
||||
t.tuv_tank_temp_c,
|
||||
t.water_outlet_temp_c,
|
||||
t.power_w,
|
||||
t.operating_mode,
|
||||
t.cop_actual,
|
||||
t.defrost_active,
|
||||
t.alarm_code,
|
||||
-- Odhadovaný COP pro aktuální venkovní teplotu
|
||||
ems.fn_cop_estimate(t.heat_pump_id, t.outdoor_temp_c) AS cop_estimated,
|
||||
now() - t.measured_at AS data_age
|
||||
FROM ems.telemetry_heat_pump t
|
||||
JOIN ems.asset_heat_pump hp ON hp.id = t.heat_pump_id
|
||||
ORDER BY t.heat_pump_id, t.measured_at DESC;
|
||||
|
||||
COMMENT ON VIEW ems.vw_latest_heat_pump IS
|
||||
'Nejnovější telemetrická data pro každé tepelné čerpadlo včetně odhadovaného COP.
|
||||
Slouží pro real-time dashboard a rozhodovací logiku plánování.';
|
||||
75
db/views/R__vw_operating_mode.sql
Normal file
75
db/views/R__vw_operating_mode.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- =============================================================
|
||||
-- R__vw_operating_mode.sql
|
||||
-- EMS Platform – views pro provozní režimy a heartbeat
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
-- Aktuální stav všech lokalit (pro dashboard a PostgREST)
|
||||
CREATE OR REPLACE VIEW ems.vw_site_status AS
|
||||
SELECT
|
||||
s.id AS site_id,
|
||||
s.code AS site_code,
|
||||
s.name AS site_name,
|
||||
m.mode_code AS active_mode,
|
||||
d.name AS mode_name,
|
||||
d.description AS mode_description,
|
||||
d.is_autonomous,
|
||||
m.activated_at,
|
||||
m.activated_by,
|
||||
m.valid_until,
|
||||
m.previous_mode,
|
||||
m.notes AS mode_notes,
|
||||
-- Heartbeat
|
||||
hb.last_seen AS ems_last_seen,
|
||||
hb.status AS ems_status,
|
||||
EXTRACT(EPOCH FROM (now() - hb.last_seen))::INT AS ems_age_seconds,
|
||||
-- Varování pokud EMS dlouho nepingoval
|
||||
CASE
|
||||
WHEN hb.last_seen IS NULL THEN 'never_seen'
|
||||
WHEN now() - hb.last_seen > INTERVAL '5 minutes' THEN 'stale'
|
||||
WHEN now() - hb.last_seen > INTERVAL '2 minutes' THEN 'delayed'
|
||||
ELSE 'ok'
|
||||
END AS ems_heartbeat_status,
|
||||
-- Aktuální telemetrie (snapshot)
|
||||
li.pv_power_w,
|
||||
li.battery_soc_percent,
|
||||
li.battery_power_w,
|
||||
li.grid_power_w,
|
||||
li.load_power_w,
|
||||
li.measured_at AS telemetry_at
|
||||
FROM ems.site s
|
||||
LEFT JOIN ems.site_operating_mode m ON m.site_id = s.id
|
||||
LEFT JOIN ems.operating_mode_def d ON d.code = m.mode_code
|
||||
LEFT JOIN ems.site_heartbeat hb ON hb.site_id = s.id
|
||||
LEFT JOIN ems.vw_latest_inverter li ON li.site_id = s.id
|
||||
WHERE s.active = true;
|
||||
|
||||
COMMENT ON VIEW ems.vw_site_status IS
|
||||
'Kompletní stavový přehled lokality: aktivní režim, heartbeat EMS, aktuální telemetrie.
|
||||
Primární view pro dashboard a health check endpoint. Jeden řádek na aktivní lokalitu.
|
||||
ems_heartbeat_status slouží pro EMS vlastní alerting – Loxone watchdog tuto tabulku nečte,
|
||||
sleduje HTTP pulzy přímo (viz docs/loxone-integration.md).';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
-- Log přepnutí režimů (pro UI historii)
|
||||
CREATE OR REPLACE VIEW ems.vw_mode_log_recent AS
|
||||
SELECT
|
||||
l.id,
|
||||
l.site_id,
|
||||
s.code AS site_code,
|
||||
l.mode_code,
|
||||
d.name AS mode_name,
|
||||
l.activated_at,
|
||||
l.deactivated_at,
|
||||
EXTRACT(EPOCH FROM COALESCE(l.deactivated_at, now()) - l.activated_at)::INT AS duration_sec,
|
||||
l.activated_by,
|
||||
l.notes
|
||||
FROM ems.site_operating_mode_log l
|
||||
JOIN ems.site s ON s.id = l.site_id
|
||||
JOIN ems.operating_mode_def d ON d.code = l.mode_code
|
||||
WHERE l.activated_at >= now() - INTERVAL '7 days'
|
||||
ORDER BY l.activated_at DESC;
|
||||
|
||||
COMMENT ON VIEW ems.vw_mode_log_recent IS
|
||||
'Posledních 7 dní přepnutí provozních režimů. Slouží pro audit log v UI.';
|
||||
42
db/views/R__vw_site_effective_price.sql
Normal file
42
db/views/R__vw_site_effective_price.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- =============================================================
|
||||
-- R__vw_site_effective_price.sql
|
||||
-- EMS Platform – view efektivních cen per site
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_site_effective_price AS
|
||||
SELECT
|
||||
smc.site_id,
|
||||
mip.interval_start,
|
||||
mip.interval_end,
|
||||
mip.market_source,
|
||||
-- Raw ceny
|
||||
mip.buy_raw_price_czk_kwh,
|
||||
mip.sell_raw_price_czk_kwh,
|
||||
-- Marže
|
||||
smc.buy_margin_fixed_czk,
|
||||
smc.buy_margin_percent,
|
||||
smc.sell_margin_fixed_czk,
|
||||
smc.sell_margin_percent,
|
||||
-- Efektivní ceny
|
||||
ROUND(
|
||||
mip.buy_raw_price_czk_kwh
|
||||
+ smc.buy_margin_fixed_czk
|
||||
+ (mip.buy_raw_price_czk_kwh * smc.buy_margin_percent / 100.0),
|
||||
6
|
||||
) AS effective_buy_price_czk_kwh,
|
||||
ROUND(
|
||||
mip.sell_raw_price_czk_kwh
|
||||
+ smc.sell_margin_fixed_czk
|
||||
+ (mip.sell_raw_price_czk_kwh * smc.sell_margin_percent / 100.0),
|
||||
6
|
||||
) AS effective_sell_price_czk_kwh
|
||||
FROM ems.market_interval_price mip
|
||||
CROSS JOIN ems.site_market_config smc
|
||||
WHERE smc.valid_from <= mip.interval_start
|
||||
AND (smc.valid_to IS NULL OR smc.valid_to > mip.interval_start);
|
||||
|
||||
COMMENT ON VIEW ems.vw_site_effective_price IS
|
||||
'Efektivní nákupní a prodejní ceny elektřiny per lokalita a 15min interval.
|
||||
Dopočítává marže z site_market_config na raw ceny z market_interval_price.
|
||||
Nezahrnuje data bez platné market_config. Používat pro plánování a audit.';
|
||||
Reference in New Issue
Block a user