Files
ems/docs/04-modules/pool-shelly.md
Dusan Vojacek 29d854f23d
Some checks failed
CI and deploy / deploy (push) Has been cancelled
CI and deploy / migration-check (push) Has been cancelled
Bazén vizualizace + EV Discord notifikace po příjezdu (fáze A)
- R__097: vw_latest_pool_pump + vw_pool_pump_day_energy (denní kWh z delty
  čítače, minuty běhu) + ems_anon granty
- PoolCard na Dashboardu: stav/W/dnešní kWh+hodiny/7denní mini sloupce
- _notify_ev_arrival_plan: po příjezdu EV Discord souhrn (SoC auta → cíl,
  deadline, nabíjecí okna shlukovaná ze slotů aktivního plánu, ø cena)
- docs/discord-ev-interaction.md: fáze B (bot s tlačítky přes gateway —
  žádný veřejný endpoint; čeká na DISCORD_BOT_TOKEN od uživatele)
- docs: pool-shelly + ev-charging aktualizovány (pravidlo docs 1:1)

První commit na dev větvi (nová kadence: deploy až s milníkovým merge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:59:09 +02:00

122 lines
9.5 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.
# 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 240480 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 (<site_id>, 'http_api', '<SHELLY_IP>', 80, 'http', true, 'Shelly relé bazénového čerpadla')
returning id; -- → <endpoint_id>
-- 2) aktivum
insert into ems.asset_pool_pump (site_id, code, endpoint_id, rated_power_w, min_run_min, daily_runtime_min)
values (<site_id>, 'pool-pump-01', <endpoint_id>, <RATED_POWER_W>, 15, <DAILY_RUNTIME_MIN>);
-- 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 (
<site_id>, 'http_rest', <endpoint_id>, '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(<site_id>, '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[t1]` 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: +96144 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 `<SHELLY_IP>`, ověřit ručně: `curl http://<SHELLY_IP>/rpc/Switch.GetStatus?id=0` → JSON s `output` (Gen2!).
2. [ ] `insert into ems.site_endpoint …` (šablona §4) → `<endpoint_id>`.
3. [ ] `insert into ems.asset_pool_pump …` s `<RATED_POWER_W>` (štítek čerpadla, typ. 4001100 W) a `<DAILY_RUNTIME_MIN>` (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(<site_id>, '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.