diff --git a/CLAUDE.md b/CLAUDE.md index 5bdd94f..7917a7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,7 +102,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st 14. **Příchod a odjezd EV** detekuje `telemetry_collector` z telemetrie nabíječky: přechod `available` → `preparing` / `charging` (resp. jakýkoli stav ≠ `available`) znamená příjezd; přechod na `available` uzavře `ev_session`. Tabulka `ev_arrival_stats` se při příjezdu doplňuje přes `fn_update_ev_arrival_stats` a **nemá se mazat** (dlouhodobá historická statistika). -15. **Bazální spotřeba** = `load_power_w` minus řízené zátěže (součet EV z `telemetry_ev_charger`, TČ z `telemetry_heat_pump`). Tabulka `consumption_baseline_stats` se plní denně (APScheduler 00:30) přes `fn_update_baseline_stats`. **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`. +15. **Bazální spotřeba** = `load_power_w` minus řízené zátěže (součet EV z `telemetry_ev_charger`, TČ z `telemetry_heat_pump`). Tabulka `consumption_baseline_stats` se plní denně (APScheduler 00:30) přes `fn_update_baseline_stats`; **bez EMA „ocasu“** přepočítáš smaž+hromadný update přes **`ems.fn_rebuild_consumption_baseline_stats(site_id, lookback)`** (`site_id NULL` → všechny lokality). **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`. 16. **Dynamický horizont plánování (jen OTE):** konec okna z **`ems.fn_planning_horizon_end(site_id, horizon_start)`** (`min` posledního OTE konce a `start + 36 h`, NULL pokud je známé OTE kratší než **1 h** od startu – rolling se přeskočí; denní plán při NULL použije **1 h** fallback v Pythonu). Strop a práh měnit v SQL (defaultní argumenty funkce / repeatable migrace), ne přes env. Solver používá **výhradně** sloty s efektivní cenou z `vw_site_effective_price` (žádná predikce v LP). Účelová funkce má **terminal SoC shadow price**: `−(průměr buy v prvních 24 h slotů × planner_terminal_soc_value_factor / 1000) × soc[T−1]` (Kč; SoC v Wh), kde **`planner_terminal_soc_value_factor`** je **`ems.asset_battery.planner_terminal_soc_value_factor`** načtené přes **`ems.fn_planning_site_context`** (žádný skrytý faktor v Pythonu). `market_price_stats` / `fn_get_predicted_price` zůstávají pro statistiky a budoucí rozšíření; detail historie: `docs/04-modules/planning-extended-horizon.md`. @@ -159,7 +159,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st | `signal_state` | Poslední požadovaná / odeslaná / ověřená hodnota na cíli (idempotence). | | `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. | -**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`. +**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_rebuild_consumption_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`. --- diff --git a/db/routines/R__003_fn_baseline_consumption.sql b/db/routines/R__003_fn_baseline_consumption.sql index b82c025..d3a10b1 100644 --- a/db/routines/R__003_fn_baseline_consumption.sql +++ b/db/routines/R__003_fn_baseline_consumption.sql @@ -78,7 +78,8 @@ $$; COMMENT ON FUNCTION ems.fn_update_baseline_stats(INT, INT) IS 'Aktualizuje průměry bazální spotřeby z telemetrie posledních N dní. Používá exponenciální klouzavý průměr (EMA 70/30) pro postupné zpřesňování. -Volat denně po půlnoci. Pro první naplnění: fn_update_baseline_stats(2, 90).'; +Volat denně po půlnoci. Pro první naplnění: fn_update_baseline_stats(2, 90). +Pro úplný reset bucketů bez „ocasu“ EMA smaž řádky a znovu volej, nebo ems.fn_rebuild_consumption_baseline_stats.'; CREATE OR REPLACE FUNCTION ems.fn_get_baseline_forecast( diff --git a/db/routines/R__085_fn_rebuild_consumption_baseline_stats.sql b/db/routines/R__085_fn_rebuild_consumption_baseline_stats.sql new file mode 100644 index 0000000..9a9ad97 --- /dev/null +++ b/db/routines/R__085_fn_rebuild_consumption_baseline_stats.sql @@ -0,0 +1,54 @@ +-- Přepnutí profilu bazální spotřeby bez EMA ocasu z předchozích běhů: +-- smaže řádky consumption_baseline_stats a znovu je naplní fn_update_baseline_stats. + +create or replace function ems.fn_rebuild_consumption_baseline_stats( + p_site_id int default null, + p_lookback_days int default 30 +) +returns table ( + site_id int, + buckets_upserted int +) +language plpgsql +volatile +as $fn$ +declare + r record; +begin + if p_lookback_days is null or p_lookback_days < 1 then + raise exception using + message = 'p_lookback_days musí být kladný int (např. 30)', + errcode = '22023'; + end if; + + if p_site_id is null then + delete from ems.consumption_baseline_stats; + + for r in + select s.id as sid from ems.site s order by s.id + loop + site_id := r.sid::int; + buckets_upserted := ems.fn_update_baseline_stats(r.sid::int, p_lookback_days); + return next; + end loop; + + return; + end if; + + if not exists (select 1 from ems.site s where s.id = p_site_id) then + raise exception using + message = format('site_id %s neexistuje v ems.site', p_site_id), + errcode = 'P0001'; + end if; + + delete from ems.consumption_baseline_stats c + where c.site_id = p_site_id; + + site_id := p_site_id; + buckets_upserted := ems.fn_update_baseline_stats(p_site_id, p_lookback_days); + return next; +end; +$fn$; + +comment on function ems.fn_rebuild_consumption_baseline_stats is +'Maze řádky v consumption_baseline_stats pro jednu site (nenull p_site_id) nebo celou tabulku při p_site_id NULL, pak pro každý ems.site volá fn_update_baseline_stats. Řeší zaseknutí starého avg díky EMA 70/30 v fn_update. Příklad jedné lokality: select * from ems.fn_rebuild_consumption_baseline_stats(2, 30); všechny lokality: select * from ems.fn_rebuild_consumption_baseline_stats(null::int, 14); Nepředávej jen jednu číslici bez pojmenovaných argumentů — první pozice je site_id ne lookback.'; diff --git a/docs/04-modules/consumption.md b/docs/04-modules/consumption.md index 6ac22a3..a002497 100644 --- a/docs/04-modules/consumption.md +++ b/docs/04-modules/consumption.md @@ -44,6 +44,8 @@ bazální_w = load_power_w - ev_power_w - heat_pump_power_w **Solver (`planning_engine._load_slots`):** pro každý 15min interval efektivní ceny bere **`avg_power_w` z `consumption_baseline_stats`** podle DOW+hodiny slotu, jinak **500 W** – nečte `consumption_baseline_interval`. Stejná hodnota se ukládá do **`planning_interval.load_baseline_w`** při každém běhu plánovače (přehled v UI / PostgREST). Odchylka vs. skutečnost: tabulka **`baseline_load_forecast_accuracy`**, plněno po auditu. +**Operace: přepočet bez EMA „ocasu“:** denní job volá `fn_update_baseline_stats`, které při updatu bucketu míchá **70 % starý + 30 % nový** průměr. Je-li profil zaseklý, smaž statistiky a znovu načti z telemetrie — kanonické API je **`ems.fn_rebuild_consumption_baseline_stats(p_site_id, p_lookback_days)` v `db/routines/R__085_fn_rebuild_consumption_baseline_stats.sql`**: při **`p_site_id IS NULL`** maže celou `consumption_baseline_stats` a přepíná všechny řádky z `ems.site`; při konkrétním `site_id` jen řádky dané lokality. **Příklad (psql / MCP):** `select * from ems.fn_rebuild_consumption_baseline_stats(2, 30);` jedna lokality; **`select * from ems.fn_rebuild_consumption_baseline_stats(null::int, 30);`** všechny lokality *(první argument je site_id — ne zaměnit s počtem dnů).* Tenký wrapper: **`scripts/rebuild_consumption_baseline_stats.sh`**. Špatná měření (EV/TČ) funkce sama neopraví. + > **Poznámka:** TUV jako samostatný odečet zůstává otevřený bod, pokud není měřen zvlášť; aktuálně je TČ zahrnut v `heat_pump_power_w`. --- diff --git a/docs/04-modules/forecast.md b/docs/04-modules/forecast.md index b17275e..1c8ba87 100644 --- a/docs/04-modules/forecast.md +++ b/docs/04-modules/forecast.md @@ -171,6 +171,18 @@ Viz `03-data-model.md`: --- +## Jednorázové smazání PV forecastu za den (provoz) + +Projekt standardně **nemazá** `forecast_pv_interval` / `forecast_pv_run`, aby zůstala historie pro `forecast_accuracy` a učení delty. Pokud potřebuješ záměrně smazat řádky FVE predikce za **jeden kalendářní den v `Europe/Prague`** (a znovu naplnit službou forecastu), použij: + +`scripts/wipe_pv_forecast_prague_day.sh` (volitelně `SITE_ID`, `DRY_RUN=1`; vyžaduje `DATABASE_URL` nebo PG env). + +Skript maže v pořadí: `forecast_accuracy` (odpovídající páry `run_id`/`interval_start`), `forecast_pv_interval` (PK řádků ve zvoleném dni), pak `forecast_pv_run`, které po smazání už nemají žádné intervaly — **pouze pokud** jejich `id` bylo v mazaném výběru (`IN` z cílové množiny), aby se neodstraňovaly náhodné orphan běhy z jiných událostí. + +**Není to** mazání statistiky bazální spotřeby (`consumption_baseline_*`); pokud tě trápí load v plánovači, řeš výpočet/`fn_update_baseline_stats`, ne tento skript. + +--- + ## Konfigurace (env proměnné) ```env diff --git a/scripts/rebuild_consumption_baseline_stats.sh b/scripts/rebuild_consumption_baseline_stats.sh new file mode 100755 index 0000000..484b6fd --- /dev/null +++ b/scripts/rebuild_consumption_baseline_stats.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Tenký wrapper nad ems.fn_rebuild_consumption_baseline_stats (kanonické API je v PostgreSQL). +# +# Použití: +# export DATABASE_URL='postgres://…/ems' +# LOOKBACK_DAYS=14 ./scripts/rebuild_consumption_baseline_stats.sh 2 # jedna lokality (site.id) +# +# ./scripts/rebuild_consumption_baseline_stats.sh --all # celá tabulka stats + všichni sites +# +# MCP / psql přímo (doporučeno v SQL-first režimu): +# select * from ems.fn_rebuild_consumption_baseline_stats(2, 30); +# select * from ems.fn_rebuild_consumption_baseline_stats(null::int, 30); +# +# DRY_RUN=1 … — jen řádek count (žádné mazání). + +set -euo pipefail + +if [[ -z "${DATABASE_URL:-}" ]] && [[ -z "${PGHOST:-}" ]]; then + echo "Nastav DATABASE_URL nebo PG proměnné." >&2 + exit 1 +fi + +LOOKBACK="${LOOKBACK_DAYS:-30}" +if ! [[ "$LOOKBACK" =~ ^[0-9]+$ ]] || [[ "$LOOKBACK" -lt 1 ]]; then + echo "LOOKBACK_DAYS musí být kladné celé číslo." >&2 + exit 1 +fi + +PSQL=(psql -v ON_ERROR_STOP=1) +if [[ -n "${DATABASE_URL:-}" ]]; then + PSQL+=("$DATABASE_URL") +else + PSQL+=("${PGDATABASE:-ems}") +fi + +MODE="${1:?Chybí argument: site_id (číslo) nebo --all}" +DRY="${DRY_RUN:-0}" + +if [[ "$MODE" == "--all" ]]; then + if [[ "$DRY" == "1" ]] || [[ "$DRY" == "true" ]]; then + echo "DRY_RUN: consumption_baseline_stats řádků celkem, sites:" + "${PSQL[@]}" -c " + select count(*) as stats_rows from ems.consumption_baseline_stats; + select count(*) as sites from ems.site; + " + exit 0 + fi + echo "Volám fn_rebuild_consumption_baseline_stats(null, ${LOOKBACK}) …" + "${PSQL[@]}" -c "select * from ems.fn_rebuild_consumption_baseline_stats(null::int, ${LOOKBACK}::int);" +else + SITE_ID="$MODE" + if ! [[ "$SITE_ID" =~ ^[0-9]+$ ]]; then + echo "První argument: site_id (číslo) nebo --all." >&2 + exit 1 + fi + if [[ "$DRY" == "1" ]] || [[ "$DRY" == "true" ]]; then + "${PSQL[@]}" -c "select count(*) from ems.consumption_baseline_stats where site_id = ${SITE_ID}::int;" + exit 0 + fi + echo "Volám fn_rebuild_consumption_baseline_stats(${SITE_ID}, ${LOOKBACK}) …" + "${PSQL[@]}" -c "select * from ems.fn_rebuild_consumption_baseline_stats(${SITE_ID}::int, ${LOOKBACK}::int);" +fi + +echo "Hotovo. Spusť rolling/denní plán nebo počkej na scheduler." diff --git a/scripts/wipe_pv_forecast_prague_day.sh b/scripts/wipe_pv_forecast_prague_day.sh new file mode 100755 index 0000000..470e206 --- /dev/null +++ b/scripts/wipe_pv_forecast_prague_day.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Odstraní predikované PV intervaly (a navázaný tracking přesnosti) pro jeden kalendářní den v Europe/Prague. +# +# VAROVÁNÍ (provozní / datová kontinuita) +# ──────────────────────────────────────── +# - Řád v repozitáři je držet historické běhy FVE forecastu pro analýzu a učení delty (@see docs/04-modules/forecast.md). +# - Používej jen když vědomě potřebuješ „načisto“ vygenerovat nový forecast (forecast service). +# - Fyzický breaker řeší měnič/Deye — skript jen čistí databázi od uloženého PV forecastu. +# +# Nenahrazuje mazání/load baseline spotřeby (`consumption_baseline_stats` / její výpočet). +# +# Použití: +# export DATABASE_URL='postgres://…/ems' +# ./scripts/wipe_pv_forecast_prague_day.sh # dnešní den (Europe/Prague), všechny lokality +# ./scripts/wipe_pv_forecast_prague_day.sh 2026-05-02 # konkrétní datum YYYY-MM-DD +# ./scripts/wipe_pv_forecast_prague_day.sh 2026-05-02 2 # jen site.id = 2 (např. home-01) +# +# Šedý režim (jen počty, žádné mazání): +# DRY_RUN=1 ./scripts/wipe_pv_forecast_prague_day.sh 2026-05-02 2 + +set -euo pipefail + +DAY="${1:-$(TZ=Europe/Prague date +%Y-%m-%d)}" +SITE_ID="${2:-}" + +if ! [[ "$DAY" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Datum musí být YYYY-MM-DD, dostal jsem: $DAY" >&2 + exit 1 +fi + +if [[ -z "${DATABASE_URL:-}" ]] && [[ -z "${PGHOST:-}" ]]; then + echo "Nastav DATABASE_URL nebo standardní PG proměnné (PGHOST, PGUSER, PGDATABASE, …)." >&2 + exit 1 +fi + +PSQL=(psql -v ON_ERROR_STOP=1) +if [[ -n "${DATABASE_URL:-}" ]]; then + PSQL+=("$DATABASE_URL") +else + PSQL+=("${PGDATABASE:-ems}") +fi + +SITE_CLAUSE="" +if [[ -n "$SITE_ID" ]]; then + if ! [[ "$SITE_ID" =~ ^[0-9]+$ ]]; then + echo "Druhý argument musí být číslo site_id." >&2 + exit 1 + fi + SITE_CLAUSE="AND r.site_id = ${SITE_ID}::int" +fi + +DRY="${DRY_RUN:-0}" + +if [[ "$DRY" == "1" ]] || [[ "$DRY" == "true" ]]; then + echo "DRY_RUN: den $DAY (Europe/Prague)${SITE_ID:+ site_id=$SITE_ID}" + "${PSQL[@]}" -c " + with bounds as ( + select + ('${DAY}'::date::text || ' 00:00:00')::timestamp at time zone 'Europe/Prague' as ts_start, + (('${DAY}'::date + 1)::text || ' 00:00:00')::timestamp at time zone 'Europe/Prague' as ts_end + ), + targets as ( + select fi.run_id, fi.pv_array_id, fi.interval_start + from ems.forecast_pv_interval fi + inner join ems.forecast_pv_run r on r.id = fi.run_id + cross join bounds b + where fi.interval_start >= b.ts_start + and fi.interval_start < b.ts_end + ${SITE_CLAUSE} + ) + select count(*) as interval_rows_to_delete, count(distinct run_id) as distinct_run_ids + from targets; +" + exit 0 +fi + +echo "Mažu PV forecast intervaly pro den $DAY (Europe/Prague)${SITE_ID:+ site_id=$SITE_ID} …" + +"${PSQL[@]}" -c " +begin; + +create temporary table _ems_wipe_pv_forecast_targets ( + run_id int not null, + pv_array_id int not null, + interval_start timestamptz not null +) on commit drop; + +with bounds as ( + select + ('${DAY}'::date::text || ' 00:00:00')::timestamp at time zone 'Europe/Prague' as ts_start, + (('${DAY}'::date + 1)::text || ' 00:00:00')::timestamp at time zone 'Europe/Prague' as ts_end +) +insert into _ems_wipe_pv_forecast_targets (run_id, pv_array_id, interval_start) +select fi.run_id, fi.pv_array_id, fi.interval_start +from ems.forecast_pv_interval fi +inner join ems.forecast_pv_run r on r.id = fi.run_id +cross join bounds b +where fi.interval_start >= b.ts_start + and fi.interval_start < b.ts_end + ${SITE_CLAUSE}; + +delete from ems.forecast_accuracy fa +using (select distinct run_id, interval_start from _ems_wipe_pv_forecast_targets) t +where fa.run_id = t.run_id + and fa.interval_start = t.interval_start; + +delete from ems.forecast_pv_interval fi +using _ems_wipe_pv_forecast_targets t +where fi.run_id = t.run_id + and fi.pv_array_id = t.pv_array_id + and fi.interval_start = t.interval_start; + +delete from ems.forecast_pv_run fr +where fr.id in (select distinct run_id from _ems_wipe_pv_forecast_targets) + and not exists ( + select 1 from ems.forecast_pv_interval x where x.run_id = fr.id + ); + +select + (select count(*) from _ems_wipe_pv_forecast_targets) as targets_interval_rows, + (select count(distinct run_id) from _ems_wipe_pv_forecast_targets) as distinct_run_ids_touched; + +commit; +" + +echo "Hotovo. Spusť forecast job / službu v backendu (Open-Meteo běh), aby se řádky ${DAY} doplnily znovu."