register 340 -omezovani vyrkonu pv pole (home-01)
This commit is contained in:
@@ -121,6 +121,7 @@ DEYE_REGISTER_NAMES: dict[int, str] = {
|
||||
142: "limit_control (0=selling first, 1=zero export to load, 2=zero export to CT)",
|
||||
143: "export_limit_w (max export do sítě)",
|
||||
145: "solar_sell (0=disabled, 1=enabled)",
|
||||
340: "max_solar_power_w (strop DC PV A v W; součet nominal_power_wp řiditelných polí)",
|
||||
178: "control_board_special_1 (bits0-1: MI export cutoff disable=2 enable=3; bits4-5 peak shaving 32/48)",
|
||||
148: "time_point_1_time",
|
||||
149: "time_point_2_time",
|
||||
@@ -202,6 +203,16 @@ class InverterConfig:
|
||||
deye_tou_inactive_signature: str | None = None
|
||||
deye_zero_export_mode: int = 1
|
||||
deye_gen_microinverter_cutoff_enabled: bool = False
|
||||
manufacturer: str = ""
|
||||
#: Součet nominal_power_wp controllable PV na invertoru; 0 = EMS nezapisuje reg 340.
|
||||
pv_a_cap_w: int = 0
|
||||
|
||||
|
||||
def compute_pv_a_reg340_max_solar_w(cap_w: int, forecast_w: int, curtail_w: int) -> int:
|
||||
"""Hodnota pro Deye reg 340 (max solar power, W) z capu a plánovaného curtailmentu pole A."""
|
||||
if curtail_w <= 0:
|
||||
return int(cap_w)
|
||||
return max(0, min(int(cap_w), int(forecast_w) - int(curtail_w)))
|
||||
|
||||
|
||||
def _prague_minute_start_utc() -> datetime:
|
||||
@@ -368,6 +379,8 @@ class ControlSetpoints:
|
||||
lock_battery: bool = False
|
||||
#: Režim SELF_SUSTAIN: plný rozsah nabíjení/vybíjení na invertoru + zero-export (reg 142) a nízké TOU %.
|
||||
self_sustain_local_use: bool = False
|
||||
#: Deye reg 340 (max solar power, W). None = EMS reg 340 v tomto ticku neřeší (PRESERVE/SELF_SUSTAIN/CHARGE_CHEAP/…).
|
||||
pv_a_allowed_w: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1062,6 +1075,8 @@ async def _load_inverter_config(
|
||||
"""
|
||||
SELECT
|
||||
ai.id, ai.code,
|
||||
coalesce(ai.manufacturer, '') AS manufacturer,
|
||||
coalesce(ems.fn_inverter_pv_a_max_w(ai.id), 0) AS pv_a_cap_w,
|
||||
se.host, se.port, se.unit_id,
|
||||
sgc.max_export_power_w,
|
||||
sgc.max_import_power_w,
|
||||
@@ -1162,6 +1177,8 @@ async def _load_inverter_config(
|
||||
deye_tou_inactive_signature=row["deye_tou_inactive_signature"],
|
||||
deye_zero_export_mode=int(row["deye_zero_export_mode"]),
|
||||
deye_gen_microinverter_cutoff_enabled=bool(row["deye_gen_microinverter_cutoff_enabled"] or False),
|
||||
manufacturer=str(row["manufacturer"] or ""),
|
||||
pv_a_cap_w=int(row["pv_a_cap_w"] or 0),
|
||||
)
|
||||
|
||||
|
||||
@@ -1240,7 +1257,13 @@ class _DictRecord:
|
||||
return k in self._d
|
||||
|
||||
|
||||
def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> ControlSetpoints | None:
|
||||
def _build_setpoints(
|
||||
mode: OperatingModeInfo,
|
||||
pi: asyncpg.Record | None,
|
||||
*,
|
||||
pv_a_cap_w: int = 0,
|
||||
inverter_manufacturer: str = "",
|
||||
) -> ControlSetpoints | None:
|
||||
code = mode.mode_code
|
||||
if code == "MANUAL":
|
||||
return None
|
||||
@@ -1262,6 +1285,11 @@ def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> Cont
|
||||
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 = bool(gen_cutoff_raw) if gen_cutoff_raw is not None else False
|
||||
pv_a_allowed: int | None = None
|
||||
if (inverter_manufacturer or "").strip().lower() == "deye" and int(pv_a_cap_w) > 0:
|
||||
forecast = int(pi.get("pv_a_forecast_solver_w") or 0)
|
||||
curtail = int(pi.get("pv_a_curtailed_w") or 0)
|
||||
pv_a_allowed = compute_pv_a_reg340_max_solar_w(int(pv_a_cap_w), forecast, curtail)
|
||||
return ControlSetpoints(
|
||||
battery_w=int(pi["battery_setpoint_w"] or 0),
|
||||
grid_export_limit=abs(min(grid_sp, 0)),
|
||||
@@ -1276,6 +1304,7 @@ def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> Cont
|
||||
export_ban=bool(export_ban),
|
||||
deye_gen_cutoff_enabled=bool(gen_cutoff),
|
||||
effective_sell_price_czk_kwh=sell_f,
|
||||
pv_a_allowed_w=pv_a_allowed,
|
||||
)
|
||||
|
||||
if code == "SELF_SUSTAIN":
|
||||
@@ -1349,6 +1378,7 @@ def _apply_price_failsafe_guard(
|
||||
ev2_power_w=sp.ev2_power_w,
|
||||
target_soc_pct=sp.target_soc_pct,
|
||||
effective_sell_price_czk_kwh=sp.effective_sell_price_czk_kwh,
|
||||
pv_a_allowed_w=sp.pv_a_allowed_w,
|
||||
)
|
||||
|
||||
|
||||
@@ -1633,6 +1663,14 @@ async def write_inverter_setpoints(
|
||||
]
|
||||
)
|
||||
|
||||
mfr = (inv.manufacturer or "").strip().lower()
|
||||
if (
|
||||
mfr == "deye"
|
||||
and int(inv.pv_a_cap_w) > 0
|
||||
and setpoints_now.pv_a_allowed_w is not None
|
||||
):
|
||||
registers.append((340, "max_solar_power_w", int(setpoints_now.pv_a_allowed_w)))
|
||||
|
||||
# Reg 178: bitové pole. Nastavujeme bits4–5 (peak shaving) vždy; bits0–1 (MI export cutoff) jen pokud feature.
|
||||
# Ostatní bity musí zůstat zachované → read-modify-write.
|
||||
try:
|
||||
@@ -1768,13 +1806,13 @@ async def write_inverter_setpoints(
|
||||
|
||||
return (
|
||||
f"OK inverter: batt_w={raw_bat!r} "
|
||||
f"(time points + FC 0x10: 108/109/141/142/178/143)"
|
||||
f"(time points + FC 0x10: 108/109/141/142/178/143/145/340 dle plánu)"
|
||||
)
|
||||
|
||||
|
||||
async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict[str, Any]:
|
||||
"""
|
||||
Živé čtení holding registrů Deye 108, 109, 141–145, 178, 191 (stejné TCP spojení jako telemetrie/export).
|
||||
Živé čtení holding registrů Deye 108, 109, 141–145, 178, 191, 340 (stejné TCP spojení jako telemetrie/export).
|
||||
Vše pod jedním mutexem + sdružené FC3 bloky — mezi jednotlivými read_register dřív telemetrie
|
||||
střídavě brala lock a RS485 brány házely cizí transaction_id / I/O timeouty.
|
||||
"""
|
||||
@@ -1791,11 +1829,13 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
|
||||
b141 = await mb.read_holding_registers(141, 5)
|
||||
r178 = await mb.read_holding_registers(178, 1)
|
||||
r191 = await mb.read_holding_registers(191, 1)
|
||||
r340 = await mb.read_holding_registers(340, 1)
|
||||
r108, r109 = b108[0], b108[1]
|
||||
r141, r142, r143 = b141[0], b141[1], b141[2]
|
||||
r145 = b141[4]
|
||||
r178 = r178[0]
|
||||
r191 = r191[0]
|
||||
r340v = int(r340[0]) if r340 and len(r340) >= 1 else 0
|
||||
except Exception:
|
||||
logger.exception("read_deye_registers_live site=%s failed", site_id)
|
||||
raise
|
||||
@@ -1812,6 +1852,7 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
|
||||
"reg178_mi_export_cutoff_bits": int(r178) & int(REG178_MI_EXPORT_MASK),
|
||||
"reg178_mi_export_cutoff_is_on": (int(r178) & int(REG178_MI_EXPORT_MASK)) == int(REG178_MI_EXPORT_ENABLE),
|
||||
"reg191_peak_shaving_w": int(r191),
|
||||
"reg340_max_solar_power_w": int(r340v),
|
||||
"read_at": read_at.isoformat(),
|
||||
}
|
||||
|
||||
@@ -1955,10 +1996,17 @@ async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None:
|
||||
return
|
||||
|
||||
try:
|
||||
inv_for_pv = await _load_inverter_config(site_id, db)
|
||||
cap_pv = int(inv_for_pv.pv_a_cap_w) if inv_for_pv is not None else 0
|
||||
mfr_pv = (inv_for_pv.manufacturer or "") if inv_for_pv is not None else ""
|
||||
pi_now = await _fetch_plan_row_for_slot_offset(site_id, db, 0)
|
||||
pi_next = await _fetch_plan_row_for_slot_offset(site_id, db, 1)
|
||||
sp_now = _build_setpoints(mode, pi_now)
|
||||
sp_next = _build_setpoints(mode, pi_next)
|
||||
sp_now = _build_setpoints(
|
||||
mode, pi_now, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
|
||||
)
|
||||
sp_next = _build_setpoints(
|
||||
mode, pi_next, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
|
||||
)
|
||||
|
||||
if mode.mode_code == "AUTO" and sp_now is None:
|
||||
if pi_now is None:
|
||||
|
||||
Reference in New Issue
Block a user