second version

This commit is contained in:
Dusan Vojacek
2026-04-03 14:23:16 +02:00
parent 897b95f728
commit 9f4126946d
105 changed files with 9738 additions and 1470 deletions

View File

@@ -30,7 +30,7 @@ VALUES (
-- 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.'
SELECT id, 'modbus_tcp', '172.16.1.10', 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';
-- Teltonika EV nabíječka 1 přes Waveshare
@@ -88,7 +88,7 @@ INSERT INTO ems.asset_inverter (site_id, code, manufacturer, model, endpoint_id,
SELECT
s.id, 'deye-main', 'Deye', 'SUN-20K-SG01LP1-EU',
ep.id,
20000, 20000, 20000,
18000, 18000, 18000,
true,
'Hlavní hybridní střídač 20kW LV. RS485 Modbus RTU přes Waveshare.'
FROM ems.site s
@@ -129,8 +129,8 @@ SELECT
s.id, inv.id, 'pv-a', 'FVE pole A',
10000, -- 10 kWp
184,
35, -- sklon odhad; upřesnit dle střechy
NULL,
22, -- sklon odhad; upřesnit dle střechy
18,
1.0,
true,
'Hlavní FVE pole řízené Deye střídačem.'

View File

@@ -41,11 +41,7 @@ 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;
-- Seed zeleného bonusu přesunut: V017__green_bonus.sql (ems.asset_pv_array.green_bonus_*).
-- Sloupce green_bonus_* na site_market_config odstraňuje V018__cleanup_legacy_green_bonus.sql;
-- UPDATE zde by při změně pořadí / rebuild konfliktních migrací selhal.
-- UPDATE ems.site_market_config SET green_bonus_czk_kwh = 1.20, green_bonus_asset_code = 'pv-b' ...

View File

@@ -0,0 +1,29 @@
-- Nové sloupce telemetrie Deye (GEN port, PV1/PV2, denní energie baterie, run_state).
-- V011 je již použito pro indexy/agregace.
ALTER TABLE ems.telemetry_inverter
ADD COLUMN IF NOT EXISTS pv1_power_w INT,
ADD COLUMN IF NOT EXISTS pv2_power_w INT,
ADD COLUMN IF NOT EXISTS gen_port_power_w INT,
ADD COLUMN IF NOT EXISTS batt_charge_today_wh INT,
ADD COLUMN IF NOT EXISTS batt_discharge_today_wh INT,
ADD COLUMN IF NOT EXISTS run_state INT;
COMMENT ON COLUMN ems.telemetry_inverter.pv1_power_w IS
'Výkon PV1 vstupu W (Deye holding register).';
COMMENT ON COLUMN ems.telemetry_inverter.pv2_power_w IS
'Výkon PV2 vstupu W (Deye holding register).';
COMMENT ON COLUMN ems.telemetry_inverter.gen_port_power_w IS
'Výkon GEN portu W výroba FVE pole B (ongridový střídač).
Nelze řídit, jen měřit. Klíčový pro audit zeleného bonusu.';
COMMENT ON COLUMN ems.telemetry_inverter.batt_charge_today_wh IS
'Dnešní nabití baterie Wh (denní čítač z Modbus).';
COMMENT ON COLUMN ems.telemetry_inverter.batt_discharge_today_wh IS
'Dnešní vybití baterie Wh (denní čítač z Modbus).';
COMMENT ON COLUMN ems.telemetry_inverter.run_state IS
'Provozní stav střídače (raw enum z Modbus registru run_state).';
COMMENT ON COLUMN ems.telemetry_inverter.battery_power_w IS
'Výkon baterie v W (signed). Kladné = vybíjení, záporné = nabíjení (mapování dle registru 590).';
COMMENT ON COLUMN ems.telemetry_inverter.pv_power_w IS
'Součet okamžitého výkonu FVE stringů na střídači (typicky PV1+PV2) v W.';

View File

@@ -0,0 +1,20 @@
-- Predikovaná okna záporných spotových cen (cache pro UI / API)
CREATE TABLE ems.predicted_negative_price_window (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site (id),
predicted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
predicted_date DATE NOT NULL,
window_start_hour INT NOT NULL,
window_end_hour INT NOT NULL,
probability_pct INT NOT NULL,
expected_min_price NUMERIC(10, 4),
reason TEXT,
UNIQUE (site_id, predicted_date, window_start_hour)
);
COMMENT ON TABLE ems.predicted_negative_price_window IS
'Výstup ems.fn_predict_negative_price_windows predikce intervalů s rizikem záporné nákupní ceny; obnovovat po importu cen a forecastu FVE.';
CREATE INDEX idx_predicted_neg_price_site_date
ON ems.predicted_negative_price_window (site_id, predicted_date);

View File

@@ -0,0 +1,78 @@
-- =============================================================
-- V014__asset_model_refinement.sql
-- Rozlišení limitů: AC střídač / DC FVE / baterie přes měnič / BMS+C-rate
-- =============================================================
-- Střídač: nové sloupce (staré max_charge_power_w / max_discharge_power_w ponechány kvůli kompatibilitě)
ALTER TABLE ems.asset_inverter
ADD COLUMN IF NOT EXISTS max_ac_output_w INT,
ADD COLUMN IF NOT EXISTS max_dc_input_w INT,
ADD COLUMN IF NOT EXISTS max_battery_charge_w INT,
ADD COLUMN IF NOT EXISTS max_battery_discharge_w INT,
ADD COLUMN IF NOT EXISTS gen_port_max_power_w INT;
COMMENT ON COLUMN ems.asset_inverter.max_ac_output_w IS
'Maximální AC výkon střídače v W. Deye SUN-20K = 22000 W.';
COMMENT ON COLUMN ems.asset_inverter.max_dc_input_w IS
'Maximální DC vstupní výkon z FVE v W. Deye SUN-20K = 40000 W.';
COMMENT ON COLUMN ems.asset_inverter.max_battery_charge_w IS
'Maximální výkon nabíjení baterie v W limit střídače (ne BMS).
Deye SUN-20K s LV baterií = 18000 W (350A × 51.2V).';
COMMENT ON COLUMN ems.asset_inverter.max_battery_discharge_w IS
'Maximální výkon vybíjení baterie v W limit střídače.
Deye SUN-20K s LV baterií = 18000 W.';
COMMENT ON COLUMN ems.asset_inverter.gen_port_max_power_w IS
'Maximální výkon GEN portu v W. Zahrnuje součet všech zařízení
zapojených do GEN portu (mikroinvertory, ongrid střídač).
Pro home-01 = 10080 W (pv-b pole).
Pro druhou instalaci = 4400 W (2× 2.2 kW mikroinvertory).';
-- Baterie: C-rate a BMS
ALTER TABLE ems.asset_battery
ADD COLUMN IF NOT EXISTS max_charge_c_rate NUMERIC(4,2),
ADD COLUMN IF NOT EXISTS max_discharge_c_rate NUMERIC(4,2),
ADD COLUMN IF NOT EXISTS bms_max_charge_w INT,
ADD COLUMN IF NOT EXISTS bms_max_discharge_w INT;
COMMENT ON COLUMN ems.asset_battery.max_charge_c_rate IS
'Maximální nabíjecí C-rate. 0.5C pro 64 kWh = 32 kW teoretické maximum.
Skutečný limit je min(bms_max_charge_w, inverter.max_battery_charge_w).';
COMMENT ON COLUMN ems.asset_battery.max_discharge_c_rate IS
'Maximální vybíjecí C-rate (symetricky k nabíjení).';
COMMENT ON COLUMN ems.asset_battery.bms_max_charge_w IS
'Maximální nabíjecí výkon dle BMS v W. Pokud NULL, použij C-rate výpočet.';
COMMENT ON COLUMN ems.asset_battery.bms_max_discharge_w IS
'Maximální vybíjecí výkon dle BMS v W. Pokud NULL, použij C-rate výpočet.';
-- Z existujících sloupců přejmenujeme sémantiku do nových (kde ještě nejsou vyplněné)
UPDATE ems.asset_inverter
SET
max_battery_charge_w = COALESCE(max_battery_charge_w, max_charge_power_w),
max_battery_discharge_w = COALESCE(max_battery_discharge_w, max_discharge_power_w)
WHERE max_charge_power_w IS NOT NULL
OR max_discharge_power_w IS NOT NULL;
-- Seed home-01: hlavní Deye (ne ongrid řádek)
UPDATE ems.asset_inverter inv
SET
max_ac_output_w = 22000,
max_dc_input_w = 40000,
max_battery_charge_w = 18000,
max_battery_discharge_w = 18000,
gen_port_max_power_w = 10080
FROM ems.site s
WHERE s.id = inv.site_id
AND s.code = 'home-01'
AND inv.code = 'deye-main';
UPDATE ems.asset_battery ab
SET
max_charge_c_rate = 0.28,
max_discharge_c_rate = 0.28,
bms_max_charge_w = 18000,
bms_max_discharge_w = 18000
FROM ems.asset_inverter inv
JOIN ems.site s ON s.id = inv.site_id
WHERE ab.inverter_id = inv.id
AND s.code = 'home-01'
AND inv.code = 'deye-main';

View File

@@ -0,0 +1,100 @@
-- ============================================================
-- Distribuční tarify a HDO
-- ============================================================
CREATE TABLE ems.distribution_tariff (
id SERIAL PRIMARY KEY,
distributor TEXT NOT NULL, -- 'EGD', 'CEZ', 'PRE'
code TEXT NOT NULL, -- 'D02d', 'C25d', 'custom_fve'
name TEXT NOT NULL,
has_dual_rate BOOLEAN NOT NULL DEFAULT true, -- NT/VT nebo jednotarif
vat_rate NUMERIC(5,4) NOT NULL DEFAULT 0.21,
notes TEXT,
UNIQUE (distributor, code)
);
COMMENT ON TABLE ems.distribution_tariff IS
'Číselník distribučních tarifů. Jeden záznam = jeden tarif jednoho distributora.
has_dual_rate=true znamená NT/VT dvojsazba, false = jednotarif.';
-- ============================================================
CREATE TABLE ems.distribution_tariff_rate (
id SERIAL PRIMARY KEY,
tariff_id INT NOT NULL REFERENCES ems.distribution_tariff(id),
rate_type TEXT NOT NULL, -- 'NT', 'VT', 'single'
price_czk_kwh NUMERIC(10,6) NOT NULL, -- variabilní složka bez DPH
valid_from DATE NOT NULL,
valid_to DATE, -- NULL = platí dosud
notes TEXT,
UNIQUE (tariff_id, rate_type, valid_from)
);
COMMENT ON TABLE ems.distribution_tariff_rate IS
'Sazby distribučního tarifu Kč/kWh bez DPH. Verzováno přes valid_from/valid_to.
Při roční změně tarifů: nastav valid_to na starém záznamu a přidej nový.
price_czk_kwh = pouze variabilní distribuce, BEZ systémových služeb a OTE.';
COMMENT ON COLUMN ems.distribution_tariff_rate.price_czk_kwh IS
'Variabilní distribuční složka Kč/kWh bez DPH.
Nezahrnuje: systémové služby ČEPS, poplatek OTE, silovou elektřinu (spot).
Tyto ostatní fixní složky jsou v site_market_config jako system_services_czk_kwh.';
-- ============================================================
CREATE TABLE ems.hdo_code (
id SERIAL PRIMARY KEY,
distributor TEXT NOT NULL, -- 'EGD', 'CEZ', 'PRE'
code TEXT NOT NULL, -- 'B1', 'C3', 'custom_fve_home01'
description TEXT,
valid_from DATE NOT NULL,
valid_to DATE, -- NULL = platí dosud
notes TEXT,
UNIQUE (distributor, code, valid_from)
);
COMMENT ON TABLE ems.hdo_code IS
'Číselník HDO kódů per distributor. Při roční změně přidat nový záznam
s novým valid_from starý zůstane v historii pro audit.
Kód "custom_fve_home01" pro FVE instalace bez standardního HDO kódu.';
-- ============================================================
CREATE TABLE ems.hdo_code_window (
id SERIAL PRIMARY KEY,
hdo_code_id INT NOT NULL REFERENCES ems.hdo_code(id),
day_type TEXT NOT NULL DEFAULT 'all',
-- 'all' = každý den, 'workday' = Po-Pá, 'weekend' = So-Ne
rate_type TEXT NOT NULL DEFAULT 'VT', -- 'VT' nebo 'NT'
window_from TIME NOT NULL, -- začátek okna (inclusive)
window_to TIME NOT NULL -- konec okna (exclusive)
);
COMMENT ON TABLE ems.hdo_code_window IS
'NT/VT časová okna pro HDO kód. Více řádků = více oken za den.
Logika: pokud aktuální čas spadá do VT okna → rate_type=VT, jinak NT.
day_type=all znamená stejná okna každý den (workday i weekend).
Příklad home-01: VT 09:00-10:00, 12:00-13:00, 16:00-17:00, 20:00-21:00.';
-- ============================================================
-- Rozšíření site_market_config
-- ============================================================
ALTER TABLE ems.site_market_config
ADD COLUMN IF NOT EXISTS tariff_id INT REFERENCES ems.distribution_tariff(id),
ADD COLUMN IF NOT EXISTS hdo_code_id INT REFERENCES ems.hdo_code(id),
ADD COLUMN IF NOT EXISTS system_services_czk_kwh NUMERIC(10,6) DEFAULT 0,
ADD COLUMN IF NOT EXISTS ote_fee_czk_kwh NUMERIC(10,6) DEFAULT 0;
COMMENT ON COLUMN ems.site_market_config.tariff_id IS
'Distribuční tarif přiřazený k této lokalitě. FK na distribution_tariff.';
COMMENT ON COLUMN ems.site_market_config.hdo_code_id IS
'HDO kód přiřazený k této lokalitě. Určuje NT/VT časová okna.
Při změně HDO předpisu stačí přidat nový hdo_code záznam a aktualizovat FK.';
COMMENT ON COLUMN ems.site_market_config.system_services_czk_kwh IS
'Součet systémových poplatků Kč/kWh bez DPH:
systémové služby ČEPS + poplatek OTE + příspěvek na OZE.
Orientační hodnota EG.D 2025: ~0.40 Kč/kWh. Doplnit ze smlouvy/faktury.';
COMMENT ON COLUMN ems.site_market_config.ote_fee_czk_kwh IS
'Poplatek OTE za použití trhu Kč/kWh bez DPH.
Orientačně ~0.001 Kč/kWh. Lze zahrnout do system_services_czk_kwh.';

View File

@@ -0,0 +1,55 @@
-- EG.D tarif pro home-01 (FVE speciální režim)
INSERT INTO ems.distribution_tariff
(distributor, code, name, has_dual_rate, vat_rate, notes)
VALUES
('EGD', 'custom_fve_home01',
'EG.D FVE vlastní spotřeba dvojsazba',
true, 0.21,
'Speciální tarif pro FVE instalaci home-01. VT okna dle smlouvy.');
-- Sazby placeholder hodnoty, doplnit ze smlouvy/faktury
INSERT INTO ems.distribution_tariff_rate
(tariff_id, rate_type, price_czk_kwh, valid_from)
SELECT
id, 'NT', 0.2243, '2025-01-01'
FROM ems.distribution_tariff WHERE distributor='EGD' AND code='custom_fve_home01';
INSERT INTO ems.distribution_tariff_rate
(tariff_id, rate_type, price_czk_kwh, valid_from)
SELECT
id, 'VT', 0.74987, '2025-01-01'
FROM ems.distribution_tariff WHERE distributor='EGD' AND code='custom_fve_home01';
-- HDO kód pro home-01
INSERT INTO ems.hdo_code
(distributor, code, description, valid_from, notes)
VALUES
('EGD', 'custom_fve_home01',
'FVE home-01 VT okna 09-10, 12-13, 16-17, 20-21 (každý den)',
'2025-01-01',
'Platí stejně workday i weekend. Při změně přidat nový záznam s novým valid_from.');
-- VT okna (4 okna, každý den)
INSERT INTO ems.hdo_code_window
(hdo_code_id, day_type, rate_type, window_from, window_to)
SELECT
hc.id, 'all', 'VT', w.wf::TIME, w.wt::TIME
FROM ems.hdo_code hc,
(VALUES
('09:00', '10:00'),
('12:00', '13:00'),
('16:00', '17:00'),
('20:00', '21:00')
) AS w(wf, wt)
WHERE hc.distributor = 'EGD' AND hc.code = 'custom_fve_home01';
-- Napojit home-01 na tarif a HDO kód
UPDATE ems.site_market_config SET
tariff_id = (SELECT id FROM ems.distribution_tariff
WHERE distributor='EGD' AND code='custom_fve_home01'),
hdo_code_id = (SELECT id FROM ems.hdo_code
WHERE distributor='EGD' AND code='custom_fve_home01'
ORDER BY valid_from DESC LIMIT 1),
system_services_czk_kwh = 0.192,
ote_fee_czk_kwh = 0.001
WHERE site_id = (SELECT id FROM ems.site WHERE code='home-01');

View File

@@ -0,0 +1,47 @@
-- =============================================================
-- V017__green_bonus.sql
-- Zelený bonus na úrovni FVE pole (asset_pv_array), ne v prodejní ceně
-- =============================================================
ALTER TABLE ems.asset_pv_array
ADD COLUMN IF NOT EXISTS green_bonus_czk_kwh NUMERIC(10,6),
ADD COLUMN IF NOT EXISTS green_bonus_valid_from DATE,
ADD COLUMN IF NOT EXISTS green_bonus_valid_to DATE,
ADD COLUMN IF NOT EXISTS green_bonus_meter_code TEXT;
COMMENT ON COLUMN ems.asset_pv_array.green_bonus_czk_kwh IS
'Aktuální sazba zeleného bonusu Kč/kWh za vyrobenou elektřinu.
NULL = pole nemá zelený bonus. Bonus se počítá z celkové výroby pole
bez ohledu na to kam energie šla (interní spotřeba i export).
Sazba se mění ročně při změně nastav green_bonus_valid_to na starém
záznamu a aktualizuj na novou hodnotu s novým green_bonus_valid_from.';
COMMENT ON COLUMN ems.asset_pv_array.green_bonus_valid_from IS
'Datum od kdy platí aktuální sazba zeleného bonusu (včetně).';
COMMENT ON COLUMN ems.asset_pv_array.green_bonus_valid_to IS
'Datum do kdy platí aktuální sazba zeleného bonusu (exclusive).
NULL = platí dosud. Při roční změně nastav na první den nového roku
a aktualizuj green_bonus_czk_kwh na novou sazbu.';
COMMENT ON COLUMN ems.asset_pv_array.green_bonus_meter_code IS
'Číslo zeleného elektroměru (EAN nebo číslo ze smlouvy s distributorem).
Slouží pro audit bonus se počítá z odečtů tohoto elektroměru.';
ALTER TABLE ems.audit_interval
ADD COLUMN IF NOT EXISTS green_bonus_czk NUMERIC(10,4) DEFAULT 0;
COMMENT ON COLUMN ems.audit_interval.green_bonus_czk IS
'Příjem ze zeleného bonusu za výrobu bonusových FVE polí v Kč.
Počítáno přes fn_green_bonus_revenue() v audit_filler.
Nezahrnuto v actual_cost_czk je to samostatný příjem.';
-- Seed home-01: zelený bonus jen na pv-b (ongrid střídač na GEN portu)
UPDATE ems.asset_pv_array
SET
green_bonus_czk_kwh = 7.135, -- TODO: doplnit skutečnou sazbu ze smlouvy
green_bonus_valid_from = '2026-01-01',
green_bonus_valid_to = NULL, -- platí dosud
green_bonus_meter_code = 'TODO' -- doplnit EAN zeleného elektroměru
WHERE site_id = (SELECT id FROM ems.site WHERE code = 'home-01')
AND code = 'pv-b';

View File

@@ -0,0 +1,13 @@
-- =============================================================
-- V018__cleanup_legacy_green_bonus.sql
-- Odstranění legacy sloupců zeleného bonusu ze site_market_config (nahrazeno V017 asset_pv_array)
-- =============================================================
ALTER TABLE ems.site_market_config
DROP COLUMN IF EXISTS green_bonus_czk_kwh,
DROP COLUMN IF EXISTS green_bonus_asset_code;
COMMENT ON TABLE ems.site_market_config IS
'Konfigurace tržního prostředí per site.
Zelený bonus je od V017 na ems.asset_pv_array (green_bonus_czk_kwh),
nikoliv zde bonus je vlastností fyzického FVE pole, ne site konfigurace.';

View File

@@ -0,0 +1,43 @@
-- ============================================================
-- Tabulka pro tracking přesnosti forecastu
-- ============================================================
CREATE TABLE ems.forecast_accuracy (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
pv_array_id INT NOT NULL REFERENCES ems.asset_pv_array(id),
interval_start TIMESTAMPTZ NOT NULL,
run_id INT NOT NULL REFERENCES ems.forecast_pv_run(id),
-- Forecast hodnoty
forecast_power_w INT NOT NULL,
forecast_created_at TIMESTAMPTZ NOT NULL,
lead_time_hours NUMERIC(6,2), -- kolik hodin předem byl forecast vytvořen
-- Skutečnost (doplněna zpětně z telemetrie)
actual_power_w INT,
actual_filled_at TIMESTAMPTZ,
-- Odchylka
error_w INT, -- forecast - actual
error_pct NUMERIC(8,4), -- (forecast - actual) / actual * 100
UNIQUE (run_id, interval_start)
);
COMMENT ON TABLE ems.forecast_accuracy IS
'Tracking přesnosti FVE forecastu. Každý řádek = jeden 15min slot
z jednoho forecast runu. actual_power_w se doplňuje zpětně z telemetrie
po uplynutí intervalu přes fn_fill_forecast_accuracy().
Uchovávat navždy slouží pro analýzu přesnosti a budoucí kalibraci solveru.';
COMMENT ON COLUMN ems.forecast_accuracy.lead_time_hours IS
'Kolik hodin předem byl tento forecast vytvořen.
Příklad: forecast vytvořen v pondělí 14:00, interval ve středu 12:00 = 46h.
Slouží pro analýzu: je 6h forecast přesnější než 48h forecast?';
COMMENT ON COLUMN ems.forecast_accuracy.error_pct IS
'Relativní chyba v %. Kladná = forecast nadhodnotil, záporná = podhodnotil.
NULL pokud actual_power_w = 0 (zamezení dělení nulou).';
CREATE INDEX idx_forecast_accuracy_site_time
ON ems.forecast_accuracy (site_id, interval_start DESC);
CREATE INDEX idx_forecast_accuracy_array_lead
ON ems.forecast_accuracy (pv_array_id, lead_time_hours, interval_start DESC);

View File

@@ -0,0 +1,30 @@
-- Statistika příjezdů EV (den v týdnu × hodina); plní telemetry_collector při přechodu available → nabíjení.
CREATE TABLE ems.ev_arrival_stats (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
vehicle_id INT REFERENCES ems.asset_vehicle(id),
charger_id INT NOT NULL REFERENCES ems.asset_ev_charger(id),
day_of_week INT NOT NULL,
arrival_hour INT NOT NULL,
sample_count INT NOT NULL DEFAULT 0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT chk_ev_arrival_stats_dow CHECK (day_of_week >= 0 AND day_of_week <= 6),
CONSTRAINT chk_ev_arrival_stats_hour CHECK (arrival_hour >= 0 AND arrival_hour <= 23),
UNIQUE (site_id, charger_id, day_of_week, arrival_hour)
);
CREATE INDEX idx_ev_arrival_stats_site_charger
ON ems.ev_arrival_stats (site_id, charger_id);
COMMENT ON TABLE ems.ev_arrival_stats IS
'Statistika příjezdů EV dle dne v týdnu a hodiny (časová zóna Europe/Prague).
Plní se z ev_session / telemetrie při detekci připojení (available → preparing/charging).
Po ~4 týdnech dat lze odhadovat typickou hodinu příjezdu.';
-- Nejvýše jedna otevřená session na nabíječku (pro INSERT … ON CONFLICT při startu session).
CREATE UNIQUE INDEX uidx_ev_session_charger_open
ON ems.ev_session (charger_id)
WHERE session_end IS NULL;
GRANT SELECT ON ems.ev_arrival_stats TO ems_anon;

View File

@@ -0,0 +1,27 @@
-- Historické průměry bazální spotřeby (DOW + hodina) pro solver a forecast.
CREATE TABLE ems.consumption_baseline_stats (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
day_of_week INT NOT NULL, -- 0=neděle, 1=pondělí... 6=sobota
hour_of_day INT NOT NULL, -- 0-23
avg_power_w NUMERIC(10,2) NOT NULL,
stddev_power_w NUMERIC(10,2),
sample_count INT NOT NULL DEFAULT 0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (site_id, day_of_week, hour_of_day)
);
COMMENT ON TABLE ems.consumption_baseline_stats IS
'Historické průměry bazální spotřeby per den v týdnu a hodinu.
Plní se automaticky z telemetrie přes fn_update_baseline_stats().
Bazální = load_power_w - ev - tc (bez řízených zátěží).
Používá se jako vstup do solveru pro predikci spotřeby.';
COMMENT ON COLUMN ems.consumption_baseline_stats.avg_power_w IS
'Průměrný výkon bazální spotřeby W pro daný DOW+hodinu.
Exponenciální klouzavý průměr nová data mají větší váhu.';
COMMENT ON COLUMN ems.consumption_baseline_stats.stddev_power_w IS
'Směrodatná odchylka W míra variability spotřeby.
Lze použít pro konzervativní odhad: avg + 0.5*stddev.';

View File

@@ -0,0 +1,38 @@
-- Rozšířený horizont plánování: statistiky cen a TUV pro predikce za horizont OTE.
CREATE TABLE ems.market_price_stats (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
day_of_week INT NOT NULL,
hour_of_day INT NOT NULL,
avg_price NUMERIC(10,6) NOT NULL,
stddev_price NUMERIC(10,6),
p25_price NUMERIC(10,6),
p75_price NUMERIC(10,6),
sample_count INT NOT NULL DEFAULT 0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (site_id, day_of_week, hour_of_day)
);
COMMENT ON TABLE ems.market_price_stats IS
'Historické průměry spotové ceny OTE per DOW+hodina.
Analogie consumption_baseline_stats pro ceny.
Používá se pro predikci cen za horizont OTE (36h+).
Min. 3 měsíce dat pro smysluplné průměry.';
CREATE TABLE ems.tuv_usage_stats (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
day_of_week INT NOT NULL,
hour_of_day INT NOT NULL,
avg_temp_delta_c NUMERIC(6,3) NOT NULL,
stddev_temp_delta NUMERIC(6,3),
sample_count INT NOT NULL DEFAULT 0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (site_id, day_of_week, hour_of_day)
);
COMMENT ON TABLE ems.tuv_usage_stats IS
'Průměrná změna teploty TUV zásobníku per DOW+hodina.
Záporná hodnota = zásobník se ochlazuje (spotřeba teplé vody).
Kladná = TČ ohřívalo. Používá se pro predikci kdy bude potřeba ohřev.';

View File

@@ -0,0 +1,54 @@
-- Modbus command journal + cut-off switch audit (EMS)
CREATE TABLE ems.modbus_command (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
asset_type TEXT NOT NULL, -- 'inverter', 'ev_charger', 'heat_pump', 'cutoff'
asset_id INT NOT NULL, -- ID v příslušné asset tabulce
asset_code TEXT NOT NULL, -- např. 'deye-main' (denorm. pro čitelnost)
device_host TEXT NOT NULL,
device_port INT NOT NULL,
device_unit_id INT NOT NULL,
register INT NOT NULL, -- číslo registru (decimal)
register_name TEXT, -- 'export_limit', 'charge_limit' (pro čitelnost)
value_to_write INT NOT NULL,
value_written INT, -- skutečně zapsaná hodnota (NULL = nezapsáno)
value_verified INT, -- přečtená hodnota po verifikaci (NULL = neověřeno)
status TEXT NOT NULL DEFAULT 'pending',
-- 'pending', 'written', 'verified', 'failed', 'mismatch', 'retrying'
planning_run_id INT REFERENCES ems.planning_run(id),
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
written_at TIMESTAMPTZ,
verified_at TIMESTAMPTZ,
error_msg TEXT
);
COMMENT ON TABLE ems.modbus_command IS
'Command journal pro Modbus zápisy do zařízení.
Každý zápis = jeden řádek. Verifikační job ověří value_verified == value_to_write.
Při mismatch: retry max 3× → přepnout na SELF_SUSTAIN + Discord alert.
asset_type+asset_id odkazuje na příslušnou asset tabulku (inverter/ev_charger/...).';
CREATE INDEX idx_modbus_command_status
ON ems.modbus_command (site_id, status, created_at DESC);
CREATE INDEX idx_modbus_command_pending
ON ems.modbus_command (status, created_at)
WHERE status IN ('pending', 'retrying');
CREATE TABLE ems.cutoff_switch_log (
id SERIAL PRIMARY KEY,
site_id INT NOT NULL REFERENCES ems.site(id),
asset_code TEXT NOT NULL, -- 'cutoff-pv-b', 'cutoff-microinverter'
switched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
new_state BOOLEAN NOT NULL, -- true=zapnuto/připojeno, false=odpojeno
previous_state BOOLEAN,
reason TEXT NOT NULL, -- 'negative_sell_price', 'manual', 'auto_restore'
sell_price_czk NUMERIC(10,6), -- spotová cena v době přepnutí
triggered_by TEXT -- 'control_exporter', 'user:jan', 'system'
);
COMMENT ON TABLE ems.cutoff_switch_log IS
'Log přepnutí cut-off přepínačů. Loguje se jen při změně stavu (edge trigger).
Používá se pro mikroinvertory na GEN portu při záporných prodejních cenách.';

View File

@@ -0,0 +1,8 @@
-- Označení intervalů plánu, kde solver použil predikovanou cenu (mimo přesné OTE v efektivní ceně).
ALTER TABLE ems.planning_interval
ADD COLUMN IF NOT EXISTS is_predicted_price BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN ems.planning_interval.is_predicted_price IS
'True pokud cena pro tento slot pochází z predikce (market_price_stats)
a ne z přesných OTE dat. Sloty > 36h od now() při daily_plan běhu.';

View File

@@ -0,0 +1,8 @@
-- Fyzický režim Deye u záznamů journalu (PASSIVE / SELL / CHARGE)
ALTER TABLE ems.modbus_command
ADD COLUMN IF NOT EXISTS deye_physical_mode TEXT;
COMMENT ON COLUMN ems.modbus_command.deye_physical_mode IS
'Fyzický režim Deye při zápisu: PASSIVE / SELL / CHARGE.
Slouží pro audit a analýzu přepínání režimů.';

View File

@@ -0,0 +1,11 @@
-- Tune battery economics for planning behavior:
-- - lower reserve SOC to allow economically justified discharge
-- - lower degradation cost to avoid overly conservative cycling
--
-- Idempotent update for currently deployed sites.
UPDATE ems.asset_battery
SET
reserve_soc_percent = 10.00,
degradation_cost_czk_kwh = 0.1500
WHERE reserve_soc_percent <> 10.00
OR degradation_cost_czk_kwh <> 0.1500;