diff --git a/backend/app/main.py b/backend/app/main.py index bac3569..abb72e3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1009,7 +1009,7 @@ async def get_site_forecast_pv( rows = await conn.fetch( """ SELECT run_id, pv_array_id, interval_start, power_w, - irradiance_wm2, temp_c, pv_array_code + irradiance_wm2, temp_c, pv_array_code, controllable FROM ( SELECT DISTINCT ON (fpi.interval_start, fpr.pv_array_id) fpi.run_id, @@ -1018,7 +1018,8 @@ async def get_site_forecast_pv( fpi.power_w, fpi.irradiance_wm2, fpi.temp_c, - apa.code AS pv_array_code + apa.code AS pv_array_code, + apa.controllable FROM ems.forecast_pv_interval fpi JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id JOIN ems.asset_pv_array apa @@ -1028,20 +1029,21 @@ async def get_site_forecast_pv( AND fpr.status = 'ok' ORDER BY fpi.interval_start, fpr.pv_array_id, fpr.created_at DESC ) latest - ORDER BY pv_array_code, interval_start + ORDER BY controllable DESC, pv_array_code, interval_start """, site_id, d, ) + # pv_a = řiditelná pole (curtailment / Deye), pv_b = neřízená (GEN, …) — sloučí více orientací pv_a: list[dict[str, Any]] = [] pv_b: list[dict[str, Any]] = [] for r in rows: item = record_to_dict(r) - code = item.get("pv_array_code") - if code == "pv-a": + item.pop("controllable", None) + if r["controllable"]: pv_a.append(item) - elif code == "pv-b": + else: pv_b.append(item) return {"pv_a": pv_a, "pv_b": pv_b} diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index d82d7c0..0a185d2 100644 --- a/backend/services/planning_engine.py +++ b/backend/services/planning_engine.py @@ -1052,20 +1052,42 @@ async def _load_slots(site_id, from_dt, to_dt, db) -> list[PlanningSlot]: LEFT JOIN ems.vw_site_effective_price ep ON ep.site_id = $1 AND ep.interval_start = s.interval_start LEFT JOIN LATERAL ( - SELECT fpi.power_w FROM ems.forecast_pv_interval fpi - JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id - JOIN ems.asset_pv_array apa ON apa.id = fpi.pv_array_id AND apa.site_id = fpr.site_id - WHERE fpr.site_id = $1 AND apa.code = 'pv-a' - AND fpi.interval_start = s.interval_start AND fpr.status = 'ok' - ORDER BY fpr.created_at DESC LIMIT 1 + SELECT COALESCE(SUM(u.power_w), 0)::INT AS power_w + FROM ( + SELECT DISTINCT ON (apa.id) + fpi.power_w + FROM ems.asset_pv_array apa + JOIN ems.forecast_pv_run fpr + ON fpr.pv_array_id = apa.id + AND fpr.site_id = apa.site_id + AND fpr.status = 'ok' + JOIN ems.forecast_pv_interval fpi + ON fpi.run_id = fpr.id + AND fpi.pv_array_id = apa.id + AND fpi.interval_start = s.interval_start + WHERE apa.site_id = $1 + AND apa.controllable IS TRUE + ORDER BY apa.id, fpr.created_at DESC + ) u ) fpi_a ON true LEFT JOIN LATERAL ( - SELECT fpi.power_w FROM ems.forecast_pv_interval fpi - JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id - JOIN ems.asset_pv_array apa ON apa.id = fpi.pv_array_id AND apa.site_id = fpr.site_id - WHERE fpr.site_id = $1 AND apa.code = 'pv-b' - AND fpi.interval_start = s.interval_start AND fpr.status = 'ok' - ORDER BY fpr.created_at DESC LIMIT 1 + SELECT COALESCE(SUM(u.power_w), 0)::INT AS power_w + FROM ( + SELECT DISTINCT ON (apa.id) + fpi.power_w + FROM ems.asset_pv_array apa + JOIN ems.forecast_pv_run fpr + ON fpr.pv_array_id = apa.id + AND fpr.site_id = apa.site_id + AND fpr.status = 'ok' + JOIN ems.forecast_pv_interval fpi + ON fpi.run_id = fpr.id + AND fpi.pv_array_id = apa.id + AND fpi.interval_start = s.interval_start + WHERE apa.site_id = $1 + AND apa.controllable IS FALSE + ORDER BY apa.id, fpr.created_at DESC + ) u ) fpi_b ON true LEFT JOIN LATERAL ( SELECT t.status diff --git a/db/migration/V043__site_25a_fixed_buy_seed.sql b/db/migration/V043__site_25a_fixed_buy_seed.sql new file mode 100644 index 0000000..c7ee9f0 --- /dev/null +++ b/db/migration/V043__site_25a_fixed_buy_seed.sql @@ -0,0 +1,388 @@ +-- ============================================================= +-- V043__site_25a_fixed_buy_seed.sql +-- Sloupce pro fixní nákupní energii (NT + příplatek VT) a seed lokality site-25a. +-- +-- Jedna verzovaná migrace: čtyři FVE pole (různá orientace), žádný mezikrok pv-a/pv-b. +-- +-- Obnova / přepnutí checksum na DB, kde už běžela starší varianta V043 nebo V044: +-- DELETE FROM flyway_schema_history WHERE version IN ('043', '044'); +-- Potom: flyway migrate +-- (Sloupce buy_fixed_* zůstanou díky ADD COLUMN IF NOT EXISTS; DO blok smaže legacy pv-a/pv-b +-- a doplní pv-str-*/pv-mi-* pokud chybí.) +-- ============================================================= + +-- Fixní složka nákupu bez DPH (k distribuci / poplatkům / marži / DPH dle fn_effective_buy_price) +ALTER TABLE ems.site_market_config + ADD COLUMN IF NOT EXISTS buy_fixed_energy_nt_czk_kwh NUMERIC(10,6), + ADD COLUMN IF NOT EXISTS buy_fixed_vt_surcharge_czk_kwh NUMERIC(10,6) NOT NULL DEFAULT 0; + +COMMENT ON COLUMN ems.site_market_config.buy_fixed_energy_nt_czk_kwh IS +'Při purchase_pricing_mode = fixed: základní nákupní cena energie Kč/kWh bez DPH v NT hodinách. VT = tato hodnota + buy_fixed_vt_surcharge_czk_kwh podle HDO oken.'; + +COMMENT ON COLUMN ems.site_market_config.buy_fixed_vt_surcharge_czk_kwh IS +'Při purchase_pricing_mode = fixed: příplatek Kč/kWh bez DPH k NT ceně ve VT oknech dle hdo_code_id.'; + +-- ============================================================= +-- Seed lokality (idempotentní DO blok) +-- Viz docs/new-site-setup-template.md – ev-charger-1 pro planner/telemetrii. +-- FVE: čtyři záznamy asset_pv_array (forecast service běží per pole; planner sčítá controllable / !controllable). +-- ============================================================= + +DO $$ +DECLARE + v_site_code TEXT := 'BA81'; + + v_host_modbus TEXT := '109.164.83.155'; + v_port_modbus INT := 502; + v_host_loxone TEXT := '109.164.83.155'; + v_port_loxone INT := 8080; + + v_site_id INT; + v_ep_deye INT; + v_ep_ev INT; + v_ep_loxone INT; + v_inv_main INT; + v_inv_gen INT; + v_hdo_id INT; + v_ch_id INT; +BEGIN + SELECT hc.id INTO v_hdo_id + FROM ems.hdo_code hc + WHERE hc.distributor = 'EGD' AND hc.code = 'custom_fve_home01' + ORDER BY hc.valid_from DESC NULLS LAST + LIMIT 1; + + INSERT INTO ems.site (code, name, timezone, latitude, longitude, active, notes) + VALUES ( + v_site_code, + 'Lokalita 25A / 17 kW příkon', + 'Europe/Prague', + 49.24368977130069, + 17.425553019721196, + true, + 'Připojení 3×25 A → import max 17 kW, export max 16 kW. ' + 'Při omezení exportu do DS nastavit v Deye SmartLoad: „MI export to Grid cutoff“ = enable; ' + 'po uvolnění exportu znovu disable. Veřejná IP tunelovaná z EMS serveru.' + ) + ON CONFLICT (code) DO UPDATE SET + name = EXCLUDED.name, + timezone = EXCLUDED.timezone, + latitude = EXCLUDED.latitude, + longitude = EXCLUDED.longitude, + active = EXCLUDED.active, + notes = EXCLUDED.notes + RETURNING id INTO v_site_id; + + SELECT se.id INTO v_ep_deye + FROM ems.site_endpoint se + WHERE se.site_id = v_site_id + AND se.endpoint_type = 'modbus_tcp' + AND se.notes ILIKE '%Deye%' + ORDER BY se.id + LIMIT 1; + + IF v_ep_deye IS NULL THEN + INSERT INTO ems.site_endpoint ( + site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes + ) + VALUES ( + v_site_id, 'modbus_tcp', v_host_modbus, v_port_modbus, 'modbus_tcp', 1, true, + 'Deye 12kW LV – Modbus TCP (Waveshare).' + ) + RETURNING id INTO v_ep_deye; + END IF; + + SELECT se.id INTO v_ep_ev + FROM ems.site_endpoint se + WHERE se.site_id = v_site_id + AND se.endpoint_type = 'modbus_tcp' + AND se.notes ILIKE '%Teltonika%' + ORDER BY se.id + LIMIT 1; + + IF v_ep_ev IS NULL THEN + INSERT INTO ems.site_endpoint ( + site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes + ) + VALUES ( + v_site_id, 'modbus_tcp', v_host_modbus, v_port_modbus, 'modbus_tcp', 2, true, + 'Teltonika TeltoCharge 22kW – stejná IP jako Deye, unit_id 2 (upřesni dle zapojení).' + ) + RETURNING id INTO v_ep_ev; + END IF; + + SELECT se.id INTO v_ep_loxone + FROM ems.site_endpoint se + WHERE se.site_id = v_site_id + AND se.endpoint_type = 'loxone_http' + ORDER BY se.id + LIMIT 1; + + IF v_ep_loxone IS NULL THEN + INSERT INTO ems.site_endpoint ( + site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes + ) + VALUES ( + v_site_id, 'loxone_http', v_host_loxone, v_port_loxone, 'http', NULL, true, + 'Loxone Miniserver (HTTP Virtual Inputs).' + ) + RETURNING id INTO v_ep_loxone; + END IF; + + INSERT INTO ems.site_grid_connection ( + site_id, max_import_power_w, max_export_power_w, no_export, reserved_capacity_w, notes + ) + VALUES ( + v_site_id, 17000, 16000, false, 0, + 'Max 25 A přívod → cca 17 kW import; přetok / export povolen 16 kW.' + ) + ON CONFLICT (site_id) DO UPDATE SET + max_import_power_w = EXCLUDED.max_import_power_w, + max_export_power_w = EXCLUDED.max_export_power_w, + no_export = EXCLUDED.no_export, + reserved_capacity_w = EXCLUDED.reserved_capacity_w, + notes = EXCLUDED.notes; + + IF NOT EXISTS ( + SELECT 1 FROM ems.site_market_config smc + WHERE smc.site_id = v_site_id AND smc.valid_to IS NULL + ) THEN + INSERT INTO ems.site_market_config ( + site_id, + purchase_pricing_mode, sale_pricing_mode, + buy_margin_fixed_czk, buy_margin_percent, + sell_margin_fixed_czk, sell_margin_percent, + currency, valid_from, valid_to, notes, + tariff_id, hdo_code_id, system_services_czk_kwh, ote_fee_czk_kwh, + buy_fixed_energy_nt_czk_kwh, buy_fixed_vt_surcharge_czk_kwh + ) + VALUES ( + v_site_id, + 'fixed', 'spot', + 0, 0, + -0.020, 0, + 'CZK', now(), NULL, + 'Nákup fixní 3,67 Kč/kWh bez DPH (NT) + 0,52 Kč/kWh bez DPH ve VT (okna dle HDO jako home-01). ' + 'Prodej na spotu jako home-01. Distribuce v efektivní ceně 0 (tariff_id NULL) – energie jen fix + DPH dle vat_rate výchozí.', + NULL, + v_hdo_id, + 0, + 0, + 3.67, + 0.52 + ); + END IF; + + INSERT INTO ems.site_operating_mode (site_id, mode_code, activated_by, notes) + VALUES ( + v_site_id, + 'MANUAL', + 'migration:V043_site_25a', + 'Start MANUAL; po ověření přepnout na AUTO.' + ) + ON CONFLICT (site_id) DO NOTHING; + + SELECT ai.id INTO v_inv_main + FROM ems.asset_inverter ai + WHERE ai.site_id = v_site_id AND ai.code = 'deye-main' + LIMIT 1; + + IF v_inv_main IS NULL THEN + INSERT INTO ems.asset_inverter ( + site_id, code, manufacturer, model, endpoint_id, + max_charge_power_w, max_discharge_power_w, max_export_power_w, + max_ac_output_w, max_dc_input_w, max_battery_charge_w, max_battery_discharge_w, + gen_port_max_power_w, + controllable, active, notes + ) + VALUES ( + v_site_id, + 'deye-main', + 'Deye', + NULL, + v_ep_deye, + 6250, 6250, 12000, + 12000, 24000, 6250, 6250, + 5000, + true, true, + '12kW LV hybrid. Baterie limit 0,5C ≈ 6,25 kW (280 A teoreticky vyšší – plánování dle 6,25 kW). ' + 'GEN port max ~5 kW součet MI.' + ) + RETURNING id INTO v_inv_main; + END IF; + + SELECT ai.id INTO v_inv_gen + FROM ems.asset_inverter ai + WHERE ai.site_id = v_site_id AND ai.code = 'ongrid-gen' + LIMIT 1; + + IF v_inv_gen IS NULL THEN + INSERT INTO ems.asset_inverter ( + site_id, code, manufacturer, model, endpoint_id, + max_export_power_w, controllable, active, notes + ) + VALUES ( + v_site_id, + 'ongrid-gen', + NULL, NULL, NULL, + 5000, false, true, + 'Mikroinvertory na GEN portu (2 skupiny panelů), EMS necurtailuje.' + ) + RETURNING id INTO v_inv_gen; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_battery ab + WHERE ab.site_id = v_site_id AND ab.code = 'bat-main' + ) THEN + INSERT INTO ems.asset_battery ( + site_id, inverter_id, code, + usable_capacity_wh, min_soc_percent, reserve_soc_percent, max_soc_percent, + charge_efficiency, discharge_efficiency, degradation_cost_czk_kwh, + max_charge_c_rate, max_discharge_c_rate, bms_max_charge_w, bms_max_discharge_w + ) + VALUES ( + v_site_id, v_inv_main, 'bat-main', + 12500, + 10, 15, 95, + 0.95, 0.95, + 0.50, + 0.5, 0.5, + 6250, 6250 + ); + END IF; + + -- Odstranění starého agregovaného seedu (pv-a / pv-b), pokud na DB zůstal z dřívější verze. + DELETE FROM ems.forecast_accuracy fa + WHERE fa.pv_array_id IN ( + SELECT id FROM ems.asset_pv_array + WHERE site_id = v_site_id AND code IN ('pv-a', 'pv-b') + ); + + DELETE FROM ems.forecast_pv_interval fpi + USING ems.asset_pv_array apa + WHERE apa.site_id = v_site_id + AND apa.code IN ('pv-a', 'pv-b') + AND fpi.pv_array_id = apa.id; + + DELETE FROM ems.forecast_pv_run fpr + WHERE fpr.site_id = v_site_id + AND fpr.pv_array_id IN ( + SELECT id FROM ems.asset_pv_array + WHERE site_id = v_site_id AND code IN ('pv-a', 'pv-b') + ); + + DELETE FROM ems.asset_pv_array + WHERE site_id = v_site_id AND code IN ('pv-a', 'pv-b'); + + -- String 1: 12×620 Wp @110° / 45° (Deye, řiditelné) + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_pv_array ap + WHERE ap.site_id = v_site_id AND ap.code = 'pv-str-1' + ) THEN + INSERT INTO ems.asset_pv_array ( + site_id, inverter_id, code, name, + nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, + controllable, telemetry_source, notes + ) + VALUES ( + v_site_id, v_inv_main, 'pv-str-1', 'String 1 – 12×620 Wp', + 7440, 110, 45, 12, 1.0, true, 'pv_strings', + 'Hlavní telemetrie stringů Deye (pv1+pv2); druhý string má telemetry_source NULL.' + ); + END IF; + + -- String 2: 8×620 Wp @200° / 10° (Deye, řiditelné) + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_pv_array ap + WHERE ap.site_id = v_site_id AND ap.code = 'pv-str-2' + ) THEN + INSERT INTO ems.asset_pv_array ( + site_id, inverter_id, code, name, + nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, + controllable, telemetry_source, notes + ) + VALUES ( + v_site_id, v_inv_main, 'pv-str-2', 'String 2 – 8×620 Wp', + 4960, 200, 10, 8, 1.0, true, NULL, + 'Vlastní predikce orientace; telemetrie sdílená se stringem 1.' + ); + END IF; + + -- MI 5×620 Wp @200° / 45° (GEN, neriditelné) + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_pv_array ap + WHERE ap.site_id = v_site_id AND ap.code = 'pv-mi-1' + ) THEN + INSERT INTO ems.asset_pv_array ( + site_id, inverter_id, code, name, + nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, + controllable, telemetry_source, notes + ) + VALUES ( + v_site_id, v_inv_gen, 'pv-mi-1', 'Mikroinvertory 5×620 Wp', + 3100, 200, 45, 5, 1.0, false, 'gen_port', + 'Souhrnná telemetrie GEN portu; druhá MI skupina má telemetry NULL.' + ); + END IF; + + -- MI 3×620 Wp @110° / 10° (GEN, neriditelné) + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_pv_array ap + WHERE ap.site_id = v_site_id AND ap.code = 'pv-mi-2' + ) THEN + INSERT INTO ems.asset_pv_array ( + site_id, inverter_id, code, name, + nominal_power_wp, azimuth_deg, tilt_deg, module_count, shading_factor, + controllable, telemetry_source, notes + ) + VALUES ( + v_site_id, v_inv_gen, 'pv-mi-2', 'Mikroinvertory 3×620 Wp', + 1860, 110, 10, 3, 1.0, false, NULL, + 'Predikce samostatně; gen_port u pv-mi-1.' + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_ev_charger c + WHERE c.site_id = v_site_id AND c.code = 'ev-charger-1' + ) THEN + INSERT INTO ems.asset_ev_charger ( + site_id, code, manufacturer, model, endpoint_id, + max_power_w, min_power_w, phases, connector_count, schedulable, notes + ) + VALUES ( + v_site_id, 'ev-charger-1', 'Teltonika', 'TeltoCharge 22kW', + v_ep_ev, + 22000, 1380, 3, 1, true, + 'Jedna nabíječka; kód ev-charger-1 kvůli planneru / telemetrii.' + ) + RETURNING id INTO v_ch_id; + ELSE + SELECT id INTO v_ch_id FROM ems.asset_ev_charger + WHERE site_id = v_site_id AND code = 'ev-charger-1' + LIMIT 1; + END IF; + + INSERT INTO ems.asset_vehicle ( + site_id, code, name, make, model, + battery_capacity_kwh, max_charge_power_w, default_charger_id, api_type, + default_target_soc_pct, default_deadline_hour, active + ) + VALUES ( + v_site_id, + 'ev-default', + 'EV (výchozí)', + NULL, NULL, + 60.0, + 11000, + v_ch_id, + 'none', + 80, + 7, + true + ) + ON CONFLICT (site_id, code) DO NOTHING; + +END; +$$; diff --git a/db/routines/R__fn_effective_price.sql b/db/routines/R__fn_effective_price.sql index 334d96a..752a9d4 100644 --- a/db/routines/R__fn_effective_price.sql +++ b/db/routines/R__fn_effective_price.sql @@ -14,6 +14,7 @@ STABLE AS $$ DECLARE v_spot_price NUMERIC; + v_energy_czk NUMERIC; v_dist_rate NUMERIC; v_system_services NUMERIC; v_ote_fee NUMERIC; @@ -27,8 +28,14 @@ DECLARE v_hdo_code_id INT; v_tariff_id INT; v_rate_type TEXT; + v_purchase_mode TEXT; + v_fixed_nt NUMERIC; + v_fixed_vt_sur NUMERIC; BEGIN SELECT + smc.purchase_pricing_mode, + smc.buy_fixed_energy_nt_czk_kwh, + smc.buy_fixed_vt_surcharge_czk_kwh, smc.buy_margin_fixed_czk, smc.buy_margin_percent, smc.system_services_czk_kwh, @@ -37,6 +44,9 @@ BEGIN smc.tariff_id, dt.vat_rate INTO + v_purchase_mode, + v_fixed_nt, + v_fixed_vt_sur, v_buy_margin_fixed, v_buy_margin_pct, v_system_services, @@ -62,10 +72,6 @@ BEGIN AND interval_start = p_interval_start LIMIT 1; - IF v_spot_price IS NULL THEN - RETURN NULL; - END IF; - v_local_time := (p_interval_start AT TIME ZONE 'Europe/Prague')::TIME; v_dow := EXTRACT(DOW FROM p_interval_start AT TIME ZONE 'Europe/Prague'); -- 0=neděle, 6=sobota @@ -106,11 +112,23 @@ BEGIN v_ote_fee := COALESCE(v_ote_fee, 0); v_buy_margin_fixed := COALESCE(v_buy_margin_fixed, 0); v_buy_margin_pct := COALESCE(v_buy_margin_pct, 0); - v_buy_margin := v_buy_margin_fixed + (v_spot_price * v_buy_margin_pct / 100.0); v_vat_rate := COALESCE(v_vat_rate, 0.21); + v_fixed_vt_sur := COALESCE(v_fixed_vt_sur, 0); + + IF upper(trim(COALESCE(v_purchase_mode, ''))) = 'FIXED' + AND v_fixed_nt IS NOT NULL THEN + v_energy_czk := v_fixed_nt + + CASE WHEN v_is_vt THEN v_fixed_vt_sur ELSE 0 END; + ELSIF v_spot_price IS NULL THEN + RETURN NULL; + ELSE + v_energy_czk := v_spot_price; + END IF; + + v_buy_margin := v_buy_margin_fixed + (v_energy_czk * v_buy_margin_pct / 100.0); RETURN ROUND( - (v_spot_price + v_dist_rate + v_system_services + v_ote_fee + v_buy_margin) + (v_energy_czk + v_dist_rate + v_system_services + v_ote_fee + v_buy_margin) * (1 + v_vat_rate), 6 ); @@ -119,8 +137,9 @@ $$; COMMENT ON FUNCTION ems.fn_effective_buy_price(INT, TIMESTAMPTZ) IS 'Efektivní nákupní cena elektřiny Kč/kWh včetně DPH. -Složky: spot OTE + distribuce NT/VT (dle HDO) + systémové služby + OTE poplatek + marže (fix + % ze spotu). -DPH aplikováno na celou částku. Distribuce závisí na HDO kódu site.'; +Režim spot: energie = OTE buy_raw + distribuce NT/VT (dle HDO) + systémové služby + OTE poplatek + marže (fix + % z energie). +Režim fixed: energie = buy_fixed_energy_nt_czk_kwh (+ buy_fixed_vt_surcharge_czk_kwh ve VT oknech dle HDO), pak stejné příplatky a DPH. +DPH aplikováno na celou částku.'; -- ------------------------------------------------------------ diff --git a/docs/04-modules/planning.md b/docs/04-modules/planning.md index 1a01287..a0bfc3d 100644 --- a/docs/04-modules/planning.md +++ b/docs/04-modules/planning.md @@ -27,6 +27,7 @@ - **Záporná nákupní cena:** - horní mez `grid_import` zahrnuje `load_baseline_w` + nabíjení/EV/TČ (bez nekonečného importu). - **Uložené vstupy plánu** (`planning_interval`): `load_baseline_w`, `pv_*_forecast_raw_w`, `pv_*_forecast_solver_w` pro UI a audit. +- **Více FVE polí s různou orientací:** `planning_engine._load_slots` sčítá predikovaný výkon za 15min přes **všechna** `asset_pv_array` dané lokality — `pv_a_forecast_w` = součet řádků s `controllable = true`, `pv_b_forecast_w` = součet s `controllable = false`. Pro každé pole a slot se bere **nejnovější** `forecast_pv_run` (`ORDER BY created_at DESC`, `DISTINCT ON (pv_array_id)`). Curtailment v LP zůstává **jedno** agregované `pv_a` (součet řiditelných polí); per-string curtailment by vyžadovalo rozšíření modelu. Solver optimalizuje celý horizont (typicky 36h) najednou, čímž přirozeně zvládá: - pohled dopředu (ráno ví že přes poledne bude záporná cena → prodává z baterie) diff --git a/docs/new-site-setup-template.md b/docs/new-site-setup-template.md index 692bd11..db01eee 100644 --- a/docs/new-site-setup-template.md +++ b/docs/new-site-setup-template.md @@ -38,7 +38,7 @@ Použij jako checklist při přidávání dalšího objektu do EMS. Odkazy: dato |------|---------|----------| | ☐ | `ems.asset_inverter` | Vazba na `endpoint_id` kde je potřeba; `controllable`, `active`; výkonové limity | | ☐ | `ems.asset_battery` | Vazba na střídač; SoC limity, účinnosti, degradace — **solver očekává baterii**, jinak denní/rolling plán padá | -| ☐ | `ems.asset_pv_array` | Min. jedno pole k FVE forecastu / solveru; `controllable` u pole A vs B dle [`CLAUDE.md`](../CLAUDE.md) | +| ☐ | `ems.asset_pv_array` | Jedno nebo více polí (různé orientace = vlastní forecast běh na `id`). Plánovač **sčítá** predikce: `pv_a` = všechna `controllable = true`, `pv_b` = všechna `false` (viz [`docs/04-modules/planning.md`](04-modules/planning.md)). Kódy `pv-a` / `pv-b` už nejsou nutné. | | ☐ | `ems.asset_ev_charger` | Volitelné; `endpoint_id`, výkony | | ☐ | `ems.asset_heat_pump` | Volitelné; TČ parametry, TUV | | ☐ | `ems.asset_vehicle` | Až **dva** záznamy na site (EV1/EV2 sloty ve solveru), pokud řešíš nabíjení |