From cf663ae41774e404754783b77e6e6a0d6f5f25be Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Thu, 11 Jun 2026 22:37:57 +0200 Subject: [PATCH] =?UTF-8?q?Docs:=20pool-shelly=20=E2=80=94=20architektura,?= =?UTF-8?q?=20=C5=A1ablony=20seed=C5=AF,=20n=C3=A1vrh=20solver=20integrace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - šablona insertů endpoint/asset/signal_route (placeholdery IP, výkon, runtime) - tok ovládání přes fn_signal_enqueue_bool a signal_service - návrh pool[t] binárky analogicky hp[t] s denním runtime constraintem - checklist oživení, otevřené otázky (sezónnost, bazál, UI) Co-Authored-By: Claude Fable 5 --- docs/04-modules/pool-shelly.md | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/04-modules/pool-shelly.md diff --git a/docs/04-modules/pool-shelly.md b/docs/04-modules/pool-shelly.md new file mode 100644 index 0000000..1764b8f --- /dev/null +++ b/docs/04-modules/pool-shelly.md @@ -0,0 +1,112 @@ +# Bazénové čerpadlo přes Shelly relé + +**Stav:** telemetrie + ovládací infrastruktura **implementováno** (V085); integrace do LP solveru **📋 návrh / follow-up** (viz `planning-neg-sell-strategy.md`, sekce bazén a UI workshop). + +Cíl: (a) vlastní historie spotřeby čerpadla, (b) ovládání on/off, (c) plánovač jako odložitelná zátěž — „denní povinné hodiny filtrace, ideálně v levných / přebytkových slotech". + +--- + +## 1. Architektura + +``` +Shelly relé (Gen2 RPC, HTTP) + ▲ poll 60 s ▲ Switch.Set + readback verify + │ Switch.GetStatus │ +telemetry_collector.poll_pool_pumps signal_service (15 s worker) + │ ▲ + ▼ │ signal_outbound_journal (queued) +ems.telemetry_pool_pump ems.fn_signal_enqueue_bool(site, 'POOL_PUMP_ON', bool) + (hypertable, 1 min) ▲ + │ zatím: operátor / cron; follow-up: plánovač +``` + +- **Jen Gen2 RPC** (`/rpc/Switch.GetStatus`, `/rpc/Switch.Set`) — Plus/Pro řada. **Gen1 REST (`/relay/0?turn=on`) záměrně nepodporujeme**; parser odmítne Gen1 odpověď (`ison`) chybou, aby se chybná konfigurace neprojevila tichým výpadkem dat. +- **Historie**: Shelly drží jen okamžitý stav a kumulativní čítač `aenergy.total` (Wh). Historii si stavíme sami 1min pollingem jako u všeho ostatního (Deye, EV, TČ). +- **Ovládání**: žádný zásah do control exporteru — používá se existující signal infrastruktura (`signal_def` / `signal_route` / `signal_outbound_journal`, `services/signal_service.py`) s journalem, retry a readback verify. + +## 2. DB objekty (V085 + repeatables) + +| Objekt | Soubor | Popis | +|--------|--------|-------| +| `ems.asset_pool_pump` | `db/migration/V085__pool_shelly.sql` | Aktivum: `endpoint_id` → `site_endpoint` (typ `http_api` / `shelly_http`), `shelly_switch_id` (Gen2 Switch id, typ. 0), `rated_power_w` (konstantní příkon), `min_run_min`, `daily_runtime_min`, `schedulable`. | +| `ems.telemetry_pool_pump` | tamtéž | Hypertable 1min: `is_on`, `power_w` (apower), `energy_wh_total` (aenergy.total, Wh). PK `(pump_id, measured_at)`. | +| `signal_def` `POOL_PUMP_ON` | tamtéž | Bool signál — požadovaný stav relé. | +| `ems.fn_telemetry_pool_pump_sample` | `db/routines/R__092_…` | Insert vzorku (on conflict do nothing). | +| `ems.vw_asset_pool_pump_http_poll` | `db/views/R__093_…` | Čerpadla s aktivním HTTP endpointem pro collector. | +| `ems.fn_signal_enqueue_bool` | `db/routines/R__094_…` | SQL-first zařazení bool signálu do odchozí fronty (všechny aktivní routy site+kód); aplikuje `transform_json.map_bool` per route stejně jako backend. | + +**Sezónnost:** `daily_runtime_min` je **aktuální sezónní hodnota** (léto typicky 240–480 min, zima méně / 0 = filtrace vypnutá). Mění ji provozovatel ručně; plnohodnotný sezónní profil (tabulka měsíc → minuty, případně podle teploty vody) je follow-up — viz §6. + +## 3. Telemetrie + +- `telemetry_collector.poll_pool_pumps(site_id, db)` — součást 60s smyčky (`run_telemetry_loop`), čte `vw_asset_pool_pump_http_poll`, volá `services/shelly_client.get_switch_status`, zapisuje přes `fn_telemetry_pool_pump_sample`. +- Při výpadku čtení se **nic nezapisuje** (žádná fabrikovaná nula — stejný princip jako EV nabíječky). +- `energy_wh_total` je čítač — energie za interval = kladná diference po sobě jdoucích vzorků (po výpadku napájení Shelly může čítač začít znovu; záporné diference zahazovat). +- Follow-up: zařadit `power_w` čerpadla mezi řízené zátěže při výpočtu bazálu (`fn_update_baseline_stats`) a do `vw_latest_telemetry`, jakmile poteče reálná telemetrie. + +## 4. Ovládání on/off (signál `POOL_PUMP_ON`) + +Route je per site a obsahuje IP — **neseeduje se migrací**, zakládá se provozně podle šablony (placeholdery `<...>`): + +```sql +-- 1) endpoint Shelly relé +insert into ems.site_endpoint (site_id, endpoint_type, host, port, protocol, enabled, notes) +values (, 'http_api', '', 80, 'http', true, 'Shelly relé bazénového čerpadla') +returning id; -- → + +-- 2) aktivum +insert into ems.asset_pool_pump (site_id, code, endpoint_id, rated_power_w, min_run_min, daily_runtime_min) +values (, 'pool-pump-01', , , 15, ); + +-- 3) route signálu na Shelly (Gen2 RPC; bool v query musí být doslova true/false → map_bool) +insert into ems.signal_route ( + site_id, destination_type, endpoint_id, signal_code, destination_key, + route_config_json, transform_json, verify_readback, verify_config_json +) +values ( + , 'http_rest', , 'POOL_PUMP_ON', 'switch0', + '{"method": "GET", "path_template": "/rpc/Switch.Set?id=0&on={value}"}', + '{"map_bool": {"true": "true", "false": "false"}}', + true, + '{"read_path": "/rpc/Switch.GetStatus?id=0", "json_path": "$.output"}' +); +``` + +Tok: `select ems.fn_signal_enqueue_bool(, 'POOL_PUMP_ON', true);` → `signal_outbound_journal` (`queued`) → worker `signal_outbound_send` (15 s) pošle `GET /rpc/Switch.Set?id=0&on=true` → `sent` → worker `signal_outbound_verify` přečte `Switch.GetStatus` a porovná `$.output` → `verified` (retry/backoff a `abandoned` po 12 pokusech dle `signal_service`). + +**Kdo signál nastavuje (fáze):** + +1. **Teď:** operátor ručně (`fn_signal_enqueue_bool`) nebo jednoduchý cron (např. pg_cron / APScheduler tick: zapnout v naplánovaných hodinách, vypnout mimo ně). +2. **Follow-up (plná integrace):** plánovač zapíše běh bazénu do `planning_interval` (nový sloupec, např. `pool_pump_on boolean`); tick na hranici 15min slotu (analogický control exporteru, ale přes signály) porovná plán s `signal_state` a zavolá `fn_signal_enqueue_bool` jen při změně (idempotenci řeší `signal_state` + `_should_skip_enqueue` logika). + +## 5. Integrace do solveru (📋 návrh — analogie `hp[t]`) + +V `solver_v2.py` je TČ spojitá proměnná `hp[t] ∈ [0, rated_w]` vstupující do bilance `load_site`. Bazén je jednodušší — **konstantní příkon, binární běh**: + +- Proměnné: `pool[t] ∈ {0, 1}` (LpBinary) pro sloty v plánovacím horizontu; příkon ve slotu = `pool[t] * rated_power_w`. +- Bilance: `load_site = load_baseline + ev + hp[t] + pool[t] * rated_power_w`. +- **Denní povinný runtime** (kalendářní den v `site.timezone`, jako ostatní denní logika): pro každý den `d` plně pokrytý horizontem: `sum(pool[t] for t in day_d) * 15 >= daily_runtime_min` (zbytek dne při rolling replanu: odečíst již odběhané minuty z `telemetry_pool_pump` od půlnoci — viz `fn_battery_cycle_audit` vzor agregace). +- **Min. souvislý běh**: `min_run_min / 15` slotů — klasická minimální up-time formulace přes binárku startu `pool_start[t] >= pool[t] − pool[t−1]` a `pool[t..t+k] >= pool_start[t]`. +- Cíl: žádný extra term — levné/přebytkové sloty vyberou samy ceny v účelové funkci (import za `buy[t]`, ušlý export za `sell[t]`); v okně `sell < 0` funguje bazén přirozeně jako **flex sink** (viz `planning-neg-sell-strategy.md`, `E_surplus_after_t`). +- `schedulable = false` → solver čerpadlo ignoruje (jen telemetrie), `daily_runtime_min = 0` → žádný constraint. +- Pozor na MILP velikost: +96–144 binárek/den; držet se vzoru `z_export`/`y_imp` (HiGHS to zvládá). +- Výstup: `planning_interval.pool_pump_on` (nová migrace) + export přes signál (§4 fáze 2); audit follow-up: skutečnost z `telemetry_pool_pump` do `audit_interval`. + +## 6. Otevřené otázky / follow-upy + +- Sezónní profil `daily_runtime_min` (tabulka měsíc → minuty? řízení podle teploty vody?) — zatím ruční změna hodnoty. +- Produktové rozhodnutí UI pro flex zátěže (workshop dle `planning-neg-sell-strategy.md` §UI) — bazén do slot detailu a „Dnes X/Y h filtrace". +- Bazál: odečítat `telemetry_pool_pump.power_w` v `fn_update_baseline_stats` (jinak se bazén započte do baseline a solver by ho počítal dvakrát). +- PostgREST granty (`ems_anon`) na `telemetry_pool_pump` / view, až bude UI číst. +- Více čerpadel na jednom Shelly Pro 2PM (`shelly_switch_id` 0/1) — schéma to umí, collector i route ano; netestováno. + +## 7. Checklist oživení (placeholdery) + +1. [ ] Shelly připojené na LAN, statická IP ``, ověřit ručně: `curl http:///rpc/Switch.GetStatus?id=0` → JSON s `output` (Gen2!). +2. [ ] `insert into ems.site_endpoint …` (šablona §4) → ``. +3. [ ] `insert into ems.asset_pool_pump …` s `` (štítek čerpadla, typ. 400–1100 W) a `` (sezóna). +4. [ ] Počkat ≤ 60 s, ověřit telemetrii: `select * from ems.telemetry_pool_pump order by measured_at desc limit 5;` +5. [ ] `insert into ems.signal_route …` (šablona §4). +6. [ ] Test zapnutí: `select ems.fn_signal_enqueue_bool(, 'POOL_PUMP_ON', true);` → do ~30 s `signal_outbound_journal.status = 'verified'` a relé sepnuté; pak vypnout (`false`). +7. [ ] Zkontrolovat `power_w` v telemetrii při běhu ≈ `rated_power_w` (případně upravit). +8. [ ] Nastavit dočasné spínání (cron / ručně) do doby solver integrace (§5).