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

11 KiB
Raw Blame History

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_idsite_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 <...>):

-- 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=truesent → worker signal_outbound_verify přečte Switch.GetStatus a porovná $.outputverified (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):

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:

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.