prekopani SELL
This commit is contained in:
@@ -92,7 +92,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`.
|
||||
|
||||
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žimy střídače jsou tři:** **PASSIVE**, **SELL**, **CHARGE** (mapování z plánu / politik EMS v `control_exporter.get_deye_mode`). Reg **108** (charge A) se řídí záměrem solveru: **max** při nabíjení (`bat_w > 0`), **0** jinak (pass-through, self-consumption). Reg **109** (discharge A) vždy **max z DB** (**výjimka PRESERVE:** `lock_battery=True` → **0 / 0**). **Řízení:** time points – blok **1** = začátek **aktuálního** 15min slotu + plán pro tento slot, blok **2** = začátek **následujícího** slotu + plán pro něj (`current_slot_hhmm` / `next_slot_hhmm`); bloky **3–6** neaktivní **2355** (ne 23:59 kvůli firmware), zápis **nejednou častěji než 1× denně** (Europe/Prague) + při změně podpisu (`deye_tou_inactive_signature`: `HHMM|min_soc|reserve_soc|tp_discharge_w`, V028 meta + V029 komentář); **reg 166+** u TP: **SELL** = `reserve_soc_percent`, **PASSIVE** / řádky **3–6** = `min_soc_percent`. **108** / **109** / **141** (0) / **142** (`deye_zero_export_mode` z DB: 1 = to load / 2 = to CT v non-SELL; 0 = selling first ve **SELL**) / **178** (pevně **32** ve **SELL**, **48** v **PASSIVE** a **CHARGE** – bez read-modify-write) / **143** (export limit W z DB) / **145** (solar sell, vždy **1** = enabled) z **aktuálního** setpointu. **Reg 191** EMS **nezapisuje**. **Čas 62–64:** před zařazením do fronty **čtení** 62–64; zápis jen při driftu **> 60 s**, nebo **NULL** `deye_last_system_time_sync_at`, nebo uplynulých **24 h** od posledního syncu; `deye_last_system_time_sync_at` / `deye_last_system_time_sync_minute` po **úspěšném zápisu** 62–64 a znovu po **úspěšné toleranční verifikaci**; při chybě čtení se čas zapisuje; reg **64** se zapisuje s **sekundami 0**; verify **vždy** čte 62–64 najednou — **reg 64 nesmí** do striktní větve; toleranční odchylka až **120 s**; po 3 neúspěších u hodin **bez** SELF_SUSTAIN (jen Discord). **SELL:** `battery_w` < −500 a `grid_setpoint_w` < −200 (aktivní vybíjení baterie pro export). **CHARGE:** `battery_w` > 500 a `grid_setpoint_w` > 200. **PASSIVE:** ostatní (včetně pass-through s reg 108=0, self-consumption, `battery_w=None` u SELF_SUSTAIN). **Čtyři typy slotů:** Charge (108=max), Pass-through (108=0, PV→síť), Discharge-export (SELL, 142=0), Self-consumption (108=0, noc). Reg 109 vždy max kromě PRESERVE. Detail: `docs/04-modules/modbus-registers.md`, režimy: `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. **Režimy:** `get_deye_mode` → **SELL** jen při **\|battery_w\| ≥ \|grid_setpoint_w\|** a obou záporných (záměr výdeje baterie do sítě); **CHARGE** při `battery_w` > 500 a `grid_setpoint_w` > 200; jinak **PASSIVE**. **PASSIVE:** reg. **108/109** škálované podle `battery_w` z plánu (ne vždy max); **SELL:** 108=0, 109=max, **143** omezeno podle `|grid_setpoint_w|`; **142/178/145/TOU** jako v `write_inverter_setpoints`. **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 princip *Keep it simple* u režimů **`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).
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ DEYE_CLOCK_RESYNC_INTERVAL_HOURS = 24
|
||||
# Deye LV baterie: převod výkon → proud pro registry 108/109 (viz docs/04-modules/modbus-registers.md)
|
||||
BATT_VOLTAGE_V = 51.2
|
||||
|
||||
# Reg 143 ve SELL: strop exportu = min(site max, |grid_setpoint|), dolní podlahová konstanta kvůli nule.
|
||||
REG143_SELL_CAP_MIN_W = 200
|
||||
|
||||
# Reg 178 – pevné hodnoty (bit4–5); bez read-modify-write (kolize s Loxone / transaction ID)
|
||||
REG178_SELL = 0b00100000 # 32, grid peak shaving disable
|
||||
REG178_PASSIVE = 0b00110000 # 48, grid peak shaving enable (PASSIVE i CHARGE)
|
||||
@@ -1374,18 +1377,30 @@ def get_deye_mode(setpoints: ControlSetpoints) -> str:
|
||||
"""
|
||||
Fyzický režim Deye: SELL | CHARGE | PASSIVE.
|
||||
|
||||
SELL only when battery actively discharges for grid export (bat_w < -500
|
||||
AND grid_w < -200). Pass-through (PV → grid, battery idle) stays PASSIVE
|
||||
with reg 108 = 0 + reg 145 = 1 (solar sell).
|
||||
battery_w=None (SELF_SUSTAIN) → bat_w considered 0 → PASSIVE; při exportu se ale
|
||||
zapíše plný reg. 108/109 (viz ``self_sustain_local_use`` v ``write_inverter_setpoints``).
|
||||
Pravidlo držet jednoduché (viz ``docs/04-modules/operating-modes.md``):
|
||||
|
||||
- **SELL** — jen když plán explicitně počítá s **vybíjením baterie do sítě** ve smyslu:
|
||||
záporný export z portu sítě a zároveň **|battery_w| ≥ |grid_setpoint_w|** (výdej z
|
||||
baterie není menší než plánovaný čistý export). Pak Deye „selling first“ (reg. 142=0).
|
||||
|
||||
- **PASSIVE** — všude jinde: přetok FVE do sítě / do baterie řeší reg. **108** (nabíjení),
|
||||
**109** (vybíjení, škálované podle plánu), **142** dle ``deye_zero_export_mode`` instalace,
|
||||
**145** solar sell. Žádné odhady PV vs. load v této funkci.
|
||||
|
||||
- **CHARGE** — nabíjení ze sítě (``battery_w`` > 500 a ``grid_setpoint_w`` > 200).
|
||||
|
||||
``battery_w=None`` (SELF_SUSTAIN) → bat_w 0 → PASSIVE zde; plné 108/109 řeší
|
||||
``self_sustain_local_use`` v ``write_inverter_setpoints``.
|
||||
"""
|
||||
grid_w = int(setpoints.grid_setpoint_w or 0)
|
||||
bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w)
|
||||
if bat_w < -500 and grid_w < -200:
|
||||
return "SELL"
|
||||
|
||||
if bat_w > 500 and grid_w > 200:
|
||||
return "CHARGE"
|
||||
|
||||
if grid_w < 0 and bat_w < 0 and abs(bat_w) >= abs(grid_w):
|
||||
return "SELL"
|
||||
|
||||
return "PASSIVE"
|
||||
|
||||
|
||||
@@ -1451,26 +1466,40 @@ async def write_inverter_setpoints(
|
||||
elif deye_mode == "CHARGE":
|
||||
charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a)
|
||||
discharge_a = 0
|
||||
elif deye_mode == "SELL":
|
||||
# Záměrný výdej baterie do sítě: plný vybíjecí proud; export strop dle plánu níže.
|
||||
charge_a = 0
|
||||
discharge_a = int(inv.max_discharge_a)
|
||||
elif setpoints_now.self_sustain_local_use:
|
||||
# SELF_SUSTAIN: plný nabíjecí i vybíjecí proud invertoru — přebytek FVE jde do baterie,
|
||||
# reg. 142 = zero export to load/CT (viz selling_mode níže), ne reg. 108 = 0.
|
||||
charge_a = int(inv.max_charge_a)
|
||||
discharge_a = int(inv.max_discharge_a)
|
||||
else:
|
||||
charge_a = int(inv.max_charge_a) if bat_w > 0 else 0
|
||||
discharge_a = int(inv.max_discharge_a)
|
||||
# PASSIVE: škáluj 108/109 podle |battery_w| — ne plný max při malém setpointu z LP.
|
||||
if bat_w > 0:
|
||||
charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a)
|
||||
discharge_a = 0
|
||||
elif bat_w < 0:
|
||||
charge_a = 0
|
||||
discharge_a = battery_watts_to_amps(bat_w, inv.max_discharge_a)
|
||||
else:
|
||||
charge_a = 0
|
||||
discharge_a = 0
|
||||
|
||||
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
|
||||
selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
|
||||
solar_sell = 1
|
||||
export_limit = export_lim
|
||||
if deye_mode == "SELL" and grid_w < -200:
|
||||
export_limit = min(export_lim, max(REG143_SELL_CAP_MIN_W, abs(grid_w)))
|
||||
reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE
|
||||
|
||||
logger.info(
|
||||
f"[control] site={site_id} fyzický režim Deye: {deye_mode} | "
|
||||
f"battery_w={raw_bat!r} grid_w={grid_w} | "
|
||||
f"charge_a={charge_a} discharge_a={discharge_a} | "
|
||||
f"reg142={selling_mode} reg145={solar_sell} reg178={reg178_val}"
|
||||
f"reg142={selling_mode} reg145={solar_sell} reg143={export_limit}W reg178={reg178_val}"
|
||||
)
|
||||
|
||||
now_cet, time_rows = _deye_system_time_register_rows()
|
||||
|
||||
@@ -62,13 +62,14 @@ class ModbusVerifyPolicyTests(unittest.TestCase):
|
||||
|
||||
class DeyeTouParamsTests(unittest.TestCase):
|
||||
def test_sell_uses_reserve_soc(self) -> None:
|
||||
"""SELL: plánovaný výdej baterie alesvěň tak velký jako plánovaný export (|bat| ≥ |grid|)."""
|
||||
sp = ControlSetpoints(
|
||||
battery_w=-600,
|
||||
grid_export_limit=5000,
|
||||
battery_w=-8000,
|
||||
grid_export_limit=8000,
|
||||
ev1_current_a=0,
|
||||
ev2_current_a=0,
|
||||
heat_pump_enable=False,
|
||||
grid_setpoint_w=-500,
|
||||
grid_setpoint_w=-8000,
|
||||
ev1_power_w=0,
|
||||
ev2_power_w=0,
|
||||
target_soc_pct=50,
|
||||
@@ -78,6 +79,36 @@ class DeyeTouParamsTests(unittest.TestCase):
|
||||
self.assertFalse(g)
|
||||
self.assertEqual(s, 20)
|
||||
|
||||
def test_pv_led_export_with_small_battery_is_passive(self) -> None:
|
||||
"""Regrese site 25A 17:30: |bat| < |grid| → PASSIVE (FVE přetok, ne „vylít baterku“)."""
|
||||
sp = ControlSetpoints(
|
||||
battery_w=-733,
|
||||
grid_export_limit=1294,
|
||||
ev1_current_a=0,
|
||||
ev2_current_a=0,
|
||||
heat_pump_enable=False,
|
||||
grid_setpoint_w=-1294,
|
||||
ev1_power_w=0,
|
||||
ev2_power_w=0,
|
||||
target_soc_pct=50,
|
||||
)
|
||||
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
||||
|
||||
def test_large_export_small_battery_is_passive(self) -> None:
|
||||
"""Export v plánu větší než výdej z baterie → PASSIVE."""
|
||||
sp = ControlSetpoints(
|
||||
battery_w=-1500,
|
||||
grid_export_limit=8000,
|
||||
ev1_current_a=0,
|
||||
ev2_current_a=0,
|
||||
heat_pump_enable=False,
|
||||
grid_setpoint_w=-8000,
|
||||
ev1_power_w=0,
|
||||
ev2_power_w=0,
|
||||
target_soc_pct=50,
|
||||
)
|
||||
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
||||
|
||||
def test_passive_uses_min_soc(self) -> None:
|
||||
sp = ControlSetpoints(
|
||||
battery_w=0,
|
||||
|
||||
@@ -109,27 +109,28 @@ def apply_overrides(plan, overrides) -> Setpoints:
|
||||
|
||||
## Zápis do Deye (Modbus)
|
||||
|
||||
### Fyzický režim (`get_deye_mode`)
|
||||
**Princip:** držet mapování plán → Deye **jednoduché**; detail a zdůvodnění v [`operating-modes.md`](operating-modes.md) (sekce *Keep it simple*).
|
||||
|
||||
Solver rozlišuje **čtyři typy slotů**: **Charge**, **Pass-through**, **Discharge-export**, **Self-consumption**. Na úrovni Deye se mapují na tři fyzické režimy:
|
||||
### Fyzický režim (`get_deye_mode`)
|
||||
|
||||
| Fyzický režim | Podmínka z `ControlSetpoints` |
|
||||
|---|---|
|
||||
| **SELL** | `battery_w` < −500 **a** `grid_setpoint_w` < −200 (záměrné vybíjení baterie do sítě) |
|
||||
| **SELL** | `grid_setpoint_w` < 0 **a** `battery_w` < 0 **a** **\|battery_w\| ≥ \|grid_setpoint_w\|** — plán počítá s výdejem z baterie do sítě alesvěň tak velkým jako plánovaný čistý export. |
|
||||
| **CHARGE** | `battery_w` > 500 **a** `grid_setpoint_w` > 200 (nabíjení ze sítě) |
|
||||
| **PASSIVE** | vše ostatní (pass-through, self-consumption, SELF_SUSTAIN) |
|
||||
| **PASSIVE** | vše ostatní |
|
||||
|
||||
**Pass-through** (PV → síť, baterie idle) zůstává **PASSIVE** — fyzicky se realizuje nastavením reg 108 = 0 (zákaz nabíjení) + reg 145 = 1 (solar sell), takže PV přebytky tečou do sítě.
|
||||
**PASSIVE** (včetně FVE přetoku do sítě): reg. **108/109** škálované podle `battery_w` z plánu, reg. **145** = 1 (solar sell), reg. **142** = `deye_zero_export_mode`.
|
||||
|
||||
**SELF_SUSTAIN** (záložní režim po Modbus mismatch apod.) zůstává **PASSIVE** z hlediska `get_deye_mode`, ale `write_inverter_setpoints` nastaví **reg. 108 i 109 na maximum z DB** (`self_sustain_local_use=True` v `ControlSetpoints`), **reg. 142** na `asset_inverter.deye_zero_export_mode` (1 = zero export to load, 2 = zero export to CT) a **TOU SOC** na **`min_soc_percent`** (typicky 12 %), aby střídač maximalizoval využití baterie lokálně místo zákazu nabíjení při `battery_w=None`.
|
||||
|
||||
### Klíčové registry podle typu slotu
|
||||
|
||||
| Registr | Charge | Pass-through | Discharge-export | Self-consumption |
|
||||
| Registr | Charge | Pass-through / PASSIVE | SELL (battery-led) | Self-consumption |
|
||||
|---|---|---|---|---|
|
||||
| **108** (charge A) | max z DB | **0** | 0 | **0** |
|
||||
| **109** (discharge A) | max | max | max | max |
|
||||
| **108** (charge A) | škálo dle `battery_w` | škálo dle `battery_w` (>0) / **0** | **0** | **0** |
|
||||
| **109** (discharge A) | **0** | škálo dle `\|battery_w\|` (<0) / **0** | **max z DB** | škálo dle `\|battery_w\|` |
|
||||
| **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 | `min(max_site, max(200, \|grid_setpoint_w\|))` | max z DB |
|
||||
| **145** (solar sell) | 1 | 1 | 1 | 1 |
|
||||
| **178** (peak shaving) | 48 | 48 | **32** | 48 |
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ Vychází z **`grid_setpoint_w`** a **`battery_w`** z `ControlSetpoints` (aktivn
|
||||
|
||||
| Režim | Podmínka |
|
||||
|-------|----------|
|
||||
| **SELL** | `battery_w` < −500 **a** `grid_setpoint_w` < −200 (aktivní vybíjení baterie pro export) |
|
||||
| **SELL** | `grid_setpoint_w` < 0 **a** `battery_w` < 0 **a** **\|battery_w\| ≥ \|grid_setpoint_w\|** (výdej z baterie alesvěň tak velký jako plánovaný export) |
|
||||
| **CHARGE** | `battery_w` > 500 **a** `grid_setpoint_w` > 200 |
|
||||
| **PASSIVE** | vše ostatní (včetně pass-through, self-consumption, 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.
|
||||
|
||||
**Důležité:** SELL se aktivuje **pouze** při záměrném vybíjení baterie do sítě (`bat_w < −500`). Pass-through (PV → síť, baterie idle) zůstává v PASSIVE s reg 108 = 0.
|
||||
**Důležité:** **SELL** jen pro záměr **vylít baterku do sítě** (viz `operating-modes.md`, *Keep it simple*). FVE přetok / malý doplněk z baterie vůči většímu exportu zůstává **PASSIVE** (reg. **108/109** škálované podle plánu).
|
||||
|
||||
### Provozní režim EMS SELF_SUSTAIN
|
||||
|
||||
@@ -70,12 +70,12 @@ Z hlediska `get_deye_mode` je **SELF_SUSTAIN** stále **PASSIVE** (`battery_w` z
|
||||
|
||||
Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_slots`, `_select_discharge_export_slots`). Nabíjení: vždy povoleno v slotech s PV-surplus; zbytek rozpočtu (`charge_slot_buffer × (soc_max − current_soc) − PV přínos`) doplněn nejlevnějšími sloty podle **`buy_price`** (nákupní cena ze sítě). Export-vybíjení: top-N slotů podle nejvyšší **`sell_price`**. Výsledné setpointy pak určují typ slotu:
|
||||
|
||||
| | **Charge** | **Pass-through** | **Discharge-export** | **Self-consumption** |
|
||||
| | **Charge** | **Pass-through** | **Battery→grid (SELL)** | **Self-consumption** |
|
||||
|---|---|---|---|---|
|
||||
| **Kdy** | Solver: `bat_w > 0` | Solver: `bat_w == 0`, PV > spotřeba | Solver: `bat_w < −500`, `grid_w < −200` | Noc / PV < spotřeba |
|
||||
| **Kdy** | Solver: `bat_w > 0` | Solver: typicky export z FVE; `\|bat\| < \|grid\|` při exportu | `grid_w < 0`, `bat_w < 0`, `\|bat\| ≥ \|grid\|` | Noc / PV < spotřeba |
|
||||
| **Deye mode** | PASSIVE | PASSIVE | SELL | PASSIVE |
|
||||
| **108** charge A | **max** (z DB) | **0** | 0 | **0** |
|
||||
| **109** discharge A | max | **max** | **max** | **max** |
|
||||
| **108** charge A | škálo dle `bat_w` | škálo / **0** | **0** | **0** |
|
||||
| **109** discharge A | **0** | škálo dle `\|bat_w\|` | **max** | škálo dle `\|bat_w\|` |
|
||||
| **142** limit control | `deye_zero_export_mode` (1 nebo 2) | `deye_zero_export_mode` (1 nebo 2) | **0** (selling first) | `deye_zero_export_mode` (1 nebo 2) |
|
||||
| **145** solar sell | **1** (enabled) | **1** (enabled) | **1** (enabled) | **1** (enabled) |
|
||||
| **178** peak shaving | 48 (PASSIVE) | 48 (PASSIVE) | **32** (SELL) | 48 (PASSIVE) |
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Provozní režimy EMS
|
||||
|
||||
## Keep it simple
|
||||
|
||||
- **Méně heuristik a pevných wattových práhů v řízení** — každá magická konstanta je místo, kde se rodí neshody s plánem a ekonomikou.
|
||||
- **SELL na Deye** používej jen tam, kde produktově opravdu chceme režim „**vylít baterii do sítě**“ (selling first). Vše ostatní patří do **PASSIVE** a tok FVE ↔ baterie ↔ síť se řeší **reg. 108 / 109 / 142** podle plánu a instalace.
|
||||
- Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`) a typické chybě „plán říká kW, měnič jede na MW“.
|
||||
|
||||
## Přehled
|
||||
|
||||
| Mode | Solver constraints | Deye fyzický režim | Baterie |
|
||||
@@ -14,13 +20,13 @@ Implementace: omezení LP v `planning_engine.solve_dispatch()` podle `mode_code`
|
||||
|
||||
## Fyzické režimy Deye (výstup control_exporteru)
|
||||
|
||||
Detekce z `battery_w` a `grid_setpoint_w` (`get_deye_mode`):
|
||||
Detekce v `get_deye_mode` (`battery_w` = `battery_setpoint_w` z plánu, záporné = vybíjení; `grid_setpoint_w` záporné = export do sítě):
|
||||
|
||||
- **PASSIVE:** `grid_setpoint_w >= -200` → reg **142** = `deye_zero_export_mode`, reg **178** = 48; **108** = max jen při plánovaném nabíjení (`battery_w` > 0), jinak typicky **108 = 0** a **109** = max — **výjimka SELF_SUSTAIN:** příznak `self_sustain_local_use` → **108 i 109 = max** (viz `control_exporter.py`). **0/0** jen při `lock_battery` (PRESERVE).
|
||||
- **SELL:** `grid_setpoint_w < -200` → reg142=0, reg178=32, 108/109=max
|
||||
- **CHARGE:** `grid_setpoint_w > 200` **a** `battery_w > 500` → reg142=1, reg178=48
|
||||
- **CHARGE:** `battery_w` > 500 **a** `grid_setpoint_w` > 200 → nabíjení ze sítě; reg. **142** dle CHARGE větve v exporteru, **178** = 48.
|
||||
- **SELL:** `grid_setpoint_w` < 0 **a** `battery_w` < 0 **a** `|battery_w| ≥ |grid_setpoint_w|` → záměr **vybíjet baterii do sítě** (selling first); reg. **142** = 0, **178** = 32, **108** = 0, **109** = max, reg. **143** omezen podle `|grid_setpoint_w|` (viz `control_exporter.py`).
|
||||
- **PASSIVE:** vše ostatní — tok FVE a baterie řídí **reg. 108** (nabíjení) a **109** (vybíjení, škálované podle `|battery_w|`), **142** = `deye_zero_export_mode`, **145** = 1, **178** = 48.
|
||||
|
||||
`battery_w = None` a `self_sustain_local_use=True` (**SELF_SUSTAIN**) ⇒ pro `get_deye_mode` se bere jako 0 ⇒ při `grid_setpoint_w = 0` je **PASSIVE**; v `write_inverter_setpoints` se ale zapíše **108 = max** a **109 = max**, reg **142** = zero export dle DB, TOU SOC = **`min_soc_percent`** (ne ekonomická větev 100 % z ceny).
|
||||
**SELF_SUSTAIN:** `battery_w = None` ⇒ v `get_deye_mode` jako 0 ⇒ typicky **PASSIVE**; v `write_inverter_setpoints` při `self_sustain_local_use=True` → **108 i 109 = max**, reg. **142** = zero export dle DB, TOU SOC = **`min_soc_percent`**. **PRESERVE:** `lock_battery` → **108 = 0**, **109 = 0**.
|
||||
|
||||
## EMS politiky (nejsou fyzické stavy Deye)
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Flyway validate using migration files from repo root; JDBC from env (staging / CI DB).
|
||||
# Env: EMS_CI_FLYWAY_URL — if unset, skips (warn). Optional: EMS_CI_FLYWAY_USER, EMS_CI_FLYWAY_PASSWORD, FLYWAY_IMAGE
|
||||
#
|
||||
# CI notes:
|
||||
# - Flyway runs in a throwaway container; without --network host, jdbc:…//localhost… hits the
|
||||
# container loopback, not the host (connection refused).
|
||||
# - Some runners set missing secrets to the literal string "null"; passing FLYWAY_USER=null
|
||||
# overrides credentials embedded in the JDBC URL and yields "user 'null'" from the driver.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${EMS_CI_FLYWAY_URL:-}" ]]; then
|
||||
@@ -8,6 +14,17 @@ if [[ -z "${EMS_CI_FLYWAY_URL:-}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Treat empty / JSON-null placeholders as unset so we do not override URL credentials.
|
||||
_ci_sanitize_secret() {
|
||||
local v="${1-}"
|
||||
v="${v#"${v%%[![:space:]]*}"}"
|
||||
v="${v%"${v##*[![:space:]]}"}"
|
||||
case "${v,,}" in ''|'null'|'<null>') printf '%s' '' ;; *) printf '%s' "$v" ;; esac
|
||||
}
|
||||
|
||||
EMS_CI_FLYWAY_USER="$(_ci_sanitize_secret "${EMS_CI_FLYWAY_USER-}")"
|
||||
EMS_CI_FLYWAY_PASSWORD="$(_ci_sanitize_secret "${EMS_CI_FLYWAY_PASSWORD-}")"
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
@@ -15,6 +32,7 @@ IMG="${FLYWAY_IMAGE:-flyway/flyway:12}"
|
||||
|
||||
args=(
|
||||
run --rm
|
||||
--network host
|
||||
-v "$ROOT/db/migration:/flyway/sql/migration"
|
||||
-v "$ROOT/db/routines:/flyway/sql/routines"
|
||||
-v "$ROOT/db/views:/flyway/sql/views"
|
||||
@@ -23,10 +41,10 @@ args=(
|
||||
-e "FLYWAY_LOCATIONS=filesystem:/flyway/sql/migration,filesystem:/flyway/sql/routines,filesystem:/flyway/sql/views"
|
||||
)
|
||||
|
||||
if [[ -n "${EMS_CI_FLYWAY_USER:-}" ]]; then
|
||||
if [[ -n "$EMS_CI_FLYWAY_USER" ]]; then
|
||||
args+=(-e "FLYWAY_USER=${EMS_CI_FLYWAY_USER}")
|
||||
fi
|
||||
if [[ -n "${EMS_CI_FLYWAY_PASSWORD:-}" ]]; then
|
||||
if [[ -n "$EMS_CI_FLYWAY_PASSWORD" ]]; then
|
||||
args+=(-e "FLYWAY_PASSWORD=${EMS_CI_FLYWAY_PASSWORD}")
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user