# Š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` | 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í | ### Poznámka: BLOCK_EXPORT a instalace s mikroinvertory na GEN portu (BA81 typ) Pokud má lokalita **mikroinvertory / AC coupling na GEN portu** a potřebuješ při **záporné výkupní ceně** (BLOCK_EXPORT) **tvrdě zakázat export**, nestačí jen `reg 145 = 0` (solar sell) – ten se týká primárně řiditelného PV za Deye. - Zapni feature flag na `ems.asset_inverter` (řádek `deye-main`): **`deye_gen_microinverter_cutoff_enabled = true`**.\n+- EMS pak při `effective_sell_price < 0` přepíná **Deye reg 179 bits 0–1** („MI export to Grid cutoff“) masked RMW.\n+- Detail registrů: `docs/04-modules/modbus-registers.md` (reg 145 a 179) a `docs/04-modules/operating-modes.md` (BLOCK_EXPORT). --- ## 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 |