refactor export limit semantics
This commit is contained in:
@@ -19,9 +19,6 @@ DEYE_CLOCK_RESYNC_INTERVAL_HOURS = 24
|
|||||||
# Deye LV baterie: převod výkon -> proud pro registry 108/109.
|
# Deye LV baterie: převod výkon -> proud pro registry 108/109.
|
||||||
BATT_VOLTAGE_V = 51.2
|
BATT_VOLTAGE_V = 51.2
|
||||||
|
|
||||||
# Reg 143 ve SELL: min(|grid_setpoint_w|, ...) nesmí klesnout pod tuto podlahu (W).
|
|
||||||
REG143_SELL_CAP_MIN_W = 200
|
|
||||||
|
|
||||||
# Reg 178 - bitové pole: bity 4-5 (peak shaving switch) a bity 0-1 (MI export cutoff).
|
# Reg 178 - bitové pole: bity 4-5 (peak shaving switch) a bity 0-1 (MI export cutoff).
|
||||||
REG178_SELL = 0b00100000
|
REG178_SELL = 0b00100000
|
||||||
REG178_PASSIVE = 0b00110000
|
REG178_PASSIVE = 0b00110000
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from services.control.deye_helpers import (
|
|||||||
DEYE_TOU_INACTIVE_HHMM,
|
DEYE_TOU_INACTIVE_HHMM,
|
||||||
DEYE_TOU_POWER_REGS,
|
DEYE_TOU_POWER_REGS,
|
||||||
PRAGUE_TZ,
|
PRAGUE_TZ,
|
||||||
REG143_SELL_CAP_MIN_W,
|
|
||||||
REG178_MI_EXPORT_DISABLE,
|
REG178_MI_EXPORT_DISABLE,
|
||||||
REG178_MI_EXPORT_ENABLE,
|
REG178_MI_EXPORT_ENABLE,
|
||||||
REG178_MI_EXPORT_MASK,
|
REG178_MI_EXPORT_MASK,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from services.control.deye_helpers import (
|
|||||||
DEYE_CLOCK_RESYNC_INTERVAL_HOURS,
|
DEYE_CLOCK_RESYNC_INTERVAL_HOURS,
|
||||||
DEYE_TOU_INACTIVE_HHMM,
|
DEYE_TOU_INACTIVE_HHMM,
|
||||||
PRAGUE_TZ,
|
PRAGUE_TZ,
|
||||||
REG143_SELL_CAP_MIN_W,
|
|
||||||
REG178_MI_EXPORT_DISABLE,
|
REG178_MI_EXPORT_DISABLE,
|
||||||
REG178_MI_EXPORT_ENABLE,
|
REG178_MI_EXPORT_ENABLE,
|
||||||
REG178_MI_EXPORT_MASK,
|
REG178_MI_EXPORT_MASK,
|
||||||
@@ -103,8 +102,6 @@ async def write_inverter_setpoints(
|
|||||||
selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
|
selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
|
||||||
solar_sell = 0 if (setpoints_now.export_ban and deye_mode != "SELL") else 1
|
solar_sell = 0 if (setpoints_now.export_ban and deye_mode != "SELL") else 1
|
||||||
export_limit = export_lim
|
export_limit = export_lim
|
||||||
if deye_mode == "SELL" and grid_w < 0:
|
|
||||||
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
|
reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class InverterConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ControlSetpoints:
|
class ControlSetpoints:
|
||||||
battery_w: int | None
|
battery_w: int | None
|
||||||
|
#: Tvrdý limit exportu do sítě v daném slotu (W), ne forecastová cílová hodnota.
|
||||||
grid_export_limit: int
|
grid_export_limit: int
|
||||||
ev1_current_a: int
|
ev1_current_a: int
|
||||||
ev2_current_a: int
|
ev2_current_a: int
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ def _build_setpoints(
|
|||||||
if pi is None:
|
if pi is None:
|
||||||
return None
|
return None
|
||||||
grid_sp = int(pi["grid_setpoint_w"] or 0)
|
grid_sp = int(pi["grid_setpoint_w"] or 0)
|
||||||
|
export_limit_raw = pi.get("export_limit_w")
|
||||||
|
export_limit = int(export_limit_raw) if export_limit_raw is not None else abs(min(grid_sp, 0))
|
||||||
ev1_w = int(pi["ev1_setpoint_w"] or 0) if "ev1_setpoint_w" in pi else 0
|
ev1_w = int(pi["ev1_setpoint_w"] or 0) if "ev1_setpoint_w" in pi else 0
|
||||||
ev2_w = int(pi["ev2_setpoint_w"] or 0) if "ev2_setpoint_w" in pi else 0
|
ev2_w = int(pi["ev2_setpoint_w"] or 0) if "ev2_setpoint_w" in pi else 0
|
||||||
hp_en = bool(pi["heat_pump_enabled"])
|
hp_en = bool(pi["heat_pump_enabled"])
|
||||||
@@ -90,6 +92,10 @@ def _build_setpoints(
|
|||||||
pm: str | None = str(pm_raw).strip().upper() if pm_raw is not None else None
|
pm: str | None = str(pm_raw).strip().upper() if pm_raw is not None else None
|
||||||
sell_raw = pi.get("effective_sell_price")
|
sell_raw = pi.get("effective_sell_price")
|
||||||
sell_f: float | None = float(sell_raw) if sell_raw is not None else None
|
sell_f: float | None = float(sell_raw) if sell_raw is not None else None
|
||||||
|
export_mode_raw = pi.get("export_mode")
|
||||||
|
export_mode = str(export_mode_raw).strip().upper() if export_mode_raw is not None else None
|
||||||
|
if export_mode == "NONE":
|
||||||
|
export_limit = 0
|
||||||
# Záporný výkup sám o sobě neblokuje export, pokud plán export explicitně žádá.
|
# Záporný výkup sám o sobě neblokuje export, pokud plán export explicitně žádá.
|
||||||
export_ban = sell_f is not None and float(sell_f) < 0 and grid_sp >= 0
|
export_ban = sell_f is not None and float(sell_f) < 0 and grid_sp >= 0
|
||||||
gen_cutoff_raw = pi.get("deye_gen_cutoff_enabled")
|
gen_cutoff_raw = pi.get("deye_gen_cutoff_enabled")
|
||||||
@@ -112,7 +118,7 @@ def _build_setpoints(
|
|||||||
pv_a_allowed = 0
|
pv_a_allowed = 0
|
||||||
return ControlSetpoints(
|
return ControlSetpoints(
|
||||||
battery_w=int(pi["battery_setpoint_w"] or 0),
|
battery_w=int(pi["battery_setpoint_w"] or 0),
|
||||||
grid_export_limit=abs(min(grid_sp, 0)),
|
grid_export_limit=max(0, export_limit),
|
||||||
ev1_current_a=watts_to_amps(ev1_w, phases=3),
|
ev1_current_a=watts_to_amps(ev1_w, phases=3),
|
||||||
ev2_current_a=watts_to_amps(ev2_w, phases=1),
|
ev2_current_a=watts_to_amps(ev2_w, phases=1),
|
||||||
heat_pump_enable=hp_en,
|
heat_pump_enable=hp_en,
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ class DispatchResult:
|
|||||||
battery_setpoint_w: int # kladné = nabíjení, záporné = vybíjení
|
battery_setpoint_w: int # kladné = nabíjení, záporné = vybíjení
|
||||||
battery_soc_target: float # % SoC na konci intervalu
|
battery_soc_target: float # % SoC na konci intervalu
|
||||||
grid_setpoint_w: int # kladné = import, záporné = export
|
grid_setpoint_w: int # kladné = import, záporné = export
|
||||||
|
export_limit_w: int # tvrdý limit exportu do sítě; 0 = bez exportu
|
||||||
|
export_mode: str # NONE / PV_SURPLUS / BATTERY_SELL
|
||||||
#: Explicitní fyzický režim Deye pro control exporter (PASSIVE / SELL / CHARGE).
|
#: Explicitní fyzický režim Deye pro control exporter (PASSIVE / SELL / CHARGE).
|
||||||
#: Cíl: odstranit heuristiky z exporteru a nést záměr přímo v plánu.
|
#: Cíl: odstranit heuristiky z exporteru a nést záměr přímo v plánu.
|
||||||
deye_physical_mode: str
|
deye_physical_mode: str
|
||||||
@@ -851,6 +853,10 @@ def solve_dispatch(
|
|||||||
batt_w = round(pulp.value(bc[t]) - pulp.value(bd[t]))
|
batt_w = round(pulp.value(bc[t]) - pulp.value(bd[t]))
|
||||||
grid_w = round(pulp.value(gi[t]) - pulp.value(ge[t]))
|
grid_w = round(pulp.value(gi[t]) - pulp.value(ge[t]))
|
||||||
soc_pct = round(pulp.value(soc[t]) / battery.usable_capacity_wh * 100, 1)
|
soc_pct = round(pulp.value(soc[t]) / battery.usable_capacity_wh * 100, 1)
|
||||||
|
export_limit_w = int(grid.max_export_power_w) if grid_w < 0 else 0
|
||||||
|
export_mode = "NONE"
|
||||||
|
if grid_w < 0:
|
||||||
|
export_mode = "BATTERY_SELL" if batt_w < 0 else "PV_SURPLUS"
|
||||||
|
|
||||||
# Primární klasifikace fyzického režimu pro Deye: explicitně do plánu (Variant A).
|
# Primární klasifikace fyzického režimu pro Deye: explicitně do plánu (Variant A).
|
||||||
# Default PASSIVE; SELL při export+vybíjení; CHARGE při import+nabíjení.
|
# Default PASSIVE; SELL při export+vybíjení; CHARGE při import+nabíjení.
|
||||||
@@ -874,6 +880,8 @@ def solve_dispatch(
|
|||||||
battery_setpoint_w = batt_w,
|
battery_setpoint_w = batt_w,
|
||||||
battery_soc_target = soc_pct,
|
battery_soc_target = soc_pct,
|
||||||
grid_setpoint_w = grid_w,
|
grid_setpoint_w = grid_w,
|
||||||
|
export_limit_w = export_limit_w,
|
||||||
|
export_mode = export_mode,
|
||||||
deye_physical_mode = deye_mode,
|
deye_physical_mode = deye_mode,
|
||||||
deye_gen_cutoff_enabled = deye_gen_cutoff,
|
deye_gen_cutoff_enabled = deye_gen_cutoff,
|
||||||
ev1_setpoint_w = round(pulp.value(ev_direct[0][t]) + pulp.value(ev_via_bat[0][t]))
|
ev1_setpoint_w = round(pulp.value(ev_direct[0][t]) + pulp.value(ev_via_bat[0][t]))
|
||||||
@@ -1319,6 +1327,8 @@ async def _save_planning_run(
|
|||||||
"battery_setpoint_w": r.battery_setpoint_w,
|
"battery_setpoint_w": r.battery_setpoint_w,
|
||||||
"battery_soc_target_pct": r.battery_soc_target,
|
"battery_soc_target_pct": r.battery_soc_target,
|
||||||
"grid_setpoint_w": r.grid_setpoint_w,
|
"grid_setpoint_w": r.grid_setpoint_w,
|
||||||
|
"export_limit_w": r.export_limit_w,
|
||||||
|
"export_mode": r.export_mode,
|
||||||
"deye_physical_mode": r.deye_physical_mode,
|
"deye_physical_mode": r.deye_physical_mode,
|
||||||
"deye_gen_cutoff_enabled": r.deye_gen_cutoff_enabled,
|
"deye_gen_cutoff_enabled": r.deye_gen_cutoff_enabled,
|
||||||
"ev1_setpoint_w": r.ev1_setpoint_w,
|
"ev1_setpoint_w": r.ev1_setpoint_w,
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ async def compute_export_ban_active(site_id: int, conn: asyncpg.Connection) -> b
|
|||||||
if bool(pi.get("is_predicted_price")):
|
if bool(pi.get("is_predicted_price")):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
export_mode = str(pi.get("export_mode") or "").upper()
|
||||||
|
if export_mode in ("PV_SURPLUS", "BATTERY_SELL"):
|
||||||
|
return False
|
||||||
|
|
||||||
sell_raw = pi.get("effective_sell_price")
|
sell_raw = pi.get("effective_sell_price")
|
||||||
grid_sp = int(pi.get("grid_setpoint_w") or 0)
|
grid_sp = int(pi.get("grid_setpoint_w") or 0)
|
||||||
if sell_raw is None:
|
if sell_raw is None:
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from services.control.exporter_monolith import (
|
|||||||
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.setpoints import _build_setpoints
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -110,6 +112,30 @@ class DeyeTouParamsTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
||||||
|
|
||||||
|
def test_build_setpoints_uses_explicit_export_limit(self) -> None:
|
||||||
|
mode = OperatingModeInfo(
|
||||||
|
mode_code="AUTO",
|
||||||
|
battery_mode="AUTO",
|
||||||
|
grid_mode="AUTO",
|
||||||
|
ev_enabled=False,
|
||||||
|
heat_pump_enabled_def=False,
|
||||||
|
loxone_mode_value=1,
|
||||||
|
)
|
||||||
|
pi = {
|
||||||
|
"battery_setpoint_w": 0,
|
||||||
|
"grid_setpoint_w": -3000,
|
||||||
|
"export_limit_w": 13_500,
|
||||||
|
"export_mode": "PV_SURPLUS",
|
||||||
|
"ev1_setpoint_w": 0,
|
||||||
|
"ev2_setpoint_w": 0,
|
||||||
|
"heat_pump_enabled": False,
|
||||||
|
"battery_soc_target_pct": 50,
|
||||||
|
"effective_sell_price": 1.0,
|
||||||
|
}
|
||||||
|
sp = _build_setpoints(mode, pi)
|
||||||
|
self.assertIsNotNone(sp)
|
||||||
|
self.assertEqual(sp.grid_export_limit, 13_500)
|
||||||
|
|
||||||
def test_pv_led_export_with_small_battery_is_sell(self) -> None:
|
def test_pv_led_export_with_small_battery_is_sell(self) -> None:
|
||||||
"""Obě záporné → SELL (bez porovnání |bat| vs |grid|)."""
|
"""Obě záporné → SELL (bez porovnání |bat| vs |grid|)."""
|
||||||
sp = ControlSetpoints(
|
sp = ControlSetpoints(
|
||||||
|
|||||||
@@ -254,6 +254,47 @@ class PlanningDispatchMilpTests(unittest.TestCase):
|
|||||||
self.assertEqual(int(results[0].pv_a_curtailed_w), 5000)
|
self.assertEqual(int(results[0].pv_a_curtailed_w), 5000)
|
||||||
self.assertGreaterEqual(int(results[0].grid_setpoint_w), 0)
|
self.assertGreaterEqual(int(results[0].grid_setpoint_w), 0)
|
||||||
|
|
||||||
|
def test_pv_surplus_export_uses_hard_export_cap(self) -> None:
|
||||||
|
slots = [
|
||||||
|
_slot(load=0, buy=3.0, sell=2.5, pv_a=20_000, pv_b=0),
|
||||||
|
]
|
||||||
|
battery = _battery()
|
||||||
|
hp = SimpleNamespace(
|
||||||
|
rated_heating_power_w=0,
|
||||||
|
tuv_min_temp_c=45.0,
|
||||||
|
tuv_target_temp_c=55.0,
|
||||||
|
)
|
||||||
|
grid = SimpleNamespace(max_import_power_w=20_000, max_export_power_w=13_500)
|
||||||
|
vehicles = [
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0,
|
||||||
|
battery_capacity_kwh=1.0,
|
||||||
|
default_target_soc_pct=80.0,
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0,
|
||||||
|
battery_capacity_kwh=1.0,
|
||||||
|
default_target_soc_pct=80.0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
soc0 = battery.soc_max_wh
|
||||||
|
results, _ms = solve_dispatch(
|
||||||
|
slots,
|
||||||
|
battery,
|
||||||
|
hp,
|
||||||
|
grid,
|
||||||
|
[None, None],
|
||||||
|
vehicles,
|
||||||
|
soc0,
|
||||||
|
50.0,
|
||||||
|
tuv_delta_stats=None,
|
||||||
|
operating_mode="AUTO",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(results), 1)
|
||||||
|
self.assertEqual(results[0].export_mode, "PV_SURPLUS")
|
||||||
|
self.assertEqual(results[0].export_limit_w, 13_500)
|
||||||
|
self.assertGreater(results[0].pv_a_curtailed_w, 0)
|
||||||
|
|
||||||
def test_two_tier_soc_solves_optimal(self) -> None:
|
def test_two_tier_soc_solves_optimal(self) -> None:
|
||||||
slots = [_slot()]
|
slots = [_slot()]
|
||||||
battery = _battery()
|
battery = _battery()
|
||||||
|
|||||||
@@ -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** dle `_deye_zero_export_amps_for_passive` (viz `operating-modes.md`) |
|
| **PASSIVE (ZERO)** | vše ostatní — reg. **108/109** dle `_deye_zero_export_amps_for_passive` (viz `operating-modes.md`) |
|
||||||
|
|
||||||
**PASSIVE** (AUTO, ZERO): výchozí **108** i **109** = maximum z DB; u exportu bez vybíjení **108 = 0**, u importu bez nabíjení **109 = 0** (`_deye_zero_export_amps_for_passive`). **TOU** z plánu. Reg. **142** = `deye_zero_export_mode`. 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): výchozí **108** i **109** = maximum z DB; u exportu bez vybíjení **108 = 0**, u importu bez nabíjení **109 = 0** (`_deye_zero_export_amps_for_passive`). **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*).
|
||||||
|
|
||||||
**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`.
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
|||||||
| **108** (charge A) | škálo dle `battery_w` | max / **0** (FVE přetok) | **0** | dle varianty |
|
| **108** (charge A) | škálo dle `battery_w` | max / **0** (FVE přetok) | **0** | dle varianty |
|
||||||
| **109** (discharge A) | **0** | max / **0** (import, držet bat.) | **max z DB** | dle varianty |
|
| **109** (discharge A) | **0** | max / **0** (import, držet bat.) | **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 | `min(max_site, max(200, \|grid_setpoint_w\|))` | 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` |
|
||||||
| **178** (peak shaving) | 48 | 48 | **32** | 48 |
|
| **178** (peak shaving) | 48 | 48 | **32** | 48 |
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ async def write_inverter_setpoints(site_id: int, setpoints: Setpoints, db):
|
|||||||
slave=inv.unit_id)
|
slave=inv.unit_id)
|
||||||
|
|
||||||
# Export limit
|
# Export limit
|
||||||
export_limit = max(0, -setpoints.grid_setpoint_w) if setpoints.grid_setpoint_w < 0 else 0
|
export_limit = setpoints.grid_export_limit
|
||||||
await client.write_register(0x00F6, export_limit, slave=inv.unit_id)
|
await client.write_register(0x00F6, export_limit, slave=inv.unit_id)
|
||||||
|
|
||||||
logger.info(f"Inverter {inv.code} setpoints written: batt={setpoints.battery_setpoint_w}W")
|
logger.info(f"Inverter {inv.code} setpoints written: batt={setpoints.battery_setpoint_w}W")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
|
|||||||
| 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**; při 108 = 0 a 145 = 1 přebytky z řiditelného stringu typicky tečou do sítě (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` |
|
| 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). |
|
||||||
| 190 | GEN peak shaving | 0–16000 | 1 W | Peak shaving na GEN portu |
|
| 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**. |
|
| 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**. |
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Keep it simple
|
## Keep it simple
|
||||||
|
|
||||||
- **Žá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.
|
||||||
- **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`), 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`).
|
||||||
- **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`).
|
||||||
@@ -54,6 +55,8 @@ Všechny řádky předpokládají **142** = zero export (ne SELL).
|
|||||||
| Přetok FVE do sítě | `grid_setpoint_w` < 0 **a** `battery_w` ≥ 0 | **0** | 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.
|
||||||
|
|
||||||
Nabíjení ze sítě s vysokým cílovým SoC v TOU řeší větev **CHARGE** (grid charge v time pointech), ne tato tabulka.
|
Nabíjení ze sítě s vysokým cílovým SoC v TOU řeší větev **CHARGE** (grid charge v time pointech), ne tato tabulka.
|
||||||
|
|
||||||
### ZERO a „zakázaný export FVE do sítě“ (jen řiditelná pole)
|
### ZERO a „zakázaný export FVE do sítě“ (jen řiditelná pole)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
- **Záporná prodejní cena → tvrdý zákaz vývozu (`ge = 0`)** (`planning_engine.solve_dispatch`): platí ve slotu kde `sell_price < 0`, pokud lokality zapne některou z opcí —
|
- **Záporná prodejní cena → tvrdý zákaz vývozu (`ge = 0`)** (`planning_engine.solve_dispatch`): platí ve slotu kde `sell_price < 0`, pokud lokality zapne některou z opcí —
|
||||||
- **`asset_inverter.deye_gen_microinverter_cutoff_enabled`** (`deye-main`) — spojeno s MILP binárkami GEN cut-off (BA81),
|
- **`asset_inverter.deye_gen_microinverter_cutoff_enabled`** (`deye-main`) — spojeno s MILP binárkami GEN cut-off (BA81),
|
||||||
- **nebo** **`ems.site_grid_connection.block_export_on_negative_sell`** (migrace **V074**, default **false**) — bez GEN registrů na Deye; vhodné např. pro **KV1** (fixní nákup, bez nutnosti vést výkon neriťitelného pole B do sítě). **home-01** nech **false**, jinak může být horizont při přebytku z pole B a plné/nedostupné baterii **infeasible** (solver export potřebuje jako fyzikální ventil tam, kde FVE B nelze štípnout ani odpojit modelem bez GEN řádku).
|
- **nebo** **`ems.site_grid_connection.block_export_on_negative_sell`** (migrace **V074**, default **false**) — bez GEN registrů na Deye; vhodné např. pro **KV1** (fixní nákup, bez nutnosti vést výkon neriťitelného pole B do sítě). **home-01** nech **false**, jinak může být horizont při přebytku z pole B a plné/nedostupné baterii **infeasible** (solver export potřebuje jako fyzikální ventil tam, kde FVE B nelze štípnout ani odpojit modelem bez GEN řádku).
|
||||||
|
- **Export bez forecastového capu:** solver ukládá explicitní `planning_interval.export_limit_w` jako tvrdý site/inverter limit a `planning_interval.export_mode` (`NONE` / `PV_SURPLUS` / `BATTERY_SELL`). Exportér z plánu neodvozuje žádný forecastový strop exportu.
|
||||||
- **Uložené vstupy plánu** (`planning_interval`): `load_baseline_w`, `pv_*_forecast_raw_w`, `pv_*_forecast_solver_w` pro UI a audit.
|
- **Uložené vstupy plánu** (`planning_interval`): `load_baseline_w`, `pv_*_forecast_raw_w`, `pv_*_forecast_solver_w` pro UI a audit.
|
||||||
- **Více FVE polí s různou orientací:** `planning_engine._load_slots` sčítá predikovaný výkon za 15min přes **všechna** `asset_pv_array` dané lokality — `pv_a_forecast_w` = součet řádků s `controllable = true`, `pv_b_forecast_w` = součet s `controllable = false`. Pro každé pole a slot se bere **nejnovější** `forecast_pv_run` (`ORDER BY created_at DESC`, `DISTINCT ON (pv_array_id)`). Curtailment v LP zůstává **jedno** agregované `pv_a` (součet řiditelných polí); per-string curtailment by vyžadovalo rozšíření modelu.
|
- **Více FVE polí s různou orientací:** `planning_engine._load_slots` sčítá predikovaný výkon za 15min přes **všechna** `asset_pv_array` dané lokality — `pv_a_forecast_w` = součet řádků s `controllable = true`, `pv_b_forecast_w` = součet s `controllable = false`. Pro každé pole a slot se bere **nejnovější** `forecast_pv_run` (`ORDER BY created_at DESC`, `DISTINCT ON (pv_array_id)`). Curtailment v LP zůstává **jedno** agregované `pv_a` (součet řiditelných polí); per-string curtailment by vyžadovalo rozšíření modelu.
|
||||||
- **Kalibrace PV forecastu (delta profil):** tabulka `ems.site_pv_forecast_calibration` drží per `site_id` mimo jiné `delta_learn_min_ts` (dolní mez řádků z `forecast_accuracy` pro učení delty), volitelně `pv_curtailment_policy_effective_from` a přepsání parametrů (`top_n_days`, `half_life_days`, …; **V076** navíc `reference_day_weight_mult` pro „připnuté“ dny níže). **`ems.site_pv_forecast_reference_day`** (**V076**) umožňuje zvýšit váhu konkrétních kalendářních dnů (datum ve `site.timezone` jako u časování slotů) při agregaci δ z `forecast_accuracy` (`fn_pv_forecast_delta_profile`); hromadný zápis **`ems.fn_pv_forecast_sync_reference_days`**, detail **`docs/04-modules/forecast.md`**. `ems.fn_fill_forecast_accuracy` nastavuje `learning_eligible` / `learning_exclude_reason` (sloty před cutoffem, nebo se škrcením / gen cut-off / záznamem v `ems.cutoff_switch_log` po účinnosti policy se z učení vyřadí; u škrcení zůstává `actual_power_w` NULL). Telemetrie: `ems.telemetry_inverter.is_export_limited` nebo `pv_derating_flags <> 0` v okně 15min → stejné vyloučení (`telemetry_derating`). `ems.fn_pv_forecast_delta_profile` vrací `deltas_by_array` i součtové `deltas`; `ems.fn_load_planning_slots_full` aplikuje stejnou **per-pole** korekci jako UI (`fn_forecast_pv_slots_range_corrected`); pokud v JSON profilu chybí `deltas_by_array`, použije se souhrnné `deltas` rozpuštěné podle podílu výkonu pole na slotu (solver má tak stále použitou korekci i bez per-pole JSON).
|
- **Kalibrace PV forecastu (delta profil):** tabulka `ems.site_pv_forecast_calibration` drží per `site_id` mimo jiné `delta_learn_min_ts` (dolní mez řádků z `forecast_accuracy` pro učení delty), volitelně `pv_curtailment_policy_effective_from` a přepsání parametrů (`top_n_days`, `half_life_days`, …; **V076** navíc `reference_day_weight_mult` pro „připnuté“ dny níže). **`ems.site_pv_forecast_reference_day`** (**V076**) umožňuje zvýšit váhu konkrétních kalendářních dnů (datum ve `site.timezone` jako u časování slotů) při agregaci δ z `forecast_accuracy` (`fn_pv_forecast_delta_profile`); hromadný zápis **`ems.fn_pv_forecast_sync_reference_days`**, detail **`docs/04-modules/forecast.md`**. `ems.fn_fill_forecast_accuracy` nastavuje `learning_eligible` / `learning_exclude_reason` (sloty před cutoffem, nebo se škrcením / gen cut-off / záznamem v `ems.cutoff_switch_log` po účinnosti policy se z učení vyřadí; u škrcení zůstává `actual_power_w` NULL). Telemetrie: `ems.telemetry_inverter.is_export_limited` nebo `pv_derating_flags <> 0` v okně 15min → stejné vyloučení (`telemetry_derating`). `ems.fn_pv_forecast_delta_profile` vrací `deltas_by_array` i součtové `deltas`; `ems.fn_load_planning_slots_full` aplikuje stejnou **per-pole** korekci jako UI (`fn_forecast_pv_slots_range_corrected`); pokud v JSON profilu chybí `deltas_by_array`, použije se souhrnné `deltas` rozpuštěné podle podílu výkonu pole na slotu (solver má tak stále použitou korekci i bez per-pole JSON).
|
||||||
@@ -421,6 +422,8 @@ def solve_dispatch(
|
|||||||
battery_setpoint_w = round(pulp.value(batt_charge[t]) - pulp.value(batt_discharge[t])),
|
battery_setpoint_w = round(pulp.value(batt_charge[t]) - pulp.value(batt_discharge[t])),
|
||||||
battery_soc_target = round(pulp.value(soc[t]) / battery.usable_capacity_wh * 100, 1),
|
battery_soc_target = round(pulp.value(soc[t]) / battery.usable_capacity_wh * 100, 1),
|
||||||
grid_setpoint_w = round(pulp.value(grid_import[t]) - pulp.value(grid_export[t])),
|
grid_setpoint_w = round(pulp.value(grid_import[t]) - pulp.value(grid_export[t])),
|
||||||
|
export_limit_w = int(grid.max_export_power_w) if round(pulp.value(grid_import[t]) - pulp.value(grid_export[t])) < 0 else 0,
|
||||||
|
export_mode = "BATTERY_SELL" if round(pulp.value(batt_charge[t]) - pulp.value(batt_discharge[t])) < 0 and round(pulp.value(grid_import[t]) - pulp.value(grid_export[t])) < 0 else ("PV_SURPLUS" if round(pulp.value(grid_import[t]) - pulp.value(grid_export[t])) < 0 else "NONE"),
|
||||||
ev_charge_power_w = round(pulp.value(ev_charge[t])),
|
ev_charge_power_w = round(pulp.value(ev_charge[t])),
|
||||||
heat_pump_enabled = hp_enabled,
|
heat_pump_enabled = hp_enabled,
|
||||||
heat_pump_setpoint_w = hp_power,
|
heat_pump_setpoint_w = hp_power,
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ const LiveRegistersSection = memo(
|
|||||||
valueText={live?.reg142_limit_control != null ? String(live.reg142_limit_control) : undefined}
|
valueText={live?.reg142_limit_control != null ? String(live.reg142_limit_control) : undefined}
|
||||||
/>
|
/>
|
||||||
<Metric label="Energy mode" reg={141} valueText={live?.reg141_energy_mode != null ? String(live.reg141_energy_mode) : undefined} />
|
<Metric label="Energy mode" reg={141} valueText={live?.reg141_energy_mode != null ? String(live.reg141_energy_mode) : undefined} />
|
||||||
|
<Metric
|
||||||
|
label="Export cap"
|
||||||
|
reg={143}
|
||||||
|
sub="Hard limit lokality / invertoru; neforecastuje se"
|
||||||
|
valueText={fmtW(live?.reg143_export_limit_w)}
|
||||||
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
label="Peak shaving switch"
|
label="Peak shaving switch"
|
||||||
reg={178}
|
reg={178}
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ function syntheticForecastOnlyInterval(
|
|||||||
battery_setpoint_w: null,
|
battery_setpoint_w: null,
|
||||||
battery_soc_target_pct: null,
|
battery_soc_target_pct: null,
|
||||||
grid_setpoint_w: null,
|
grid_setpoint_w: null,
|
||||||
|
export_limit_w: null,
|
||||||
|
export_mode: null,
|
||||||
deye_physical_mode: null,
|
deye_physical_mode: null,
|
||||||
ev1_setpoint_w: null,
|
ev1_setpoint_w: null,
|
||||||
ev2_setpoint_w: null,
|
ev2_setpoint_w: null,
|
||||||
@@ -341,6 +343,7 @@ function axiosDetail(e: unknown): string {
|
|||||||
function deyeSetpointLabel(i: PlanningIntervalDto): string {
|
function deyeSetpointLabel(i: PlanningIntervalDto): string {
|
||||||
const battery_w = i.battery_setpoint_w ?? 0
|
const battery_w = i.battery_setpoint_w ?? 0
|
||||||
const grid_w = i.grid_setpoint_w ?? 0
|
const grid_w = i.grid_setpoint_w ?? 0
|
||||||
|
const exportLimitW = i.export_limit_w ?? 0
|
||||||
const tgt = i.battery_soc_target_pct
|
const tgt = i.battery_soc_target_pct
|
||||||
const targetSoc = tgt != null ? Math.min(95, Math.round(Number(tgt))) : 80
|
const targetSoc = tgt != null ? Math.min(95, Math.round(Number(tgt))) : 80
|
||||||
|
|
||||||
@@ -353,14 +356,18 @@ function deyeSetpointLabel(i: PlanningIntervalDto): string {
|
|||||||
const pm = (i.deye_physical_mode ?? '').toString().trim().toUpperCase()
|
const pm = (i.deye_physical_mode ?? '').toString().trim().toUpperCase()
|
||||||
if (pm === 'SELL') {
|
if (pm === 'SELL') {
|
||||||
const tpPowerW = Math.abs(battery_w)
|
const tpPowerW = Math.abs(battery_w)
|
||||||
return `SELL | ⬇ ${fmtKw(tpPowerW)} | reg142=0 reg178=32`
|
const cap = exportLimitW > 0 ? ` | cap ${fmtKw(exportLimitW)}` : ''
|
||||||
|
return `SELL | ⬇ ${fmtKw(tpPowerW)}${cap} | reg142=0 reg178=32`
|
||||||
}
|
}
|
||||||
if (pm === 'CHARGE') {
|
if (pm === 'CHARGE') {
|
||||||
return `CHARGE | ⬆ ${fmtKw(Math.max(0, battery_w))} | grid=yes | SOC→${targetSoc}%`
|
return `CHARGE | ⬆ ${fmtKw(Math.max(0, battery_w))} | grid=yes | SOC→${targetSoc}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASSIVE (ZERO): doplň informaci o variantě 108/109 podle pravidel (bez wattových prahů).
|
// PASSIVE (ZERO): doplň informaci o variantě 108/109 podle pravidel (bez wattových prahů).
|
||||||
if (grid_w < 0 && battery_w >= 0) return 'PASSIVE | FVE→síť (108=0)'
|
if (grid_w < 0 && battery_w >= 0) {
|
||||||
|
const cap = exportLimitW > 0 ? ` | cap ${fmtKw(exportLimitW)}` : ''
|
||||||
|
return `PASSIVE | FVE→síť${cap} (108=0)`
|
||||||
|
}
|
||||||
if (grid_w > 0 && battery_w <= 0) return 'PASSIVE | držet bat. (109=0)'
|
if (grid_w > 0 && battery_w <= 0) return 'PASSIVE | držet bat. (109=0)'
|
||||||
return 'PASSIVE | max/max'
|
return 'PASSIVE | max/max'
|
||||||
}
|
}
|
||||||
@@ -369,12 +376,15 @@ function deyeModeBadge(i: PlanningIntervalDto): { label: string; klass: string;
|
|||||||
const m = (i.deye_physical_mode ?? 'PASSIVE').toString().trim().toUpperCase()
|
const m = (i.deye_physical_mode ?? 'PASSIVE').toString().trim().toUpperCase()
|
||||||
const battery_w = i.battery_setpoint_w ?? 0
|
const battery_w = i.battery_setpoint_w ?? 0
|
||||||
const grid_w = i.grid_setpoint_w ?? 0
|
const grid_w = i.grid_setpoint_w ?? 0
|
||||||
|
const exportLimitW = i.export_limit_w ?? 0
|
||||||
|
const exportMode = (i.export_mode ?? 'NONE').toString().trim().toUpperCase()
|
||||||
|
const cap = exportLimitW > 0 ? `; hard cap ${formatPlanPowerW(exportLimitW)}` : ''
|
||||||
|
|
||||||
if (m === 'SELL') {
|
if (m === 'SELL') {
|
||||||
return {
|
return {
|
||||||
label: 'SELL',
|
label: 'SELL',
|
||||||
klass: 'bg-orange-500/15 text-orange-200 ring-1 ring-orange-500/35',
|
klass: 'bg-orange-500/15 text-orange-200 ring-1 ring-orange-500/35',
|
||||||
title: 'SELL (selling first): reg142=0, reg178=32 (grid peak shaving off)',
|
title: `SELL (selling first): reg142=0, reg178=32 (grid peak shaving off)${cap}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m === 'CHARGE') {
|
if (m === 'CHARGE') {
|
||||||
@@ -386,12 +396,14 @@ function deyeModeBadge(i: PlanningIntervalDto): { label: string; klass: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
let variant = 'max/max'
|
let variant = 'max/max'
|
||||||
if (grid_w < 0 && battery_w >= 0) variant = 'FVE→síť (108=0)'
|
if (grid_w < 0 && battery_w >= 0) {
|
||||||
|
variant = exportMode === 'PV_SURPLUS' ? 'FVE→síť' : 'export'
|
||||||
|
}
|
||||||
else if (grid_w > 0 && battery_w <= 0) variant = 'držet bat. (109=0)'
|
else if (grid_w > 0 && battery_w <= 0) variant = 'držet bat. (109=0)'
|
||||||
return {
|
return {
|
||||||
label: 'PASSIVE',
|
label: 'PASSIVE',
|
||||||
klass: 'bg-slate-600/40 text-slate-200 ring-1 ring-slate-500/30',
|
klass: 'bg-slate-600/40 text-slate-200 ring-1 ring-slate-500/30',
|
||||||
title: `PASSIVE (ZERO): ${variant}; reg142=deye_zero_export_mode; reg178=48`,
|
title: `PASSIVE (ZERO): ${variant}${cap}; reg142=deye_zero_export_mode; reg178=48`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +551,8 @@ function PlanTooltip({
|
|||||||
const fveStr = formatPlanPowerW(p.pv_a_w)
|
const fveStr = formatPlanPowerW(p.pv_a_w)
|
||||||
const fveDisplay = fveStr === '—' ? '—' : fveStr.includes('kW') ? fveStr : `${fveStr} W`
|
const fveDisplay = fveStr === '—' ? '—' : fveStr.includes('kW') ? fveStr : `${fveStr} W`
|
||||||
const soc = p.battery_soc_target_pct
|
const soc = p.battery_soc_target_pct
|
||||||
|
const exportLimit = i.export_limit_w
|
||||||
|
const exportMode = i.export_mode ?? 'NONE'
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-slate-600 bg-slate-950 px-3 py-2 text-xs text-slate-200 shadow-xl">
|
<div className="rounded-lg border border-slate-600 bg-slate-950 px-3 py-2 text-xs text-slate-200 shadow-xl">
|
||||||
<div className="mb-1 font-medium text-slate-100">{formatLocal(i.interval_start)}</div>
|
<div className="mb-1 font-medium text-slate-100">{formatLocal(i.interval_start)}</div>
|
||||||
@@ -556,6 +570,12 @@ function PlanTooltip({
|
|||||||
{sell != null ? `${sell.toFixed(3)} Kč/kWh` : '—'}
|
{sell != null ? `${sell.toFixed(3)} Kč/kWh` : '—'}
|
||||||
</div>
|
</div>
|
||||||
<div>FVE (korig. předpověď / audit): {fveDisplay}</div>
|
<div>FVE (korig. předpověď / audit): {fveDisplay}</div>
|
||||||
|
{exportMode !== 'NONE' ? (
|
||||||
|
<div>
|
||||||
|
Export: {exportMode}
|
||||||
|
{exportLimit != null ? ` · limit ${formatPlanPowerW(exportLimit)}` : ''}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div>SoC cíl: {soc != null && !Number.isNaN(Number(soc)) ? `${Number(soc).toFixed(1)} %` : '—'}</div>
|
<div>SoC cíl: {soc != null && !Number.isNaN(Number(soc)) ? `${Number(soc).toFixed(1)} %` : '—'}</div>
|
||||||
<div>Dům: {i.load_baseline_w ?? '—'} W</div>
|
<div>Dům: {i.load_baseline_w ?? '—'} W</div>
|
||||||
<div>Baterie: {i.battery_setpoint_w ?? '—'} W</div>
|
<div>Baterie: {i.battery_setpoint_w ?? '—'} W</div>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export type PlanningIntervalDto = {
|
|||||||
battery_setpoint_w: number | null
|
battery_setpoint_w: number | null
|
||||||
battery_soc_target_pct: number | null
|
battery_soc_target_pct: number | null
|
||||||
grid_setpoint_w: number | null
|
grid_setpoint_w: number | null
|
||||||
|
/** Tvrdý limit exportu do sítě v daném slotu (W); 0 = bez exportu. */
|
||||||
|
export_limit_w?: number | null
|
||||||
|
/** Záměr exportu: NONE / PV_SURPLUS / BATTERY_SELL. */
|
||||||
|
export_mode?: 'NONE' | 'PV_SURPLUS' | 'BATTERY_SELL' | null
|
||||||
/** Explicitní fyzický režim Deye pro slot (PASSIVE/SELL/CHARGE). */
|
/** Explicitní fyzický režim Deye pro slot (PASSIVE/SELL/CHARGE). */
|
||||||
deye_physical_mode?: 'PASSIVE' | 'SELL' | 'CHARGE' | null
|
deye_physical_mode?: 'PASSIVE' | 'SELL' | 'CHARGE' | null
|
||||||
/** True = solver plánuje odpojit GEN port (MI export cutoff) v tomto slotu (BA81). */
|
/** True = solver plánuje odpojit GEN port (MI export cutoff) v tomto slotu (BA81). */
|
||||||
|
|||||||
Reference in New Issue
Block a user