Files
ems/docs/new-site-setup-template.md
Dusan Vojacek b8515f30df
Some checks failed
CI and deploy / migration-check (push) Failing after 9s
CI and deploy / deploy (push) Has been skipped
implmemtace cuttoff genportu
2026-04-20 10:41:10 +02:00

18 KiB
Raw Permalink Blame History

Š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, referenční seed db/migration/V003__seed_site_home01.sql, tarif/HDO u existující lokality 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). 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 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 01 („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 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.

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. Zelený bonus je na ems.asset_pv_array, ne v site_market_config.

-- =============================================================
-- 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 = falsenení 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.
  • 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