385 lines
18 KiB
Markdown
385 lines
18 KiB
Markdown
# Š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.
|
||
|
||
---
|
||
|
||
## 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 |
|