Compare commits
4 Commits
87fc9b41cf
...
refactor-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3a49806b | ||
|
|
b35f292295 | ||
|
|
6471467bc5 | ||
|
|
ba53fe5bfc |
@@ -108,7 +108,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
|
|||||||
|
|
||||||
17. **Modbus zápis = journal.** Každý zápis do zařízení přes control exporter se loguje do `ems.modbus_command`. **Verifikační job** běží každé **2 minuty** a ověřuje nedávno zápis (`written` → čtení registru). Při **mismatch** po max. **3** pokusech o zápis → u běžných registrů přepnutí na **SELF_SUSTAIN** (`run_fn_set_mode_with_discord` → `fn_set_mode`, `activated_by` = `system:mismatch`) + **Discord** při skutečné změně režimu. **Výjimka:** souvislý blok Deye **62–64** (čas) → po 3 neúspěšných ověřeních **bez** změny režimu, kritický **Discord** (`notify_modbus_clock_verify_exhausted`). **Obecně:** při jakékoli změně `mode_code` z Pythonu (`POST /api/v1/sites/{id}/mode`, mismatch → SELF_SUSTAIN, `fn_expire_modes`) lze Discord zapnout přes `DISCORD_WEBHOOK_URL`. Detail: `docs/04-modules/modbus-command-journal.md`.
|
17. **Modbus zápis = journal.** Každý zápis do zařízení přes control exporter se loguje do `ems.modbus_command`. **Verifikační job** běží každé **2 minuty** a ověřuje nedávno zápis (`written` → čtení registru). Při **mismatch** po max. **3** pokusech o zápis → u běžných registrů přepnutí na **SELF_SUSTAIN** (`run_fn_set_mode_with_discord` → `fn_set_mode`, `activated_by` = `system:mismatch`) + **Discord** při skutečné změně režimu. **Výjimka:** souvislý blok Deye **62–64** (čas) → po 3 neúspěšných ověřeních **bez** změny režimu, kritický **Discord** (`notify_modbus_clock_verify_exhausted`). **Obecně:** při jakékoli změně `mode_code` z Pythonu (`POST /api/v1/sites/{id}/mode`, mismatch → SELF_SUSTAIN, `fn_expire_modes`) lze Discord zapnout přes `DISCORD_WEBHOOK_URL`. Detail: `docs/04-modules/modbus-command-journal.md`.
|
||||||
|
|
||||||
18. **Deye zápis registrů 60–499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzický režim Deye** (`PASSIVE` / `CHARGE` / `SELL`): **výhradně** `get_deye_mode` v `exporter_monolith.py` (bez wattových prahů: **SELL** při `battery_w` < 0 a `grid_setpoint_w` < 0; **CHARGE** při obou > 0; jinak **PASSIVE**). **PASSIVE (ZERO, AUTO):** **108/109** dle `_deye_zero_export_amps_for_passive`; **TOU SOC** (reg 166+): **PASSIVE** = **`min_soc_percent`**, **CHARGE** = **`max_soc_percent`** (clamp 10–100 z DB), **SELL** = **`reserve_soc_percent`** (`_deye_passive_tou_battery_soc_pct`, `_deye_tou_params`). **SELL:** 108=0, 109=max, **178**=32 (peak shaving off), **143** omezeno podle `|grid_setpoint_w|`; **142/145/TOU** jako v `write_inverter_setpoints`. **Reg 340** (*max solar power*, W): jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (lokalita se zeleným bonusem na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`; hodnota z `pv_a_forecast_solver_w` / `pv_a_curtailed_w` (AUTO). **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. **PRESERVE:** `lock_battery` → 108/109=0. **Čas 62–64**, bloky TOU **1–2** vs **3–6**, verify, Discord: beze změny oproti dřívějšímu chování — plný popis **`docs/04-modules/modbus-registers.md`** a **`docs/04-modules/operating-modes.md`**.
|
18. **Deye zápis registrů 60–499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzický režim Deye** (`PASSIVE` / `CHARGE` / `SELL`): **výhradně** `get_deye_mode` v `exporter_monolith.py` (bez wattových prahů: **SELL** při `battery_w` < 0 a `grid_setpoint_w` < 0; **CHARGE** při obou > 0; jinak **PASSIVE**). **PASSIVE (ZERO, AUTO):** **108/109** dle `deye_battery_charge_discharge_amps` a `_deye_zero_export_amps_for_passive` (jen asymetrie **import bez vybíjení** → **109 = 0**; export **108** nenuluje); **TOU SOC** (reg 166+): **PASSIVE** = **`min_soc_percent`**, **CHARGE** = **`max_soc_percent`** (clamp 10–100 z DB), **SELL** = **`reserve_soc_percent`** (`_deye_passive_tou_battery_soc_pct`, `_deye_tou_params`). **SELL:** 108=0, 109=max, **178**=32 (peak shaving off), **143** omezeno podle `|grid_setpoint_w|`; **142/145/TOU** jako v `write_inverter_setpoints`. **Reg 340** (*max solar power*, W): jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (lokalita se zeleným bonusem na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`; hodnota z `pv_a_forecast_solver_w` / `pv_a_curtailed_w` (AUTO). **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. **PRESERVE:** `lock_battery` → 108/109=0. **Čas 62–64**, bloky TOU **1–2** vs **3–6**, verify, Discord: beze změny oproti dřívějšímu chování — plný popis **`docs/04-modules/modbus-registers.md`** a **`docs/04-modules/operating-modes.md`**.
|
||||||
|
|
||||||
19. **Baterie – export v LP:** V `solve_dispatch` binárka `z_export[t]`: pokud `grid_export` v daném slotu **≥ 1** W, platí koncové `soc[t] ≥ arb_base_wh` (ekonomická rezerva z DB, ne časová řada `arb_floor_series`). Bez exportu může plán jít k `min_soc_percent` (provozní podlaha; u paralelních packů často 11–12 %, migrace V029 + komentář sloupce).
|
19. **Baterie – export v LP:** V `solve_dispatch` binárka `z_export[t]`: pokud `grid_export` v daném slotu **≥ 1** W, platí koncové `soc[t] ≥ arb_base_wh` (ekonomická rezerva z DB, ne časová řada `arb_floor_series`). Bez exportu může plán jít k `min_soc_percent` (provozní podlaha; u paralelních packů často 11–12 %, migrace V029 + komentář sloupce).
|
||||||
|
|
||||||
|
|||||||
@@ -244,15 +244,14 @@ def _deye_zero_export_amps_for_passive(
|
|||||||
max_discharge_a: int,
|
max_discharge_a: int,
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
PASSIVE (zero export k CT/zátěži): výchozí plné 108/109.
|
PASSIVE (zero export k CT/zátěži): asymetrie jen tam, kde dává smysl pro import.
|
||||||
|
|
||||||
Export v plánu bez vybíjení baterie vypne charge A; import bez nabíjení vypne discharge A.
|
Export do sítě (grid_w < 0) už směr toku řeší režim / 142 / 145 — **108** jako strop zbytečně
|
||||||
|
nenulovat na 0 (viz home-01). Jediná speciální větev: import bez nabíjení → vypnout vybíjení.
|
||||||
"""
|
"""
|
||||||
if grid_w < 0 and bat_w >= 0:
|
|
||||||
return 0, max_discharge_a
|
|
||||||
if grid_w > 0 and bat_w <= 0:
|
if grid_w > 0 and bat_w <= 0:
|
||||||
return max_charge_a, 0
|
return max_charge_a, 0
|
||||||
return max_charge_a, max_discharge_a
|
return int(max_charge_a), int(max_discharge_a)
|
||||||
|
|
||||||
|
|
||||||
def deye_battery_charge_discharge_amps(
|
def deye_battery_charge_discharge_amps(
|
||||||
@@ -268,7 +267,10 @@ def deye_battery_charge_discharge_amps(
|
|||||||
"""
|
"""
|
||||||
Proud nabíjení / vybíjení (reg 108 / 109) pro zápis Deye.
|
Proud nabíjení / vybíjení (reg 108 / 109) pro zápis Deye.
|
||||||
|
|
||||||
PASSIVE + plán chce nabíjet z PV přebytku i při exportu do sítě: nenulový charge, discharge 0.
|
PASSIVE + plán chce nabíjet (typicky z FVE, i při exportu zbytku do sítě): **108 = max_charge_a**
|
||||||
|
z invertoru — reg. 108 je strop, ne příkaz k proudu; průměrný `battery_w` ze slotu nesmí špičku FVE
|
||||||
|
stíhat do baterie omezovat. **109 = max_discharge_a** (domácnost z baterie při výpadku PV).
|
||||||
|
**CHARGE** (import ze sítě + nabíjení): 108 dál z `battery_w` (řízený importní okamžik). **SELL** beze změny.
|
||||||
"""
|
"""
|
||||||
if lock_battery:
|
if lock_battery:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
@@ -279,7 +281,7 @@ def deye_battery_charge_discharge_amps(
|
|||||||
if self_sustain_local_use:
|
if self_sustain_local_use:
|
||||||
return int(max_charge_a), int(max_discharge_a)
|
return int(max_charge_a), int(max_discharge_a)
|
||||||
if bat_w > 0:
|
if bat_w > 0:
|
||||||
return battery_watts_to_amps(bat_w, max_charge_a), 0
|
return int(max_charge_a), int(max_discharge_a)
|
||||||
return _deye_zero_export_amps_for_passive(
|
return _deye_zero_export_amps_for_passive(
|
||||||
grid_w, bat_w, int(max_charge_a), int(max_discharge_a)
|
grid_w, bat_w, int(max_charge_a), int(max_discharge_a)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""PASSIVE + nabíjení z PV přebytku při současném exportu → nenulový nabíjecí proud."""
|
"""PASSIVE + plán chce nabíjet: 108 = plný strop z DB, 109 = max (PV špička + domácnost)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,10 +18,25 @@ class PassivePvSurplusChargeAmpsTests(unittest.TestCase):
|
|||||||
max_charge_a=100,
|
max_charge_a=100,
|
||||||
max_discharge_a=100,
|
max_discharge_a=100,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(ch, 100)
|
||||||
|
self.assertEqual(dis, 100)
|
||||||
|
|
||||||
|
def test_charge_mode_still_scales_108_from_battery_w(self) -> None:
|
||||||
|
"""CHARGE (síť + baterie): 108 podle plánu, ne vždy plný strop."""
|
||||||
|
ch, dis = deye_battery_charge_discharge_amps(
|
||||||
|
lock_battery=False,
|
||||||
|
deye_mode="CHARGE",
|
||||||
|
self_sustain_local_use=False,
|
||||||
|
bat_w=2000,
|
||||||
|
grid_w=3000,
|
||||||
|
max_charge_a=100,
|
||||||
|
max_discharge_a=100,
|
||||||
|
)
|
||||||
|
self.assertLess(ch, 100)
|
||||||
self.assertGreater(ch, 0)
|
self.assertGreater(ch, 0)
|
||||||
self.assertEqual(dis, 0)
|
self.assertEqual(dis, 0)
|
||||||
|
|
||||||
def test_passive_zero_export_still_zero_charge_when_no_battery_charge(self) -> None:
|
def test_passive_export_without_battery_charge_uses_max_108(self) -> None:
|
||||||
ch, dis = deye_battery_charge_discharge_amps(
|
ch, dis = deye_battery_charge_discharge_amps(
|
||||||
lock_battery=False,
|
lock_battery=False,
|
||||||
deye_mode="PASSIVE",
|
deye_mode="PASSIVE",
|
||||||
@@ -31,7 +46,7 @@ class PassivePvSurplusChargeAmpsTests(unittest.TestCase):
|
|||||||
max_charge_a=100,
|
max_charge_a=100,
|
||||||
max_discharge_a=100,
|
max_discharge_a=100,
|
||||||
)
|
)
|
||||||
self.assertEqual(ch, 0)
|
self.assertEqual(ch, 100)
|
||||||
self.assertEqual(dis, 100)
|
self.assertEqual(dis, 100)
|
||||||
|
|
||||||
def test_sell_unchanged(self) -> None:
|
def test_sell_unchanged(self) -> None:
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ from services.control.exporter_monolith import (
|
|||||||
_deye_reg178_verify_with_double_read,
|
_deye_reg178_verify_with_double_read,
|
||||||
_deye_tou_params,
|
_deye_tou_params,
|
||||||
_deye_tou_power_verify_match,
|
_deye_tou_power_verify_match,
|
||||||
_deye_zero_export_amps_for_passive,
|
|
||||||
deye_reg_triggers_self_sustain_after_verify_exhaust,
|
deye_reg_triggers_self_sustain_after_verify_exhaust,
|
||||||
get_deye_mode,
|
get_deye_mode,
|
||||||
)
|
)
|
||||||
from services.control.models import OperatingModeInfo
|
from services.control.models import OperatingModeInfo
|
||||||
from services.control.setpoints import _build_setpoints
|
from services.control.setpoints import _build_setpoints, _deye_zero_export_amps_for_passive
|
||||||
|
|
||||||
|
|
||||||
def _inv(*, min_soc: int | None = 12, reserve_soc: int | None = 20) -> InverterConfig:
|
def _inv(*, min_soc: int | None = 12, reserve_soc: int | None = 20) -> InverterConfig:
|
||||||
@@ -273,7 +272,7 @@ class DeyeTouParamsTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_zero_export_amps_fve_overflow(self) -> None:
|
def test_zero_export_amps_fve_overflow(self) -> None:
|
||||||
c, d = _deye_zero_export_amps_for_passive(-1000, 0, 100, 90)
|
c, d = _deye_zero_export_amps_for_passive(-1000, 0, 100, 90)
|
||||||
self.assertEqual(c, 0)
|
self.assertEqual(c, 100)
|
||||||
self.assertEqual(d, 90)
|
self.assertEqual(d, 90)
|
||||||
|
|
||||||
def test_zero_export_amps_import_hold_discharge(self) -> None:
|
def test_zero_export_amps_import_hold_discharge(self) -> None:
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
-- sloty pro LP: ceny, forecast, baseline, EV připojení + masky allow_charge / allow_discharge_export
|
-- sloty pro LP: ceny, forecast, baseline, EV připojení + masky allow_charge / allow_discharge_export
|
||||||
|
-- DROP: změna RETURNS TABLE (nové sloupce) — CREATE OR REPLACE na rozdílný row type v PG neprojde.
|
||||||
|
-- Musí být plná signatura (pg_proc ukládá int jako integer); DROP bez () funkci se směrem nemaže.
|
||||||
|
|
||||||
|
drop function if exists ems.fn_load_planning_slots_full(
|
||||||
|
integer, timestamp with time zone, timestamp with time zone, numeric
|
||||||
|
);
|
||||||
|
|
||||||
create or replace function ems.fn_load_planning_slots_full(
|
create or replace function ems.fn_load_planning_slots_full(
|
||||||
p_site_id int,
|
p_site_id int,
|
||||||
@@ -498,7 +504,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
$fn$;
|
$fn$;
|
||||||
|
|
||||||
comment on function ems.fn_load_planning_slots_full(int, timestamptz, timestamptz, numeric) is
|
comment on function ems.fn_load_planning_slots_full is
|
||||||
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
|
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
|
||||||
'Masky charge/discharge-export se berou zvlášť pro 00–12 a 12–24 Europe/Prague (polovina budgetu na segment). '
|
'Masky charge/discharge-export se berou zvlášť pro 00–12 a 12–24 Europe/Prague (polovina budgetu na segment). '
|
||||||
'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
|
'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
|||||||
| **CHARGE** | `battery_w` > 0 **a** `grid_setpoint_w` > 0 |
|
| **CHARGE** | `battery_w` > 0 **a** `grid_setpoint_w` > 0 |
|
||||||
| **PASSIVE (ZERO)** | vše ostatní — reg. **108/109** z `deye_battery_charge_discharge_amps()` v `setpoints.py` (volá `write_inverter_setpoints`) |
|
| **PASSIVE (ZERO)** | vše ostatní — reg. **108/109** z `deye_battery_charge_discharge_amps()` v `setpoints.py` (volá `write_inverter_setpoints`) |
|
||||||
|
|
||||||
**PASSIVE** (AUTO, ZERO): proudy **108/109** počítá **`deye_battery_charge_discharge_amps`**: pokud plán žádá **nabíjení** (`battery_w > 0`) včetně scénáře **PV přebytek + export do sítě** (`grid_setpoint_w < 0`), nastaví se **kladný nabíjecí proud (108)** a **109 = 0** — nesmí se použít čistě „zero export“ větev, která by při exportu vynutila **108 = 0** a rozbila soulad plán ↔ Deye. Jinak platí `_deye_zero_export_amps_for_passive` (export bez nabíjení → 108 = 0, import bez vybíjení → 109 = 0). **TOU** z plánu. Reg. **142** = `deye_zero_export_mode`. Reg. **143** je tvrdý limit exportu z lokality/invertoru (ne forecast). Reg. **145** (solar sell): **0** při `export_ban` mimo SELL, jinak **1** — význam přepínače a rozdíl vůči neřízeným FVE polím je v [`operating-modes.md`](operating-modes.md) (sekce *ZERO a zakázaný export*).
|
**PASSIVE** (AUTO, ZERO): proudy **108/109** počítá **`deye_battery_charge_discharge_amps`**: pokud plán žádá **nabíjení** (`battery_w > 0`) a režim zůstává **PASSIVE** (typicky FVE přebytek, často i **export** části výroby), **108 = max_charge_a z invertoru** — jde o **horní limit** proudu do baterie; průměrný `battery_w` ze 15min slotu nesmí špičku FVE do baterie uměle omezovat (dřívější odvod z W dával smysl jen u **CHARGE** ze sítě). **109 = max z DB**. Když plán nabíjení nechce (`battery_w ≤ 0`) a současně je export, **108** zůstává **max** (přebytek do sítě řeší **142/145**, ne vynucení **108 = 0**). **TOU** z plánu. Reg. **142** = `deye_zero_export_mode`. Reg. **143** je tvrdý limit exportu z lokality/invertoru (ne forecast). Reg. **145** (solar sell): **0** při `export_ban` mimo SELL, jinak **1** — viz [`operating-modes.md`](operating-modes.md) (sekce *ZERO a zakázaný export*).
|
||||||
|
|
||||||
**SELF_SUSTAIN** zůstává **PASSIVE** v `get_deye_mode`; **108/109** jsou vždy **max z DB** (bez variant ZERO). Rozdíl je **`self_sustain_local_use=True`**: **TOU SOC** = **`min_soc_percent`**, `battery_w=None`.
|
**SELF_SUSTAIN** zůstává **PASSIVE** v `get_deye_mode`; **108/109** jsou vždy **max z DB** (bez variant ZERO). Rozdíl je **`self_sustain_local_use=True`**: **TOU SOC** = **`min_soc_percent`**, `battery_w=None`.
|
||||||
|
|
||||||
@@ -160,8 +160,8 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
|||||||
|
|
||||||
| Registr | Charge | PASSIVE (ZERO) | SELL | Self-consumption |
|
| Registr | Charge | PASSIVE (ZERO) | SELL | Self-consumption |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **108** (charge A) | škálo dle `battery_w` | max / **0** (export bez `battery_w>0`) / **>0** při `battery_w>0` i při exportu | **0** | dle varianty |
|
| **108** (charge A) | škálo dle `battery_w` | max / **0** (export bez nabíjení) / **max** při PASSIVE + `battery_w>0` (FVE do baterie až po strop) | **0** | dle varianty |
|
||||||
| **109** (discharge A) | **0** | max / **0** (import, držet bat.) / **0** při `battery_w>0` + export z PV | **max z DB** | dle varianty |
|
| **109** (discharge A) | **0** | max / **0** (import, držet bat.) / **max** při PASSIVE + `battery_w>0` | **max z DB** | dle varianty |
|
||||||
| **142** (limit control) | `deye_zero_export_mode` | `deye_zero_export_mode` | **0** (selling first) | `deye_zero_export_mode` |
|
| **142** (limit control) | `deye_zero_export_mode` | `deye_zero_export_mode` | **0** (selling first) | `deye_zero_export_mode` |
|
||||||
| **143** (export cap) | max z DB | max z DB | max z DB (tvrdý limit, bez forecast heuristiky) | max z DB |
|
| **143** (export cap) | max z DB | max z DB | max z DB (tvrdý limit, bez forecast heuristiky) | max z DB |
|
||||||
| **145** (solar sell) | 1 / 0 při `export_ban` | 1 / 0 při `export_ban` | 1 | 1 / 0 při `export_ban` |
|
| **145** (solar sell) | 1 / 0 při `export_ban` | 1 / 0 při `export_ban` | 1 | 1 / 0 při `export_ban` |
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
|
|||||||
|
|
||||||
| Reg | Název | Rozsah | Jednotka | Použití v EMS |
|
| Reg | Název | Rozsah | Jednotka | Použití v EMS |
|
||||||
|-----|-------|--------|----------|---------------|
|
|-----|-------|--------|----------|---------------|
|
||||||
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Strop **A** z DB (`COALESCE(deye_register_max_charge_a, odvod z kW)` + clamp **350 A**). Ve **PASSIVE** (AUTO) podle `_deye_zero_export_amps_for_passive`: výchozí **max**, u exportu v plánu bez vybíjení **0**. **CHARGE:** proud z `battery_w` přes `battery_watts_to_amps`. **SELL:** **0**. |
|
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Strop **A** z DB (`COALESCE(deye_register_max_charge_a, odvod z kW)` + clamp **350 A**). **PASSIVE** + plán chce nabíjet (`battery_w>0`): **108 = max** (špička FVE nesmí být omezená průměrem slotu). **PASSIVE** + export bez nabíjení: **0**. **CHARGE:** z `battery_w` přes `battery_watts_to_amps`. **SELL:** **0**. |
|
||||||
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Ve **PASSIVE** (AUTO): výchozí **max**, u importu bez nabíjení **0**; **SELL** max vybíjení; **CHARGE** typicky **0**. |
|
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Ve **PASSIVE** (AUTO): výchozí **max**, u importu bez nabíjení **0**; při **PASSIVE + `battery_w>0` + export** zůstává **max** (domácnost z baterie při výpadku PV). **SELL** max vybíjení; **CHARGE** typicky **0**. |
|
||||||
| 128 | Grid charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Nabíjení ze sítě. Firmware automaticky sníží reálný proud tak, aby `load + battery_charge` nepřekročil velikost jističe — proto LP v `planning_engine.py` plánuje `gi[t]` až **do `max_import_power_w + BMS_max_charge`**, aby uměl využít cenově nejlepší 15min okna pro nabíjení na plný BMS strop (viz `planning.md` sekce „Plánovací strop gi[t] vs. fyzický jistič"). Fyzické dodržení jističe drží reg 128 + firmware. |
|
| 128 | Grid charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Nabíjení ze sítě. Firmware automaticky sníží reálný proud tak, aby `load + battery_charge` nepřekročil velikost jističe — proto LP v `planning_engine.py` plánuje `gi[t]` až **do `max_import_power_w + BMS_max_charge`**, aby uměl využít cenově nejlepší 15min okna pro nabíjení na plný BMS strop (viz `planning.md` sekce „Plánovací strop gi[t] vs. fyzický jistič"). Fyzické dodržení jističe drží reg 128 + firmware. |
|
||||||
| 130 | Grid charge enable | 0/1 | — | 1 = povolit 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) |
|
| 141 | Energy mgmt mode | bitmask | — | EMS vždy **0** (neměnit jinak) |
|
||||||
| 142 | Limit control (System work mode) | 0/1/2 | — | **0** = selling first, **1** = zero export to load, **2** = zero export to CT. Hodnota v non-SELL režimech pochází z `asset_inverter.deye_zero_export_mode` (závisí na instalaci – viz tabulka níže). V režimu SELL vždy **0**. |
|
| 142 | Limit control (System work mode) | 0/1/2 | — | **0** = selling first, **1** = zero export to load, **2** = zero export to CT. Hodnota v non-SELL režimech pochází z `asset_inverter.deye_zero_export_mode` (závisí na instalaci – viz tabulka níže). V režimu SELL vždy **0**. |
|
||||||
| 145 | Solar sell | 0/1 | — | **0** = disabled (přebytek FVE na **straně měniče** se nesmí vést do sítě — curtailment vůči síti), **1** = enabled. Platí jen pro **FVE pod kontrolou Deye** (`controllable = true`); druhá pole (např. **pv-b** u home-01) EMS tímto registerem neřídí. EMS dnes **vždy zapisuje 1**; při 108 = 0 a 145 = 1 přebytky z řiditelného stringu typicky tečou do sítě (viz pass-through níže). |
|
| 145 | Solar sell | 0/1 | — | **0** = disabled (přebytek FVE na **straně měniče** se nesmí vést do sítě — curtailment vůči síti), **1** = enabled. Platí jen pro **FVE pod kontrolou Deye** (`controllable = true`); druhá pole (např. **pv-b** u home-01) EMS tímto registerem neřídí. EMS dnes **vždy zapisuje 1**; směr přebytku (baterie vs. síť) řeší energie management měniče a **142**, ne umělé **108 = 0** (viz pass-through níže). |
|
||||||
| 340 | Max solar power | 0 … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). EMS zapisuje jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (zelený bonus na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`. Hodnota z aktivního `planning_interval`: bez curtailmentu = cap; s `pv_a_curtailed_w > 0` = `clamp(0, cap, pv_a_forecast_solver_w − pv_a_curtailed_w)`. **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. |
|
| 340 | Max solar power | 0 … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). EMS zapisuje jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (zelený bonus na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`. Hodnota z aktivního `planning_interval`: bez curtailmentu = cap; s `pv_a_curtailed_w > 0` = `clamp(0, cap, pv_a_forecast_solver_w − pv_a_curtailed_w)`. **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. |
|
||||||
| 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`. EMS ji neodvozuje z forecastu ani z `grid_setpoint_w`; pro exportní sloty je to tvrdý site/inverter cap. |
|
| 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`. EMS ji neodvozuje z forecastu ani z `grid_setpoint_w`; pro exportní sloty je to tvrdý site/inverter cap. |
|
||||||
| 178 | Control board special 1 | bitmask | — | Bitové pole pro více funkcí. **EMS používá:** (a) bits **4–5** pro peak shaving switch: **32** (`0b00100000`, bit4–5 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit4–5 = **11**) v **PASSIVE/CHARGE**. (b) **BA81:** bits **0–1** pro „MI export to Grid cutoff“ (AC coupling / GEN): **2** = disable (cutoff OFF), **3** = enable (cutoff ON). EMS zapisuje jako **read-modify-write** (zachová ostatní bity). V některých manuálech/UI je to označené jako „register 179“ (1-based). |
|
| 178 | Control board special 1 | bitmask | — | Bitové pole pro více funkcí. **EMS používá:** (a) bits **4–5** pro peak shaving switch: **32** (`0b00100000`, bit4–5 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit4–5 = **11**) v **PASSIVE/CHARGE**. (b) **BA81:** bits **0–1** pro „MI export to Grid cutoff“ (AC coupling / GEN): **2** = disable (cutoff OFF), **3** = enable (cutoff ON). EMS zapisuje jako **read-modify-write** (zachová ostatní bity). V některých manuálech/UI je to označené jako „register 179“ (1-based). |
|
||||||
@@ -66,7 +66,7 @@ Vychází z **`grid_setpoint_w`** a **`battery_w`** z `ControlSetpoints` (aktivn
|
|||||||
|
|
||||||
Režim **CHARGE_CHEAP** nastaví oba setpointy na stejný kladný výkon (min. 1 W), aby byl výsledek **CHARGE**.
|
Režim **CHARGE_CHEAP** nastaví oba setpointy na stejný kladný výkon (min. 1 W), aby byl výsledek **CHARGE**.
|
||||||
|
|
||||||
**PASSIVE (ZERO):** reg. **108/109** podle `_deye_zero_export_amps_for_passive` — při exportu v plánu bez vybíjení je **108 = 0** (přetok FVE); při importu bez nabíjení je **109 = 0** (držet baterii). Jinak oba max (AUTO). Detail: `operating-modes.md`.
|
**PASSIVE (ZERO):** reg. **108/109** podle `_deye_zero_export_amps_for_passive` / `deye_battery_charge_discharge_amps` — **108** a **109** jsou typicky **max** z DB; výjimka jen **import bez nabíjení** (`109 = 0`). Export bez kladného `battery_w` už **108 nenuluje** (přebytek do sítě řeší režim / 142 / 145, ne falešné „baterie plná“). Detail: `operating-modes.md`.
|
||||||
|
|
||||||
### BA81: GEN port cut-off (reg 178 bits0–1) z plánu
|
### BA81: GEN port cut-off (reg 178 bits0–1) z plánu
|
||||||
|
|
||||||
@@ -108,12 +108,12 @@ Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_s
|
|||||||
|
|
||||||
**CHARGE:** TOU řádek nese **`max_soc_percent`** z DB (**clamp 10–100**) jako cíl při **grid charge** (spolu s příznakem grid charge v time pointu). **Energy pattern** („load first“ / „battery first“) v UI střídače zůstává v kompetenci instalace — EMS ho přes Modbus nenastavuje.
|
**CHARGE:** TOU řádek nese **`max_soc_percent`** z DB (**clamp 10–100**) jako cíl při **grid charge** (spolu s příznakem grid charge v time pointu). **Energy pattern** („load first“ / „battery first“) v UI střídače zůstává v kompetenci instalace — EMS ho přes Modbus nenastavuje.
|
||||||
|
|
||||||
**Jak funguje pass-through fyzicky:**
|
**Jak funguje pass-through (logicky):**
|
||||||
|
|
||||||
1. Reg 108 = 0 → baterie se fyzicky nemůže nabíjet (Deye ji považuje za „plnou")
|
1. **108 / 109** typicky **max** z invertoru — horní limity, ne příkaz „nabíjej / vybíjej“.
|
||||||
2. Reg 142 = 1/2 → zero export mode (Deye nebude aktivně prodávat z baterie)
|
2. Reg **142** = 1/2 → zero export to load / CT (instalace závislá).
|
||||||
3. Reg 145 = 1 → solar sell enabled: protože baterie je „plná" (108 = 0), PV přebytky tečou do sítě
|
3. Reg **145** = 1 → solar sell enabled; přebytek řiditelné FVE po zátěži a limitech směřuje do sítě podle firmware.
|
||||||
4. Reg 109 = max → pokud spotřeba překročí FVE, baterie může vybíjet (ochrana self-consumption)
|
4. Plán (`battery_w`, `grid_setpoint_w`) a **CHARGE** / **SELL** větev v `deye_battery_charge_discharge_amps` dál určují asymetrie (např. **CHARGE**: 109 = 0).
|
||||||
|
|
||||||
### `deye_zero_export_mode` per inverter
|
### `deye_zero_export_mode` per inverter
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- **Žádné wattové prahy pro výběr SELL / CHARGE** — mapování z MILP setpointů je čistě ze **znamének** `battery_setpoint_w` a `grid_setpoint_w` (viz `get_deye_mode` v `exporter_monolith.py`).
|
- **Žádné wattové prahy pro výběr SELL / CHARGE** — mapování z MILP setpointů je čistě ze **znamének** `battery_setpoint_w` a `grid_setpoint_w` (viz `get_deye_mode` v `exporter_monolith.py`).
|
||||||
- **Přetok FVE do sítě** se neodvozuje z forecastového capu: plán nese explicitní `export_limit_w` jako tvrdý limit lokality / invertoru, ne jako tipované maximum z předpovědi.
|
- **Přetok FVE do sítě** se neodvozuje z forecastového capu: plán nese explicitní `export_limit_w` jako tvrdý limit lokality / invertoru, ne jako tipované maximum z předpovědi.
|
||||||
- **ZERO (PASSIVE)** = zero export k CT/zátěži (**142** = `deye_zero_export_mode`), s **plnými proudy 108/109** jen ve výchozím stavu; pro přetok FVE do sítě nebo odběr ze sítě bez vybíjení baterie se jeden z proudů **vynuluje** (`_deye_zero_export_amps_for_passive`).
|
- **ZERO (PASSIVE)** = zero export k CT/zátěži (**142** = `deye_zero_export_mode`); **108/109** jsou typicky **max** z DB. Vynulování jen u **importu bez vybíjení** (`109 = 0` přes `_deye_zero_export_amps_for_passive`). Přetok FVE do sítě při exportu **108 nenuluje** — směr přebytku řeší **142/145** a plán, ne falešné „baterie plná“.
|
||||||
- **SELL** = plánovaný export **i** plánované vybíjení (oba záporné) → **142** = selling first, **178** = vypnutý grid peak shaving (32); po návratu do ZERO/CHARGE zase **178** = 48.
|
- **SELL** = plánovaný export **i** plánované vybíjení (oba záporné) → **142** = selling first, **178** = vypnutý grid peak shaving (32); po návratu do ZERO/CHARGE zase **178** = 48.
|
||||||
- Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`).
|
- Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`).
|
||||||
|
|
||||||
@@ -51,8 +51,7 @@ Všechny řádky předpokládají **142** = zero export (ne SELL).
|
|||||||
|
|
||||||
| Situace | Podmínka (plán) | Reg. 108 (max charge A) | Reg. 109 (max discharge A) |
|
| Situace | Podmínka (plán) | Reg. 108 (max charge A) | Reg. 109 (max discharge A) |
|
||||||
|---------|-----------------|-------------------------|----------------------------|
|
|---------|-----------------|-------------------------|----------------------------|
|
||||||
| Výchozí | ostatní případy PASSIVE | max | max |
|
| Výchozí | ostatní případy PASSIVE (včetně exportu / přetoku FVE) | max | max |
|
||||||
| Přetok FVE do sítě | `grid_setpoint_w` < 0 **a** `battery_w` ≥ 0 | **0** | max |
|
|
||||||
| Držet baterii, brát ze sítě | `grid_setpoint_w` > 0 **a** `battery_w` ≤ 0 | max | **0** |
|
| Držet baterii, brát ze sítě | `grid_setpoint_w` > 0 **a** `battery_w` ≤ 0 | max | **0** |
|
||||||
|
|
||||||
V obou exportních případech platí, že `export_limit_w` je **tvrdý site/inverter cap**. Nejde o forecastový odhad exportu, takže se může pustit plný přetok v rámci distribučního limitu.
|
V obou exportních případech platí, že `export_limit_w` je **tvrdý site/inverter cap**. Nejde o forecastový odhad exportu, takže se může pustit plný přetok v rámci distribučního limitu.
|
||||||
@@ -61,7 +60,7 @@ Nabíjení ze sítě s vysokým cílovým SoC v TOU řeší větev **CHARGE** (g
|
|||||||
|
|
||||||
### ZERO a „zakázaný export FVE do sítě“ (jen řiditelná pole)
|
### ZERO a „zakázaný export FVE do sítě“ (jen řiditelná pole)
|
||||||
|
|
||||||
**Reg. 145 (solar sell)** na Deye je přepínač typu **enabled / disabled** pro **přebytek FVE na straně měniče** (počítá se vůči režimu **142** zero export a stavu **108** — viz `modbus-registers.md`, pass-through krok za krokem).
|
**Reg. 145 (solar sell)** na Deye je přepínač typu **enabled / disabled** pro **přebytek FVE na straně měniče** (vůči režimu **142** zero export a interní logice měniče — viz `modbus-registers.md`, pass-through).
|
||||||
|
|
||||||
- **Pouze to, co EMS umí přes Deye Modbus ovlivnit** — typicky **FVE pole řízené střídačem** (`asset_pv_array.controllable = true`, u referenční lokality **home-01** např. string za Deye).
|
- **Pouze to, co EMS umí přes Deye Modbus ovlivnit** — typicky **FVE pole řízené střídačem** (`asset_pv_array.controllable = true`, u referenční lokality **home-01** např. string za Deye).
|
||||||
- **Pole mimo tento kanál** (např. **pv-b** u **home-01**, `controllable = false`, často samostatný ongrid GEN) **reg. 145 neovládá**; jejich výkon jde do plánovače jako `pv_b_forecast_w`, ale curtailment / solar sell logika Deye se jich netýká.
|
- **Pole mimo tento kanál** (např. **pv-b** u **home-01**, `controllable = false`, často samostatný ongrid GEN) **reg. 145 neovládá**; jejich výkon jde do plánovače jako `pv_b_forecast_w`, ale curtailment / solar sell logika Deye se jich netýká.
|
||||||
@@ -75,7 +74,12 @@ Týká se jen výroby, kterou Deye umí ovlivnit; **pv-b / ongrid GEN** u home-0
|
|||||||
|
|
||||||
#### PV1/PV2 vs. GEN port (důležité pro BLOCK_EXPORT)
|
#### PV1/PV2 vs. GEN port (důležité pro BLOCK_EXPORT)
|
||||||
|
|
||||||
- **PV1/PV2 (hlavní stringy na DC vstupu Deye)**: výkon je v režimu zero-export **řiditelný** (střídač umí výrobu stáhnout až k nule, pokud není odběr a baterie už nemůže nabíjet). Při BLOCK_EXPORT tedy dává smysl „zakázat export“ přes **reg 145 = 0** – Deye zamezí přetokům z těchto stringů.\n+- **GEN port (AC coupling / mikroinvertory / ongrid GEN)**: výkon **nelze plynule omezovat**. Pole vyrábí „co dá slunce“ a pokud ho **nespotřebuje dům + EV/TČ + baterie**, přebytek fyzicky teče do sítě.\n+ - U instalací typu **BA81** je proto k dispozici jen **tvrdý cut-off** (reg 179 bits0–1).\n+ - U **malé baterie** (např. BA81 ~6 kW max charge a navíc při vysokém SoC ještě méně) může při plném osvitu často nastat přebytek i při BLOCK_EXPORT – a bez cut-off by šel do sítě.\n+ - Naopak při **malém osvitu / velké spotřebě** jsou „každé watty z GEN“ užitečné (jít do domu/baterie) a cut-off by zbytečně zahodil výrobu.\n+
|
- **PV1/PV2 (hlavní stringy na DC vstupu Deye)**: výkon je v režimu zero-export **řiditelný** (střídač umí výrobu stáhnout až k nule, pokud není odběr a baterie už nemůže nabíjet). Při BLOCK_EXPORT tedy dává smysl „zakázat export“ přes **reg 145 = 0** – Deye zamezí přetokům z těchto stringů.
|
||||||
|
- **GEN port (AC coupling / mikroinvertory / ongrid GEN)**: výkon **nelze plynule omezovat**. Pole vyrábí „co dá slunce“ a pokud ho **nespotřebuje dům + EV/TČ + baterie**, přebytek fyzicky teče do sítě.
|
||||||
|
- U instalací typu **BA81** je proto k dispozici jen **tvrdý cut-off** (reg 179 bits0–1).
|
||||||
|
- U **malé baterie** (např. BA81 ~6 kW max charge a navíc při vysokém SoC ještě méně) může při plném osvitu často nastat přebytek i při BLOCK_EXPORT – a bez cut-off by šel do sítě.
|
||||||
|
- Naopak při **malém osvitu / velké spotřebě** jsou „každé watty z GEN“ užitečné (jít do domu/baterie) a cut-off by zbytečně zahodil výrobu.
|
||||||
|
|
||||||
Z toho plyne: **cut-off GEN portu je smysluplné řídit podle očekávaného přebytku**, ne jen podle „sell < 0“. Detail návrhu implementace je v `docs/04-modules/planning.md` (sekce o GEN portu a export banu).
|
Z toho plyne: **cut-off GEN portu je smysluplné řídit podle očekávaného přebytku**, ne jen podle „sell < 0“. Detail návrhu implementace je v `docs/04-modules/planning.md` (sekce o GEN portu a export banu).
|
||||||
|
|
||||||
**SELF_SUSTAIN:** `battery_w = None` ⇒ v `get_deye_mode` jako 0 ⇒ **PASSIVE**; v `write_inverter_setpoints` při `self_sustain_local_use=True` → **108 i 109 = max** (bez variant ZERO výše), reg. **142** dle DB, TOU SOC = **`min_soc_percent`**. **PRESERVE:** `lock_battery` → **108 = 0**, **109 = 0**.
|
**SELF_SUSTAIN:** `battery_w = None` ⇒ v `get_deye_mode` jako 0 ⇒ **PASSIVE**; v `write_inverter_setpoints` při `self_sustain_local_use=True` → **108 i 109 = max** (bez variant ZERO výše), reg. **142** dle DB, TOU SOC = **`min_soc_percent`**. **PRESERVE:** `lock_battery` → **108 = 0**, **109 = 0**.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Jak to je v implementaci:
|
|||||||
- `export_limit_w = hard cap`
|
- `export_limit_w = hard cap`
|
||||||
- `solar_sell = 1`
|
- `solar_sell = 1`
|
||||||
- `deye_physical_mode = PASSIVE`
|
- `deye_physical_mode = PASSIVE`
|
||||||
- v PASSIVE se pro exportní slot typicky použije `108 = 0`, `109 = max`
|
- v PASSIVE se pro exportní slot (bez plánovaného nabíjení z baterie) používají typicky **`108` i `109` na max** z invertoru; přebytek do sítě řeší **142/145** a firmware, ne umělé **108 = 0** (to dřív matlo měnič jako „baterie plná“)
|
||||||
|
|
||||||
Poznámka:
|
Poznámka:
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ Jak to je v implementaci:
|
|||||||
- `deye_physical_mode = SELL`
|
- `deye_physical_mode = SELL`
|
||||||
- `reg 142 = 0`
|
- `reg 142 = 0`
|
||||||
- `reg 178 = 32`
|
- `reg 178 = 32`
|
||||||
- `reg 109` na max, `reg 108 = 0`
|
- `reg 109` na max, `reg 108 = 0` (jen ve fyzickém **SELL** — aktivní výdej baterie do sítě; u **PASSIVE** + přetoku FVE už **108** typicky **max**)
|
||||||
|
|
||||||
## 4. Další režimy, které v praxi existují
|
## 4. Další režimy, které v praxi existují
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user