From 9d37efb991bbe91ee0781315b1674bad9139ace6 Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Wed, 29 Apr 2026 13:03:41 +0200 Subject: [PATCH] telemetrie per pv_array, fix predictinos --- .idea/data_source_mapping.xml | 6 ++ ...__pv_array_telemetry_group_and_sources.sql | 52 +++++++++++++++++ .../V073__pv_telemetry_source_def_fk.sql | 56 +++++++++++++++++++ db/routines/R__019_fn_fill_audit_interval.sql | 2 + .../R__022_fn_fill_forecast_accuracy.sql | 53 ++++++++++++++---- ...__043_fn_pv_forecast_correction_factor.sql | 33 ++++++----- 6 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 db/migration/V072__pv_array_telemetry_group_and_sources.sql create mode 100644 db/migration/V073__pv_telemetry_source_def_fk.sql diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml index 89ec326..5256de3 100644 --- a/.idea/data_source_mapping.xml +++ b/.idea/data_source_mapping.xml @@ -7,8 +7,14 @@ + + + + + + diff --git a/db/migration/V072__pv_array_telemetry_group_and_sources.sql b/db/migration/V072__pv_array_telemetry_group_and_sources.sql new file mode 100644 index 0000000..6a8c91b --- /dev/null +++ b/db/migration/V072__pv_array_telemetry_group_and_sources.sql @@ -0,0 +1,52 @@ +-- ============================================================= +-- V072 – asset_pv_array.telemetry_group + rozšíření telemetry_source +-- +-- Cíl: +-- - umožnit mapování PV pole → měřicí kanál (pv1/pv2/pv_strings/pv_total/gen_port), +-- - umožnit sdílené měření pro více polí (telemetry_group) a následnou alokaci (v routines). +-- ============================================================= + +alter table ems.asset_pv_array + add column if not exists telemetry_group 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), + pv1 = pv1_power_w (DC string 1 / MPPT1), + pv2 = pv2_power_w (DC string 2 / MPPT2), + pv_strings = pv1_power_w + pv2_power_w (souhrn DC stringů, pokud nejde rozlišit), + pv_total = pv_power_w (souhrnné PV, pokud nejde rozlišit). + NULL = pole nemá přímou telemetrii (fallback na forecast).'; + +comment on column ems.asset_pv_array.telemetry_group is +'Volitelná skupina pro sdílené měření: pokud více pv_array sdílí jeden telemetrický kanál (např. GEN port rozdělený do více orientací), +pak mají shodné (site_id, telemetry_source, telemetry_group) a routines alokují actual proporčně podle forecastu.'; + +-- --- Seed / upgrade stávajících referenčních lokalit --- + +-- home-01: dvě GEN pole sdílí jeden GEN port → stejné telemetry_group +update ems.asset_pv_array +set telemetry_source = 'gen_port', + telemetry_group = 'gen_port_1' +where site_id = (select id from ems.site where code = 'home-01') + and code in ('pv-b', 'pv-b-flat'); + +-- BA81: stringy mapujeme na PV1/PV2, mikroinvertory sdílí GEN port (alokace podle forecastu). +update ems.asset_pv_array +set telemetry_source = 'pv1', + telemetry_group = null +where site_id = (select id from ems.site where code = 'BA81') + and code = 'pv-str-1'; + +update ems.asset_pv_array +set telemetry_source = 'pv2', + telemetry_group = null +where site_id = (select id from ems.site where code = 'BA81') + and code = 'pv-str-2'; + +update ems.asset_pv_array +set telemetry_source = 'gen_port', + telemetry_group = 'gen_port_1' +where site_id = (select id from ems.site where code = 'BA81') + and code in ('pv-mi-1', 'pv-mi-2'); + diff --git a/db/migration/V073__pv_telemetry_source_def_fk.sql b/db/migration/V073__pv_telemetry_source_def_fk.sql new file mode 100644 index 0000000..da92b1a --- /dev/null +++ b/db/migration/V073__pv_telemetry_source_def_fk.sql @@ -0,0 +1,56 @@ +-- ============================================================= +-- V073 – číselník PV telemetrie + FK na asset_pv_array.telemetry_source +-- +-- Cíl: referenční integrita pro telemetry_source (povolené kódy), +-- aby se zabránilo překlepům a nekonzistentním datům. +-- ============================================================= + +create table if not exists ems.pv_telemetry_source_def ( + code text primary key, + description text not null, + telemetry_inverter_expr text null, + active boolean not null default true +); + +comment on table ems.pv_telemetry_source_def is + 'Číselník zdrojů PV telemetrie (kanálů) pro asset_pv_array.telemetry_source.'; + +comment on column ems.pv_telemetry_source_def.code is + 'Stabilní kód zdroje telemetrie (FK z asset_pv_array.telemetry_source).'; + +comment on column ems.pv_telemetry_source_def.telemetry_inverter_expr is + 'Volitelně: lidsky čitelný výraz, jak se kanál počítá z telemetry_inverter (informativní; runtime logika je v routines).'; + +insert into ems.pv_telemetry_source_def (code, description, telemetry_inverter_expr) values + ('gen_port', 'AC-coupled výroba na GEN portu (souhrn).', 'gen_port_power_w'), + ('pv1', 'DC string/MPPT 1 (samostatně).', 'pv1_power_w'), + ('pv2', 'DC string/MPPT 2 (samostatně).', 'pv2_power_w'), + ('pv_strings', 'Součet DC stringů (pv1+pv2).', 'pv1_power_w + pv2_power_w'), + ('pv_total', 'Souhrnná PV výroba (pokud nelze rozlišit).','pv_power_w') +on conflict (code) do update +set description = excluded.description, + telemetry_inverter_expr = excluded.telemetry_inverter_expr, + active = true; + +-- FK (idempotentně): NULL povolen (pole bez přímé telemetrie / fallback na forecast). +do $$ +begin + if not exists ( + select 1 + from pg_constraint c + join pg_class t on t.oid = c.conrelid + join pg_namespace n on n.oid = t.relnamespace + where n.nspname = 'ems' + and t.relname = 'asset_pv_array' + and c.conname = 'asset_pv_array_telemetry_source_fk' + ) then + alter table ems.asset_pv_array + add constraint asset_pv_array_telemetry_source_fk + foreign key (telemetry_source) + references ems.pv_telemetry_source_def(code) + on update cascade + on delete restrict; + end if; +end; +$$; + diff --git a/db/routines/R__019_fn_fill_audit_interval.sql b/db/routines/R__019_fn_fill_audit_interval.sql index 66305c4..d0f961b 100644 --- a/db/routines/R__019_fn_fill_audit_interval.sql +++ b/db/routines/R__019_fn_fill_audit_interval.sql @@ -188,6 +188,8 @@ BEGIN SELECT AVG( CASE r_bonus.telemetry_source WHEN 'gen_port' THEN ti.gen_port_power_w + WHEN 'pv1' THEN ti.pv1_power_w + WHEN 'pv2' THEN ti.pv2_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 diff --git a/db/routines/R__022_fn_fill_forecast_accuracy.sql b/db/routines/R__022_fn_fill_forecast_accuracy.sql index 8880963..bf74222 100644 --- a/db/routines/R__022_fn_fill_forecast_accuracy.sql +++ b/db/routines/R__022_fn_fill_forecast_accuracy.sql @@ -50,7 +50,9 @@ BEGIN v.learning_exclude_reason FROM ems.forecast_pv_interval fpi JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id - JOIN ems.asset_pv_array pa ON pa.id = fpr.pv_array_id + JOIN ems.asset_pv_array pa + ON pa.id = fpr.pv_array_id + AND pa.site_id = fpr.site_id LEFT JOIN ems.site_pv_forecast_calibration cal ON cal.site_id = fpr.site_id LEFT JOIN LATERAL ( @@ -115,16 +117,47 @@ BEGIN (flags.is_curtailed_learning_slot OR flags.is_telemetry_derated_slot) AS exclude_actual_for_learning ) v ON true LEFT JOIN LATERAL ( - SELECT AVG( + WITH base AS ( + SELECT AVG( + CASE coalesce(pa.telemetry_source, '') + WHEN 'pv1' THEN ti.pv1_power_w::NUMERIC + WHEN 'pv2' THEN ti.pv2_power_w::NUMERIC + WHEN 'pv_strings' THEN (COALESCE(ti.pv1_power_w, 0) + COALESCE(ti.pv2_power_w, 0))::NUMERIC + WHEN 'pv_total' THEN ti.pv_power_w::NUMERIC + WHEN 'gen_port' THEN ti.gen_port_power_w::NUMERIC + ELSE NULL + END + ) AS avg_actual_w + FROM ems.telemetry_inverter ti + WHERE ti.site_id = fpr.site_id + AND ti.measured_at >= fpi.interval_start + AND ti.measured_at < fpi.interval_start + INTERVAL '15 minutes' + ), + grp AS ( + -- Pokud více pv_array sdílí stejné měření (např. GEN port rozdělený do více orientací), + -- rozdělíme actual proporčně podle forecastu v daném slotu. + SELECT + sum(fpi2.power_w)::numeric AS forecast_group_w + FROM ems.forecast_pv_interval fpi2 + JOIN ems.forecast_pv_run fpr2 ON fpr2.id = fpi2.run_id + JOIN ems.asset_pv_array pa2 + ON pa2.id = fpi2.pv_array_id + AND pa2.site_id = fpr2.site_id + WHERE pa.telemetry_group IS NOT NULL + AND pa2.site_id = fpr.site_id + AND pa2.telemetry_group = pa.telemetry_group + AND pa2.telemetry_source = pa.telemetry_source + AND fpi2.interval_start = fpi.interval_start + AND fpr2.id = fpr.id + ) + SELECT CASE - WHEN pa.controllable = false THEN ti.gen_port_power_w::NUMERIC - ELSE (COALESCE(ti.pv1_power_w, 0) + COALESCE(ti.pv2_power_w, 0))::NUMERIC - END - ) AS avg_actual_w - FROM ems.telemetry_inverter ti - WHERE ti.site_id = fpr.site_id - AND ti.measured_at >= fpi.interval_start - AND ti.measured_at < fpi.interval_start + INTERVAL '15 minutes' + WHEN pa.telemetry_group IS NULL THEN (SELECT avg_actual_w FROM base) + WHEN (SELECT forecast_group_w FROM grp) IS NULL THEN NULL + WHEN (SELECT forecast_group_w FROM grp) <= 0 THEN NULL + WHEN (SELECT avg_actual_w FROM base) IS NULL THEN NULL + ELSE (SELECT avg_actual_w FROM base) * (fpi.power_w::numeric / (SELECT forecast_group_w FROM grp)) + END AS avg_actual_w ) slot ON true WHERE fpr.site_id = p_site_id AND fpr.status = 'ok' diff --git a/db/routines/R__043_fn_pv_forecast_correction_factor.sql b/db/routines/R__043_fn_pv_forecast_correction_factor.sql index 761ed36..17720e4 100644 --- a/db/routines/R__043_fn_pv_forecast_correction_factor.sql +++ b/db/routines/R__043_fn_pv_forecast_correction_factor.sql @@ -25,23 +25,30 @@ begin and ti.measured_at >= p_window_start and ti.measured_at < p_window_end; + with pv_arrays as ( + select apa.id as pv_array_id + from ems.asset_pv_array apa + where apa.site_id = p_site_id + ), + latest_run as ( + select distinct on (fpr.pv_array_id) + fpr.pv_array_id, + fpr.id as run_id + from pv_arrays pa + join ems.forecast_pv_run fpr + on fpr.pv_array_id = pa.pv_array_id + and fpr.site_id = p_site_id + where fpr.status = 'ok' + and fpr.created_at <= p_window_start + order by fpr.pv_array_id, fpr.created_at desc + ) select coalesce(sum(fpi.power_w) * 0.25 / 1000.0, 0) into v_forecast 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 fpi.interval_start >= p_window_start + join latest_run lr on lr.run_id = fpi.run_id + where fpi.interval_start >= p_window_start and fpi.interval_start < p_window_end - and fpr.status = 'ok' - and fpr.id = ( - select fpr2.id - from ems.forecast_pv_run fpr2 - where fpr2.site_id = p_site_id - and fpr2.status = 'ok' - and fpr2.created_at <= p_window_start - order by fpr2.created_at desc - limit 1 - ); + and fpi.pv_array_id = lr.pv_array_id; if v_forecast < 0.1 or coalesce(v_actual, 0) < 0.05 then return jsonb_build_object(