second version
This commit is contained in:
184
docs/04-modules/modbus-registers.md
Normal file
184
docs/04-modules/modbus-registers.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Deye Modbus Registry – EMS řízení
|
||||
|
||||
## Důležité pravidlo
|
||||
|
||||
- Registry **60–499**: POUZE **FC 0x10** (`write_registers`)
|
||||
- Registry **0–59**: FC 0x03 čtení, FC 0x06 zápis
|
||||
- Registry **500+**: FC 0x03 pouze čtení
|
||||
|
||||
EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_registers`** (FC 0x10), nikdy `write_register` (FC 0x06) pro rozsah 60–499.
|
||||
|
||||
## Řídící registry (R/W, FC 0x10)
|
||||
|
||||
| Reg | Název | Rozsah | Jednotka | Použití v EMS |
|
||||
|-----|-------|--------|----------|---------------|
|
||||
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Limit nabíjení baterie; horní mez není napříč modely stejná (nižší výkonové řady mívají jiný strop než např. SUN-20K) |
|
||||
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Limit vybíjení baterie; viz výše |
|
||||
| 128 | Grid charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Nabíjení ze sítě |
|
||||
| 130 | Grid charge enable | 0/1 | — | 1 = povolit nabíjení ze sítě |
|
||||
| 141 | Energy mgmt mode | bitmask | — | EMS vždy **0** (neměnit jinak) |
|
||||
| 142 | Limit control | 0/1/2 | — | **0** = selling first, **1** = zero export (built-in CT); EMS přepíná export vs. idle/nabíjení |
|
||||
| 143 | Export limit W | závisí na typu (SUN-20K až ~13 500) | 1 W | Max export do sítě; hodnota z `site_grid_connection.max_export_power_w` |
|
||||
| 178 | Grid peak shaving switch | bitmask | — | EMS zapisuje **pevnou** hodnotu (bez read-modify-write kvůli kolizím s paralelním čtením z Loxone): **32** (`0b00100000`, bit4–5 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit4–5 = **11**) v **PASSIVE** a **CHARGE**. |
|
||||
| 190 | GEN peak shaving | 0–16000 | 1 W | Peak shaving na GEN portu |
|
||||
| 191 | Grid peak shaving power | 0–16000 | 1 W | **EMS NEZAPISUJE** – nastavit **manuálně v SolarmanApp**. Hodnota určuje výkon peak shavingu v **W**. |
|
||||
|
||||
`control_exporter.write_inverter_setpoints` zapisuje přes **`modbus_command`** (journal + verifikace) po řadě: **62–64** (čas), **time points 148–177**, **108, 109, 141, 142, 178, 143**. Popisné názvy registrů v DB bere `DEYE_REGISTER_NAMES` v `control_exporter.py`. **Reg 191** do journalu nepatří – EMS ho nezapisuje.
|
||||
|
||||
### Reg 191 (výkon grid peak shaving)
|
||||
|
||||
- **EMS NEZAPISUJE** – nastavit **manuálně v SolarmanApp**.
|
||||
- Hodnota určuje výkon peak shavingu v **W** (typicky 0–16 000).
|
||||
|
||||
### Reg 178 – hodnoty podle fyzického režimu
|
||||
|
||||
- **SELL:** **32** – bit4–5 = **10**, grid peak shaving **disable** (export do sítě).
|
||||
- **PASSIVE** a **CHARGE:** **48** – bit4–5 = **11**, grid peak shaving **enable**.
|
||||
|
||||
EMS **nezapisuje** read-modify-write (paralelní čtení jinými klienty může způsobit nesoulad).
|
||||
|
||||
## Klíčové registry podle fyzického režimu Deye
|
||||
|
||||
Provozní režimy EMS (AUTO, SELF_SUSTAIN, SELL, …) se mapují na **tři fyzické režimy** střídače: **PASSIVE**, **SELL**, **CHARGE**. Ostatní je politika solveru / EMS, ne samostatný „režim“ invertoru.
|
||||
|
||||
| Reg | PASSIVE | SELL | CHARGE |
|
||||
|-----|---------|------|--------|
|
||||
| 142 | 1 (zero export to load) | 0 (selling first) | 1 |
|
||||
| 108 | `max_charge_a` z DB | `max_charge_a` z DB | `battery_watts_to_amps(battery_w, max_charge_a)` |
|
||||
| 109 | `max_discharge_a` z DB | `max_discharge_a` z DB | 0 |
|
||||
| 178 | 48 | 32 | 48 |
|
||||
| 143 | max export W z DB | max export W z DB | max export W z DB |
|
||||
| 141 | 0 | 0 | 0 |
|
||||
|
||||
**Důležité:** V **PASSIVE** i **SELL** jsou registry **108** a **109** vždy na **plném limitu z DB**. Deye si tok energie reguluje sám; snížení 108/109 pod maximum brání reakci na nepředvídatelnou spotřebu nebo přebytky FVE.
|
||||
|
||||
### Detekce fyzického režimu (`get_deye_mode` v `control_exporter.py`)
|
||||
|
||||
Vychází z **`grid_setpoint_w`** a **`battery_w`** z `ControlSetpoints` (aktivní plán / politika EMS), ne z telemetrie.
|
||||
|
||||
| Režim | Podmínka |
|
||||
|-------|----------|
|
||||
| **SELL** | `grid_setpoint_w` < −200 |
|
||||
| **CHARGE** | `battery_w` > 500 **a** `grid_setpoint_w` > 200 |
|
||||
| **PASSIVE** | vše ostatní (včetně SELF_SUSTAIN, IDLE, …) |
|
||||
|
||||
Režim **CHARGE_CHEAP** v EMS nastaví `grid_setpoint_w` tak, aby platila podmínka importu (> 200 W), jinak by fyzicky zůstal PASSIVE.
|
||||
|
||||
Všechny limity (`max_charge_a`, `max_discharge_a`, `max_export_power_w` / reg 143) pocházejí **výhradně z DB** (`_load_inverter_config`).
|
||||
|
||||
## Time Points – řízení podle fyzického režimu
|
||||
|
||||
Deye má 6 časových bloků. EMS přepisuje **bloky 1–2** při každém `control_export` (cron např. :14, :29, :44, :59).
|
||||
|
||||
**Výběr aktivního segmentu na invertoru:** platí poslední časový bod, jehož **HH:MM ≤ aktuálnímu času** na hodinách střídače (po synchronizaci 62–64). Proto **nesmí** zůstat jako jediný „minulý“ bod např. **00:00** s pasivním profilem, zatímco profil s nabíjením ze sítě je až u budoucího času – mezi půlnocí a tím budoucím časem by invertor celou dobu používal špatný segment.
|
||||
|
||||
| Blok | Čas (HHMM, Europe/Prague) | Zdroj plánu | Účel | SOC min | Grid charge |
|
||||
|------|---------------------------|-------------|------|---------|-------------|
|
||||
| 1 | **`current_slot_hhmm()`** – začátek **probíhajícího** 15min slotu | `planning_interval` pro **aktuální** slot (`_fetch_plan_row_for_slot_offset(..., 0)`) | PASSIVE / SELL / CHARGE dle `_deye_tou_params` | viz tabulka níže | viz tabulka níže |
|
||||
| 2 | **`next_slot_hhmm()`** – začátek **následujícího** 15min slotu | `planning_interval` pro **další** slot (`_fetch_plan_row_for_slot_offset(..., 1)`) | Přechod na další čtvrthodinu | viz tabulka níže | viz tabulka níže |
|
||||
| 3–6 | 23:59 | — | Neaktivní (rezerva) | `reserve_soc` (DB) | NE |
|
||||
|
||||
**Registry 108 / 109 / 142 / 178 / 143** odpovídají **aktuálnímu** plánu (okamžitý výstup; `setpoints_now` v `write_inverter_setpoints`). TOU řádky 1–2 doplňují stejnou logiku pro časové segmenty (`_deye_tou_params`).
|
||||
|
||||
Příklad v 14:18: blok 1 má čas **1415**, blok 2 čas **1430** – mezi 14:15 a 14:29 je aktivní segment z bloku 1 (sladěný s plánem pro 14:15–14:30), po 14:30 blok 2 (plán 14:30–14:45). Po dalším exportu se oba časy posunou (např. 14:30 / 14:45).
|
||||
|
||||
### Fyzické režimy Deye – parametry jednoho time pointu (bloky 1–2)
|
||||
|
||||
| Režim | Výkon (W) | SOC min | Grid charge |
|
||||
|-------|-----------|---------|-------------|
|
||||
| **PASSIVE** | `max_discharge_a × 51,2` | rezerva z DB | NE |
|
||||
| **SELL** | `max_discharge_a × 51,2` | rezerva z DB | NE |
|
||||
| **CHARGE** | `battery_watts_to_amps(battery_w, max_charge_a) × 51,2` | min(95, cíl SoC z plánu nebo 80) | ANO |
|
||||
|
||||
Bloky 3–6 zůstávají na **23:59** s pasivním profilem (`reserve_soc`, grid charge = NE).
|
||||
|
||||
### Synchronizace času
|
||||
|
||||
Registry **62–64** se při každém `control_export` nastaví na aktuální čas v **Europe/Prague**:
|
||||
|
||||
- reg **62:** `(rok - 2000) << 8 | měsíc`
|
||||
- reg **63:** `den << 8 | hodina`
|
||||
- reg **64:** `minuta << 8 | sekunda`
|
||||
|
||||
Zápis time pointů i systémového času prochází stejným **`modbus_command`** journal jako registry 108 / 109 / 141 / 142 / 178 / 143 (FC 0x10 po jednom registru).
|
||||
|
||||
### Mapování registrů (time point *i*, i = 0…5)
|
||||
|
||||
| Účel | Adresa |
|
||||
|------|--------|
|
||||
| Čas HHMM | 148 + *i* |
|
||||
| Výkon (W) | 154 + *i* |
|
||||
| Min. SOC % | 166 + *i* |
|
||||
| Grid charge enable 0/1 | 172 + *i* |
|
||||
|
||||
Limity nabíjení/vybíjení v ampérech a export z **site_grid_connection** / **asset_inverter** / **asset_battery** načítá `_load_inverter_config()` (`max_charge_a` / `max_discharge_a` jako `LEAST(BMS, střídač) / 51,2`). Python **neřeže** na univerzální číslo – hodnoty v DB mají odpovídat **skutečnému modelu** střídače a BMS (maximální povolená hodnota v registru se liší podle typu; není to všude např. 185 A). Ověřit v dokumentaci k danému SUN-*K.
|
||||
|
||||
## Telemetrické registry (R only, FC 0x03)
|
||||
|
||||
| Reg | Název | Jednotka | Poznámka |
|
||||
|-----|-------|----------|----------|
|
||||
| 500 | Run state | — | 0 = standby, 2 = normal |
|
||||
| 588 | Battery SOC | 1 % | |
|
||||
| 590 | Battery power | 1 W S16 | + vybíjení / − nabíjení |
|
||||
| 625 | Grid total power | 1 W S16 | + import / − export |
|
||||
| 653 | Load total power | 1 W S16 | |
|
||||
| 667 | GEN port power | 1 W | FVE pole B |
|
||||
| 672 | PV1 power | 1 W | |
|
||||
| 673 | PV2 power | 1 W | |
|
||||
|
||||
## Přepočty
|
||||
|
||||
- Výkon baterie → proud (LV 51,2 V): `battery_watts_to_amps(power_w, max_amps) = min(max(0, max_amps), max(0, round(|power_w| / 51.2)))`, kde `max_amps` je z DB
|
||||
- `max_export_power_w` / `max_import_power_w` / limity baterie berou se z DB (`_load_inverter_config`), ne z natvrdo v Pythonu
|
||||
- Export do registru **143** = `site_grid_connection.max_export_power_w` (např. home-01 / SUN-20K **13 500 W**)
|
||||
|
||||
## Ověření (Modbus + DB)
|
||||
|
||||
```bash
|
||||
docker compose up -d --build backend
|
||||
```
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from pymodbus.client import AsyncModbusTcpClient
|
||||
|
||||
async def check():
|
||||
c = AsyncModbusTcpClient('172.16.1.10', port=502, timeout=5)
|
||||
await c.connect()
|
||||
|
||||
times = await c.read_holding_registers(148, count=2)
|
||||
for i in range(2):
|
||||
h, m = divmod(times.registers[i], 100)
|
||||
print(f'Time point {i+1}: {h:02d}:{m:02d}')
|
||||
|
||||
for name, reg in [
|
||||
('Limit control', 142),
|
||||
('Peak sw (bit4-5)', 178),
|
||||
('Export limit', 143),
|
||||
('Discharge A', 109),
|
||||
('Grid power', 625),
|
||||
]:
|
||||
r = await c.read_holding_registers(reg, count=1)
|
||||
raw = r.registers[0]
|
||||
signed = raw - 65536 if raw > 32767 else raw
|
||||
print(f'{name} ({reg}): {signed}')
|
||||
|
||||
c.close()
|
||||
|
||||
asyncio.run(check())
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose exec db psql -U ems_user -d ems -c "
|
||||
SELECT register_name, value_to_write, status,
|
||||
created_at AT TIME ZONE 'Europe/Prague' AS cas
|
||||
FROM ems.modbus_command
|
||||
WHERE site_id=2 AND register IN (108, 109, 142)
|
||||
ORDER BY created_at DESC LIMIT 9;"
|
||||
```
|
||||
|
||||
## Související
|
||||
|
||||
- `docs/04-modules/modbus-command-journal.md` – journal a verifikace
|
||||
- `backend/services/control_exporter.py` – zápisy
|
||||
- `backend/services/modbus_client.py` – `write_registers` (FC 0x10)
|
||||
Reference in New Issue
Block a user