# 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 (<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[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 `<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. 400–1100 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.

## Sezóna a teplotní funkce (2026-06-12, dev)

**Sezóna = jeden přepínač** (`asset_pool_pump.schedulable`):
```sql
update ems.asset_pool_pump set schedulable = false where code = 'pool-pump-1';  -- konec sezóny
update ems.asset_pool_pump set schedulable = true  where code = 'pool-pump-1';  -- začátek
```
Off-season: telemetrie běží dál (zimování čerpadla vidíš), plánovač a signály ne.
(Později tlačítko v Konfiguraci.)

**Délka filtrace dle teploty vody** (`fn_pool_daily_runtime_min`, V092):
`runtime = clamp(base + per_°C × (t − ref), min, max)`; defaulty pro 30 m³ /
8 m³/h (obrátka 3.75 h, slaná voda — chlorinátor potřebuje průtok):
20 °C→4.5 h, 24 °C→6.5 h, 28 °C→8.5 h, strop 10 h. Bez čidla / měření
staršího 24 h → fallback `daily_runtime_min`. Vše sloupce na čerpadle.

**Čidlo teploty**: až přidáš do Loxonu, jeden INSERT:
```sql
insert into ems.loxone_sensor (site_id, code, loxone_name, unit)
  values ((select id from ems.site where code='home-01'), 'pool-water-temp', '<JmenoVLoxonu>', '°C');
update ems.asset_pool_pump set water_temp_sensor_id =
  (select id from ems.loxone_sensor where code='pool-water-temp') where code='pool-pump-1';
```
Collector čte každou minutu (`poll_loxone_sensors` — generické, poslouží i
akumulační nádrži pro ohřev).

**Hranice s ohřevem přes TČ (žádný šelmostroj):** filtrace = denní rozpočet
minut pro solver; ohřev = samostatný explicitní program (docs z 12. 6.:
sekvence Shelly pump → HEX pump → TČ), který si čerpadlo prostě zapne
(interlock) — minuty běhu se započtou samy přes telemetrii. Jediná vazba je
teplotní čidlo, které sdílí obě logiky.
