implementace Ekonomiky
This commit is contained in:
@@ -16,9 +16,11 @@ SECURITY DEFINER
|
||||
SET search_path = pg_catalog, public
|
||||
AS $$
|
||||
DECLARE
|
||||
hid integer;
|
||||
chunk_ids integer[];
|
||||
n integer;
|
||||
hid integer;
|
||||
chunk_ids integer[];
|
||||
n integer;
|
||||
has_dropped boolean;
|
||||
q text;
|
||||
BEGIN
|
||||
SELECT h.id
|
||||
INTO hid
|
||||
@@ -40,18 +42,39 @@ BEGIN
|
||||
PERFORM _timescaledb_functions.remove_dropped_chunk_metadata(hid);
|
||||
END IF;
|
||||
|
||||
SELECT coalesce(array_agg(c.id ORDER BY c.id), ARRAY[]::integer[])
|
||||
INTO chunk_ids
|
||||
FROM _timescaledb_catalog.chunk c
|
||||
WHERE c.hypertable_id = hid
|
||||
AND NOT c.dropped
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_class cl
|
||||
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
||||
WHERE ns.nspname = c.schema_name
|
||||
AND cl.relname = c.table_name
|
||||
);
|
||||
-- Sloupec chunk.dropped byl v novějším TimescaleDB odstraněn (metadata dropnutých chunků se maže).
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_attribute a
|
||||
JOIN pg_catalog.pg_class r ON r.oid = a.attrelid
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = r.relnamespace
|
||||
WHERE n.nspname = '_timescaledb_catalog'
|
||||
AND r.relname = 'chunk'
|
||||
AND a.attname = 'dropped'
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
)
|
||||
INTO has_dropped;
|
||||
|
||||
q := format(
|
||||
$fmt$
|
||||
SELECT coalesce(array_agg(c.id ORDER BY c.id), ARRAY[]::integer[])
|
||||
FROM _timescaledb_catalog.chunk c
|
||||
WHERE c.hypertable_id = %s
|
||||
%s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_class cl
|
||||
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
||||
WHERE ns.nspname = c.schema_name
|
||||
AND cl.relname = c.table_name
|
||||
)
|
||||
$fmt$,
|
||||
hid,
|
||||
CASE WHEN has_dropped THEN 'AND NOT c.dropped' ELSE '' END
|
||||
);
|
||||
|
||||
EXECUTE q INTO chunk_ids;
|
||||
|
||||
n := coalesce(array_length(chunk_ids, 1), 0);
|
||||
IF n = 0 THEN
|
||||
|
||||
18
db/migration/V036__pv_array_telemetry_source.sql
Normal file
18
db/migration/V036__pv_array_telemetry_source.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- =============================================================
|
||||
-- V036 – asset_pv_array.telemetry_source
|
||||
-- Explicitní mapování FVE pole → sloupec v telemetry_inverter.
|
||||
-- =============================================================
|
||||
|
||||
ALTER TABLE ems.asset_pv_array
|
||||
ADD COLUMN IF NOT EXISTS telemetry_source TEXT;
|
||||
|
||||
COMMENT ON COLUMN ems.asset_pv_array.telemetry_source IS
|
||||
'Který sloupec v telemetry_inverter odpovídá tomuto poli.
|
||||
gen_port = gen_port_power_w (AC-coupled pole na GEN portu),
|
||||
pv_strings = pv1_power_w + pv2_power_w (DC stringy na MPPT),
|
||||
pv_total = pv_power_w (souhrnné, pokud pole nelze rozlišit).
|
||||
NULL = pole nemá přímou telemetrii (fallback na forecast).';
|
||||
|
||||
-- Seed pro referenční site home-01:
|
||||
UPDATE ems.asset_pv_array SET telemetry_source = 'pv_strings' WHERE code = 'pv-a';
|
||||
UPDATE ems.asset_pv_array SET telemetry_source = 'gen_port' WHERE code = 'pv-b';
|
||||
23
db/migration/V037__audit_day_lock.sql
Normal file
23
db/migration/V037__audit_day_lock.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- =============================================================
|
||||
-- V037 – audit_day_lock
|
||||
-- Zamknuté (finalizované) denní ekonomické výsledky.
|
||||
-- =============================================================
|
||||
|
||||
CREATE TABLE ems.audit_day_lock (
|
||||
site_id INT NOT NULL REFERENCES ems.site(id),
|
||||
day_local DATE NOT NULL,
|
||||
import_cost_czk NUMERIC(12,2) NOT NULL,
|
||||
export_revenue_czk NUMERIC(12,2) NOT NULL,
|
||||
net_cost_czk NUMERIC(12,2) NOT NULL,
|
||||
green_bonus_czk NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
total_balance_czk NUMERIC(12,2) NOT NULL,
|
||||
locked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
locked_by TEXT NOT NULL DEFAULT 'user',
|
||||
notes TEXT,
|
||||
PRIMARY KEY (site_id, day_local)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ems.audit_day_lock IS
|
||||
'Zamknuté (finalizované) denní ekonomické výsledky.
|
||||
Když řádek existuje, frontend zobrazí tyto hodnoty místo dynamických z vw_economics_daily.
|
||||
Uživatel zamkne den, až má jistotu o cenách – snapshot aktuálních dynamických hodnot.';
|
||||
@@ -92,24 +92,49 @@ BEGIN
|
||||
ELSE COALESCE(v_sell_price, 0) END;
|
||||
END IF;
|
||||
|
||||
-- Zelený bonus: výroba bonusových polí z poslední ok predikce pro slot (Wh = W × 0,25 h)
|
||||
-- Zelený bonus: výroba bonusových polí z reálné telemetrie (Wh = průměr W × 0,25 h)
|
||||
v_pv_b_production_wh := NULL;
|
||||
FOR r_bonus IN
|
||||
SELECT id
|
||||
FROM ems.asset_pv_array
|
||||
WHERE site_id = p_site_id
|
||||
AND green_bonus_czk_kwh IS NOT NULL
|
||||
SELECT pa.id, pa.inverter_id, pa.telemetry_source
|
||||
FROM ems.asset_pv_array pa
|
||||
WHERE pa.site_id = p_site_id
|
||||
AND pa.green_bonus_czk_kwh IS NOT NULL
|
||||
AND pa.green_bonus_valid_from <= p_interval_start::DATE
|
||||
AND (pa.green_bonus_valid_to IS NULL
|
||||
OR pa.green_bonus_valid_to > p_interval_start::DATE)
|
||||
LOOP
|
||||
SELECT fpi.power_w * 0.25
|
||||
INTO v_array_prod_wh
|
||||
FROM ems.forecast_pv_interval fpi
|
||||
JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
|
||||
WHERE fpr.site_id = p_site_id
|
||||
AND fpr.pv_array_id = r_bonus.id
|
||||
AND fpi.interval_start = p_interval_start
|
||||
AND fpr.status = 'ok'
|
||||
ORDER BY fpr.created_at DESC
|
||||
LIMIT 1;
|
||||
v_array_prod_wh := NULL;
|
||||
|
||||
IF r_bonus.telemetry_source IS NOT NULL AND r_bonus.inverter_id IS NOT NULL THEN
|
||||
SELECT AVG(
|
||||
CASE r_bonus.telemetry_source
|
||||
WHEN 'gen_port' THEN ti.gen_port_power_w
|
||||
WHEN 'pv_strings' THEN COALESCE(ti.pv1_power_w, 0)
|
||||
+ COALESCE(ti.pv2_power_w, 0)
|
||||
WHEN 'pv_total' THEN ti.pv_power_w
|
||||
ELSE NULL
|
||||
END
|
||||
)::NUMERIC * 0.25
|
||||
INTO v_array_prod_wh
|
||||
FROM ems.telemetry_inverter ti
|
||||
WHERE ti.inverter_id = r_bonus.inverter_id
|
||||
AND ti.measured_at >= p_interval_start
|
||||
AND ti.measured_at < v_interval_end;
|
||||
END IF;
|
||||
|
||||
-- Fallback na forecast pokud telemetrie není k dispozici
|
||||
IF v_array_prod_wh IS NULL THEN
|
||||
SELECT fpi.power_w * 0.25
|
||||
INTO v_array_prod_wh
|
||||
FROM ems.forecast_pv_interval fpi
|
||||
JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
|
||||
WHERE fpr.site_id = p_site_id
|
||||
AND fpr.pv_array_id = r_bonus.id
|
||||
AND fpi.interval_start = p_interval_start
|
||||
AND fpr.status = 'ok'
|
||||
ORDER BY fpr.created_at DESC
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
v_array_prod_wh := COALESCE(v_array_prod_wh, 0);
|
||||
IF v_pv_b_production_wh IS NULL THEN
|
||||
@@ -175,7 +200,8 @@ $$;
|
||||
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.
|
||||
Zelený bonus: součet přes všechna pole s nastaveným bonusem, výroba z poslední ok forecast_pv_interval.
|
||||
Zelený bonus: součet přes pole s green_bonus_czk_kwh; výroba primárně z reálné telemetrie
|
||||
(dle asset_pv_array.telemetry_source), fallback na forecast_pv_interval pokud telemetrie chybí.
|
||||
Volat každých 15 minut pro interval který právě skončil.';
|
||||
|
||||
-- ============================================================
|
||||
|
||||
75
db/views/R__vw_site_effective_price_economics.sql
Normal file
75
db/views/R__vw_site_effective_price_economics.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- =============================================================
|
||||
-- R__vw_site_effective_price_economics.sql
|
||||
-- EMS Platform – ekonomické views (závisí na vw_site_effective_price)
|
||||
-- Musí běžet až PO R__vw_site_effective_price.sql (abecední pořadí Flyway).
|
||||
-- Repeatable migration
|
||||
-- =============================================================
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_economics_interval AS
|
||||
SELECT
|
||||
ai.site_id,
|
||||
ai.interval_start,
|
||||
ROUND(GREATEST(ai.actual_grid_power_w, 0)::NUMERIC / 4000, 4) AS import_kwh,
|
||||
ROUND(ABS(LEAST(ai.actual_grid_power_w, 0))::NUMERIC / 4000, 4) AS export_kwh,
|
||||
CASE WHEN ai.actual_grid_power_w >= 0
|
||||
THEN ROUND((ai.actual_grid_power_w::NUMERIC / 4000)
|
||||
* COALESCE(ep.effective_buy_price_czk_kwh, 0), 4)
|
||||
ELSE ROUND((ai.actual_grid_power_w::NUMERIC / 4000)
|
||||
* COALESCE(ep.effective_sell_price_czk_kwh, 0), 4)
|
||||
END AS dynamic_cost_czk,
|
||||
ai.actual_cost_czk AS stored_cost_czk,
|
||||
ai.green_bonus_czk,
|
||||
pi.expected_cost_czk AS planned_cost_czk,
|
||||
pi.grid_setpoint_w AS planned_grid_w,
|
||||
ai.actual_grid_power_w,
|
||||
ep.effective_buy_price_czk_kwh,
|
||||
ep.effective_sell_price_czk_kwh,
|
||||
pi.effective_buy_price AS planned_buy_price,
|
||||
pi.effective_sell_price AS planned_sell_price,
|
||||
ai.actual_pv_power_w,
|
||||
ai.actual_load_power_w,
|
||||
ai.actual_ev_power_w,
|
||||
ai.actual_heat_pump_power_w,
|
||||
ai.actual_battery_power_w,
|
||||
ai.actual_battery_soc_pct
|
||||
FROM ems.audit_interval ai
|
||||
LEFT JOIN ems.vw_site_effective_price ep
|
||||
ON ep.site_id = ai.site_id AND ep.interval_start = ai.interval_start
|
||||
LEFT JOIN ems.planning_interval pi
|
||||
ON pi.run_id = ai.planning_run_id AND pi.interval_start = ai.interval_start;
|
||||
|
||||
COMMENT ON VIEW ems.vw_economics_interval IS
|
||||
'Dynamické ekonomické vyhodnocení per 15min slot (závisí na vw_site_effective_price).';
|
||||
|
||||
CREATE OR REPLACE VIEW ems.vw_economics_daily AS
|
||||
SELECT
|
||||
site_id,
|
||||
date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague')::date AS day_local,
|
||||
COUNT(*)::int AS interval_count,
|
||||
ROUND(SUM(import_kwh), 3) AS import_kwh,
|
||||
ROUND(SUM(export_kwh), 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,
|
||||
ROUND(SUM(GREATEST(actual_pv_power_w, 0)::NUMERIC / 4000)
|
||||
- SUM(export_kwh), 3) AS self_consumption_kwh,
|
||||
ROUND(SUM(CASE WHEN dynamic_cost_czk > 0
|
||||
THEN dynamic_cost_czk ELSE 0 END), 2) AS import_cost_czk,
|
||||
ROUND(SUM(CASE WHEN dynamic_cost_czk < 0
|
||||
THEN ABS(dynamic_cost_czk) ELSE 0 END), 2) AS export_revenue_czk,
|
||||
ROUND(SUM(dynamic_cost_czk), 2) AS net_cost_czk,
|
||||
ROUND(COALESCE(SUM(green_bonus_czk), 0), 2) AS green_bonus_czk,
|
||||
ROUND(-SUM(dynamic_cost_czk)
|
||||
+ COALESCE(SUM(green_bonus_czk), 0), 2) AS total_balance_czk,
|
||||
ROUND(SUM(planned_cost_czk), 2) AS planned_net_cost_czk,
|
||||
ROUND(-COALESCE(SUM(planned_cost_czk), 0)
|
||||
+ COALESCE(SUM(green_bonus_czk), 0), 2) AS planned_balance_czk,
|
||||
ROUND(SUM(dynamic_cost_czk)
|
||||
- COALESCE(SUM(planned_cost_czk), 0), 2) AS deviation_cost_czk
|
||||
FROM ems.vw_economics_interval
|
||||
GROUP BY site_id,
|
||||
date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague')::date;
|
||||
|
||||
COMMENT ON VIEW ems.vw_economics_daily IS
|
||||
'Denní souhrn ekonomiky (závisí na vw_economics_interval).';
|
||||
Reference in New Issue
Block a user