# 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). ## Vizualizace (2026-06-12, dev) - `vw_latest_pool_pump` (LATERAL poslední vzorek + data_age) a `vw_pool_pump_day_energy` (denní kWh z delty čítače + minuty běhu, 8 dní) — PostgREST grant `ems_anon` (R__097). - Dashboard: `PoolCard` (frontend/src/components/PoolCard.tsx) pod StatePanel — stav (běží/stojí/stale), aktuální W, dnešní kWh a hodiny běhu, mini sloupce 7 dní. Poll 60 s.