# Š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_<kód>.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.

### BESS bez FVE (příklad v repu)

Lokalita **`hulin-bess`** ([`db/migration/V080__seed_site_hulin_bess.sql`](../db/migration/V080__seed_site_hulin_bess.sql)): jen `site`, grid, market, `deye-main`, `bat-main`; **bez** `asset_pv_array`, EV, TČ. `site_grid_connection.block_export_on_negative_sell = true`. Plánovač a forecast PV fungují s nulovou FVE; baseline bez `consumption_baseline_stats` používá default **500 W** ve `fn_load_planning_slots_full` (po telemetrii přepočítat `fn_update_baseline_stats` / `fn_rebuild_consumption_baseline_stats`).

---

## 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 |
