diff --git a/docs/new-site-setup-template.md b/docs/new-site-setup-template.md new file mode 100644 index 0000000..692bd11 --- /dev/null +++ b/docs/new-site-setup-template.md @@ -0,0 +1,378 @@ +# Šablona: založení nové lokality (`site_id`) + +Použij jako checklist při přidávání dalšího objektu do EMS. Odkazy: datový model [`03-data-model.md`](03-data-model.md), referenční seed [`db/migration/V003__seed_site_home01.sql`](../db/migration/V003__seed_site_home01.sql), tarif/HDO u existující lokality [`db/migration/V016__seed_distribution_home01.sql`](../db/migration/V016__seed_distribution_home01.sql). + +--- + +## 1. Identita a přístup + +| Krok | Tabulka / akce | Poznámka | +|------|------------------|----------| +| ☐ | `ems.site` | `code` (unikátní), `name`, `timezone` (IANA), volitelně `latitude` / `longitude` (forecast/počasí) | +| ☐ | `ems.site_operating_mode` | Jedna řádek na `site_id` (`mode_code` FK na `operating_mode_def`). Bez řádku control export varuje a plánovač bere režim jako prázdný → v solveru efektivně jako AUTO. **Doporučení:** po vytvoření site explicitně nastavit režim (`ems.fn_set_mode` nebo `POST /api/v1/sites/{id}/mode`). | +| ☐ | UI / API | `GET /api/v1/me/sites` vrací jen **`site.active = true`** — neaktivní lokalita se v comboboxu neobjeví. | + +--- + +## 2. Síť, trh, ekonomika + +| Krok | Tabulka | Poznámka | +|------|---------|----------| +| ☐ | `ems.site_grid_connection` | Jeden záznam na site: `max_import_power_w`, `max_export_power_w`, `no_export`, `reserved_capacity_w` | +| ☐ | `ems.site_market_config` | Marže, režimy cenění, platnost `valid_from` / `valid_to`; volitelně `tariff_id`, `hdo_code_id`, poplatky (viz V016 u home-01) | +| ☐ | `ems.distribution_tariff` (+ `distribution_tariff_rate`, `hdo_code`, `hdo_code_window`) | Jen pokud potřebuješ distribuční složku / HDO v efektivní ceně — jinak lze doplnit později | + +--- + +## 3. Endpointy (Modbus, HTTP, …) + +| Krok | Tabulka | Poznámka | +|------|---------|----------| +| ☐ | `ems.site_endpoint` | Pro každé zařízení: `endpoint_type` (`modbus_tcp`, `loxone_http`, …), `host`, `port`, `unit_id`, **`enabled`**. Nehotové zařízení: `enabled = false` nebo zatím bez řádku — telemetrie daného typu se nebude dotazovat. | + +--- + +## 4. Aktiva (minimálně podle toho, co má objekt mít) + +| Krok | Tabulka | Poznámka | +|------|---------|----------| +| ☐ | `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_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í | + +--- + +## 5. Provoz backendu (joby) + +Pro **`site.active = true`** scheduler zpracovává mimo jiné: telemetrii, denní plán, rolling replan, control export, audit, forecast refresh, baseline/statistiky. Konkrétní seznam: [`CLAUDE.md`](../CLAUDE.md) sekce periodické úlohy. + +--- + +## 6. Po nasazení (ověření) + +| Krok | Akce | +|------|------| +| ☐ | Telemetrie: řádky v `telemetry_*` hypertables, dashboard / `vw_latest_*` | +| ☐ | Ceny: `vw_site_effective_price` pro daný `site_id` | +| ☐ | Plán: `planning_run` + `planning_interval` po denním jobu nebo ručním spuštění | +| ☐ | Režim: `vw_operating_mode` / API režimu | +| ☐ | Modbus journal: při AUTO očekávej zápisy v `modbus_command` po exportu | + +--- + +## 7. Poznámky k migracím + +- Nová data pro novou lokalitu: **nový Flyway soubor** `Vxxx__seed_site_.sql` (neupravovat už aplikované `V00x__*.sql`). +- Repeatable SQL (`db/routines`, `db/views`) se nemění kvůli jedné nové site, pokud nepotřebuješ obecnou úpravu. + +--- + +## 8. SQL šablona (kopie do verzované Flyway migrace) + +Jeden `DO $$ … $$` blok: **`v_site_id`** (a další ID endpointů / invertoru) se naplní v `DECLARE`, dál se používají v insertech. + +### Idempotence (opakovaný běh migrace) + +| Objekt | Mechanismus | Poznámka | +|--------|-------------|----------| +| `ems.site` | `ON CONFLICT (code) DO UPDATE` | Unikátní je `code`; opakovaný běh **aktualizuje** název, TZ, souřadnice, `active`, `notes` z šablony. | +| `ems.site_grid_connection` | `ON CONFLICT (site_id) DO UPDATE` | Unikátní je `site_id`; limity se při re-run **přepíší** hodnotami z migrace. | +| `ems.site_operating_mode` | `ON CONFLICT (site_id) DO NOTHING` | Druhý běh **nepřepíše** režim (např. už máš `AUTO`). | +| `ems.site_market_config` | `IF NOT EXISTS … valid_to IS NULL` | Tabulka nemá jednoznačný unikátní klíč pro „aktuální“ řádek; vloží se jen pokud pro site **ještě není** otevřená konfigurace. | +| `ems.site_endpoint` | výběr + `IF v_… IS NULL THEN INSERT` | Na `(site_id, …)` **není** UNIQUE constraint; duplicitu řešíme detekcí existujícího řádku (Deye = `modbus_tcp` + `notes ILIKE '%Deye%'`, Loxone = první `loxone_http`). | +| `ems.asset_inverter` / `battery` / `pv_array` / `ev_charger` / `heat_pump` | `IF NOT EXISTS (site_id + code) THEN INSERT` | V schématu **není** `UNIQUE (site_id, code)` u těchto tabulek (kromě vozidel). | +| `ems.asset_vehicle` | `ON CONFLICT (site_id, code) DO NOTHING` | Unikátní je `(site_id, code)` (V006). | + +Režim **`MANUAL`** = bez EMS zápisů na hardware; po ověření přepni na `AUTO` přes API / `ems.fn_set_mode`. **`tariff_id` / `hdo_code_id`**: `NULL` v šabloně nebo doplníš později jako ve [`V016__seed_distribution_home01.sql`](../db/migration/V016__seed_distribution_home01.sql). **Zelený bonus** je na `ems.asset_pv_array`, ne v `site_market_config`. + +```sql +-- ============================================================= +-- V0xx__seed_site_home02.sql ← přejmenuj číslo + název souboru +-- Idempotentní seed nové lokality (bez duplicit při opakovaném běhu). +-- ============================================================= + +DO $$ +DECLARE + v_site_code TEXT := 'home-02'; + + v_host_deye TEXT := '192.168.1.10'; + v_host_loxone TEXT := '192.168.1.20'; + + v_site_id INT; + v_ep_deye INT; + v_ep_loxone INT; + v_inv_main INT; +BEGIN + -- --- Site ---------------------------------------------------------------- + INSERT INTO ems.site (code, name, timezone, latitude, longitude, active, notes) + VALUES ( + v_site_code, + 'Název objektu', + 'Europe/Prague', + 49.200000, + 17.400000, + true, + 'TODO: poznámka k instalaci.' + ) + 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; + + -- --- Endpoint: Deye (modbus) -------------------------------------------- + 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_deye, 502, 'modbus_tcp', 1, true, + 'Deye hlavní střídač – Modbus TCP (marker pro tento seed).' + ) + RETURNING id INTO v_ep_deye; + END IF; + + -- --- Endpoint: Loxone --------------------------------------------------- + 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, 80, 'http', NULL, true, + 'Loxone Miniserver.' + ) + RETURNING id INTO v_ep_loxone; + END IF; + + -- --- Grid ---------------------------------------------------------------- + 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, 22000, 20000, false, 0, + 'Limity dle jističe / připojení – upřesni.' + ) + 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; + + -- --- Market config (jen pokud nemáš otevřený řádek valid_to NULL) ------- + 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 + ) + VALUES ( + v_site_id, + 'spot', 'spot', + 0.050, 0, + -0.020, 0, + 'CZK', now(), NULL, 'Výchozí marže – upřesni ze smlouvy.', + NULL, NULL, 0, 0 + ); + END IF; + + -- --- Operating mode (nešťouchej uživatelem změněný režim) --------------- + INSERT INTO ems.site_operating_mode (site_id, mode_code, activated_by, notes) + VALUES ( + v_site_id, + 'MANUAL', + 'migration:V0xx_seed_site', + 'Po spuštění ověř Modbus/Loxone, pak AUTO.' + ) + ON CONFLICT (site_id) DO NOTHING; + + -- --- Hlavní střídač ------------------------------------------------------ + 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', + 'SUN-20K-SG01LP1-EU', + v_ep_deye, + 18000, 18000, 18000, + 22000, 40000, 18000, 18000, + NULL, + true, true, + 'Hlavní hybridní střídač.' + ) + RETURNING id INTO v_inv_main; + END IF; + + -- --- Baterie ------------------------------------------------------------- + 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', + 64000, + 10, 20, 95, + 0.95, 0.95, + 0.50, + NULL, NULL, NULL, NULL + ); + END IF; + + -- --- FVE pole A ---------------------------------------------------------- + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_pv_array ap + WHERE ap.site_id = v_site_id AND ap.code = 'pv-a' + ) 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, + green_bonus_czk_kwh, green_bonus_valid_from, green_bonus_valid_to, green_bonus_meter_code + ) + VALUES ( + v_site_id, v_inv_main, 'pv-a', 'FVE pole A', + 10000, + 180, 25, NULL, 1.0, + true, + 'pv_strings', + 'Hlavní stringy na MPPT.', + NULL, NULL, NULL, NULL + ); + END IF; + + -- Volitelné: druhý invertor (ongrid) — odkomentuj celý IF NOT EXISTS blok + /* + IF NOT EXISTS ( + SELECT 1 FROM ems.asset_inverter ai + WHERE ai.site_id = v_site_id AND ai.code = 'ongrid-gen' + ) 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, + 10000, false, true, + 'Ongen na GEN – EMS necurtailuje.' + ); + END IF; + */ + + -- Volitelné: EV / TČ / vozidlo — nejdřív endpoint (get-or-insert jako výše), pak: + /* + 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', NULL, + 22000, 1380, 3, 1, true, + 'WB #1 – doplň endpoint_id UPDATEm nebo vlož ep před tím.' + ); + 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 + ) + SELECT + v_site_id, 'car-1', 'Auto 1', 'Make', 'Model', + 60.0, 11000, c.id, 'none', 80, 7, true + FROM ems.asset_ev_charger c + WHERE c.site_id = v_site_id AND c.code = 'ev-charger-1' + ON CONFLICT (site_id, code) DO NOTHING; + */ + +END; +$$; +``` + +--- + +# Režim „nachystat, zatím neřídit“ a „jen číst data“ + +## `site.active = false` — **není** režim „jen číst“ + +Sloupec `ems.site.active` má význam: *lokalita se přeskočí při plánování a sběru dat* (komentář ve schématu). V backendu se na `active = true` váže mimo jiné **telemetrická smyčka** — při `active = false` **EMS typicky nebude číst Modbus** pro tuto lokalitu. Neobjeví se ani v `GET /api/v1/me/sites`. + +**Shrnutí:** `active = false` = lokalita mimo provoz (žádný sběr, žádné joby pro ni). Nevhodné pro fázi „už sbíráme data, ale neřídíme“. + +## `MANUAL` — EMS neexportuje setpointy (vhodné pro „neřídit z EMS“) + +V `control_exporter.export_setpoints()` při `mode_code = 'MANUAL'` proběhne **okamžitý návrat bez zápisů** na střídač, EV, TČ a bez Loxone setpointů (log: `MANUAL, skip writes`). + +**Doporučená kombinace pro přípravu + čtení dat:** + +1. `site.active = true` — běží telemetrie a ostatní joby (včetně plánování, pokud je DB konfigurace kompletní). +2. `site_operating_mode.mode_code = 'MANUAL'` — **EMS neposílá řídicí výstupy**. + +**Upozornění:** + +- Přepnutí do `MANUAL` přes API volá `fn_set_mode` a může **poslat hodnotu režimu do Loxone** (`loxone_mode_value` pro MANUAL = 0). Loxone pak pracuje podle vlastní šablony pro manuální/servisní režim — ověř v [`docs/loxone-integration.md`](loxone-integration.md). +- Dokumentace režimů říká, že v MANUAL „solver neběží“; **v aktuální implementaci** `planning_engine` i nadále spouští LP, pokud není režim ošetřen jinak (MANUAL nespadá do větví extra constraintů jako SELF_SUSTAIN). Plán se tedy může počítat a ukládat, ale **do zařízení se neaplikuje**. Pro čistě přípravnou fázi to obvykle stačí; pokud chceš šetřit CPU, řeší se to spíš budoucí úpravou jobů. + +## Další páky (volitelně) + +- **`site_endpoint.enabled = false`** nebo chybějící endpoint — žádný polling daného zařízení. +- **`asset_inverter.active = false`** — daný střídač se v telemetrii přeskočí (viz dotaz v `telemetry_collector`). +- **`asset_inverter.controllable = false`** — solver/logika „pole B“; zápisů Deye se stejně netýká v režimu MANUAL celkově. + +--- + +## Rychlá odpověď na časté otázky + +| Cíl | Doporučení | +|-----|------------| +| Jen sbírat telemetrii, EMS neřídit hardware | `active = true`, režim **`MANUAL`** | +| Lokalitu dočasně vypnout úplně (žádné joby, žádná telemetrie) | **`active = false`** | +| Plná automatizace | `active = true`, režim **`AUTO`** po ověření Modbus/Loxone |