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

385 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Š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 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`](../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`](../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 |