rebuild consumpton baselaline
Some checks failed
CI and deploy / migration-check (push) Failing after 10s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-02 13:56:35 +02:00
parent b20cb6e0f9
commit 343f2f9847
7 changed files with 262 additions and 3 deletions

View File

@@ -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). 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[T1]` (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`. 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[T1]` (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). | | `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. | | `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`.
--- ---

View File

@@ -78,7 +78,8 @@ $$;
COMMENT ON FUNCTION ems.fn_update_baseline_stats(INT, INT) IS 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í. '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í. 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( CREATE OR REPLACE FUNCTION ems.fn_get_baseline_forecast(

View File

@@ -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.';

View File

@@ -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. **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`. > **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`.
--- ---

View File

@@ -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é) ## Konfigurace (env proměnné)
```env ```env

View File

@@ -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."

View File

@@ -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."