Initial commit

Made-with: Cursor
This commit is contained in:
Dusan Vojacek
2026-03-20 13:27:37 +01:00
commit 8b4af663d8
77 changed files with 13337 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
# Modul: Operating Modes (Provozní režimy)
## Koncept
EMS a Loxone komunikují přes **provozní režimy** pojmenované stavy které mají smysl pro obě strany.
EMS rozhoduje a přepíná režimy. Loxone dostane kód režimu jako číslo přes Virtual Input a ví jak se v daném režimu chovat **autonomně a nezávisle na EMS**.
```
EMS backend (každou minutu)
→ HTTP GET /dev/sps/io/EMS_Heartbeat/1 ← pulz do Loxone
EMS backend (při přepnutí režimu)
→ ems.fn_set_mode(site_id, 'SELF_SUSTAIN') ← zapsat do DB
→ HTTP GET /dev/sps/io/EMS_Mode/2 ← informovat Loxone
Loxone (zcela nezávisle na EMS)
→ sleduje přítomnost EMS_Heartbeat pulzů
→ pokud 5min žádný pulz → sám přepne na SELF_SUSTAIN
→ řídí střídač dle aktivního režimu bez čekání na setpointy
```
**Klíčový princip:** Loxone watchdog nečte DB. Sleduje pouze HTTP pulzy přímo.
Pokud padne celý server (RPi, Docker, síť) Loxone to pozná sám a přepne bezpečný režim.
Viz `docs/loxone-integration.md` pro kompletní popis Loxone implementace.
---
## Přehled režimů
| Kód | Loxone int | EV | TČ | Baterie | Síť | Loxone autonomní |
|---|---|---|---|---|---|---|
| `AUTO` | 1 | dle plánu | dle plánu | dle plánu | dle plánu | **ne** čeká na setpointy |
| `SELF_SUSTAIN` | 2 | ❌ stop | ❌ stop | vybíjí do domu | bez exportu | **ano** |
| `CHARGE_CHEAP` | 3 | ❌ stop | ❌ stop | max nabíjení | import ok | **ne** EMS posílá výkon |
| `PRESERVE` | 4 | ❌ stop | ❌ stop | drží SoC | import ok | **ano** |
| `MANUAL` | 0 | ❌ stop | ❌ stop | žádné akce | žádné akce | **ano** |
### `AUTO`
Normální provoz. EMS posílá přesné setpointy W každých 15 minut.
Loxone je čistý exekutor přijme číslo a zapíše do střídače.
Pokud setpoint nepřijde (výpadek EMS) → Loxone watchdog přepne na `SELF_SUSTAIN`.
### `SELF_SUSTAIN` ← výchozí stav + fallback
Aktivuje se:
- automaticky watchdogem při výpadku EMS (5min bez pulzu)
- manuálně uživatelem z UI (dovolená, odchod z domu)
- při prvním startu systému (seed data)
Loxone sám bez EMS:
- FVE pokrývá spotřebu
- baterie vybíjí do domu (ne do sítě)
- blokuje export do sítě
- zastavuje EV nabíjení a TČ
### `CHARGE_CHEAP`
Manuální přepis. EMS posílá max charge setpoint.
Použít při levné ceně nebo přetoku FVE ze sousedství (pokud víš o levné ceně dopředu).
### `PRESERVE`
Dovolená / servis. Loxone drží baterii na aktuálním SoC, žádné optimalizace.
Autonomní Loxone nevyžaduje setpointy od EMS.
### `MANUAL`
Technické práce. Žádná logika neřídí střídač. Pouze pro servis.
---
## Přepínání z UI (React)
```
POST /api/sites/{site_id}/mode
{
"mode": "SELF_SUSTAIN",
"valid_until": null, // nebo "2025-03-15T06:00:00+01:00" pro dočasný přepis
"notes": "Odjezd na dovolenou"
}
```
Backend při přepnutí:
1. Zavolá `ems.fn_set_mode(site_id, mode, 'user:'+username)` → zápis do DB + log
2. Okamžitě odešle HTTP do Loxone: `/dev/sps/io/EMS_Mode/{loxone_mode_value}`
3. Pokud `CHARGE_CHEAP` nebo návrat na `AUTO` → spustí replanning
**Dočasný přepis s automatickým návratem:**
`fn_expire_modes()` běží každou minutu a přepíná zpět lokality s prosahlým `valid_until`.
---
## EMS restart / reconnect
Při startu backendu:
1. Přečíst z Loxone aktuální `EMS_Mode_Active` (Virtual Output) přes HTTP GET
2. Porovnat s `ems.site_operating_mode` v DB
3. Pokud Loxone přepnul na `SELF_SUSTAIN` během výpadku → logovat, informovat, spustit nový plán
4. Přepnout na `AUTO` a začít posílat setpointy + heartbeat pulzy
---
## Heartbeat v DB pouze informační
Tabulka `ems.site_heartbeat` zaznamenává kdy EMS naposledy úspěšně odeslal pulz do Loxone.
Slouží pro EMS dashboard (`vw_site_status.ems_heartbeat_status`) a případný alerting.
**Neplní funkci watchdogu** to je čistě na Loxone straně.
```python
# backend/services/control_exporter.py každou minutu
async def send_heartbeat(site_id: int, loxone_endpoint, db):
try:
await loxone_http.get(f"/dev/sps/io/EMS_Heartbeat/1")
await db.execute(
"SELECT ems.fn_update_heartbeat($1, 'ok', $2)",
site_id, EMS_VERSION
)
except Exception as e:
logger.error(f"Heartbeat failed for site {site_id}: {e}")
await db.execute(
"SELECT ems.fn_update_heartbeat($1, 'error', $2)",
site_id, EMS_VERSION
)
# EMS nemůže nic dělat Loxone watchdog to vyřeší sám
```
---
## Otevřené body
- [ ] Ověřit Deye Modbus registry pro přepnutí Self-Consumption / Grid-First modu (pro SELF_SUSTAIN)
- [ ] Implementace Loxone watchdog viz `docs/loxone-integration.md`
- [ ] Alert notifikace (email / push) pokud `ems_heartbeat_status = 'stale'` déle než 10 minut