Files
ems/docs/04-modules/pool-shelly.md
Dusan Vojacek 15d47e8a80
All checks were successful
CI and deploy / migration-check (push) Successful in 42s
CI and deploy / deploy (push) Has been skipped
Bazén: sezóna (schedulable), filtrace dle teploty vody, Loxone čidla
- V092: ems.loxone_sensor + telemetry_loxone_sensor (hypertable) — generické
  čtení Loxone hodnot (poslouží i ohřevu/akumulačce); pool sloupce teplotní
  funkce (ref/base/per_c/min/max) + water_temp_sensor_id
- R__098 fn_pool_daily_runtime_min: clamp(base+per_c×(t−ref)) z poslední
  teploty <24 h, fallback daily_runtime_min; JSON detail pro UI/solver
- collector poll_loxone_sensors: /jdev/sps/io/<name>/state, LL.value parse,
  no-op bez čidel
- sezóna = schedulable přepínač (dokumentováno vč. SQL); hranice filtrace ×
  ohřev TČ (oddělené logiky, sdílí jen čidlo)

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

154 lines
11 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.
## 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.