fix rizeni pole pres reg340 jen home01
This commit is contained in:
@@ -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`.
|
||||
|
||||
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** z plánu. **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 **Deye** + `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_zero_export_amps_for_passive`; **TOU** z plánu. **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).
|
||||
|
||||
@@ -159,7 +159,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
|
||||
| `signal_state` | Poslední požadovaná / odeslaná / ověřená hodnota na cíli (idempotence). |
|
||||
| `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. |
|
||||
|
||||
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`.
|
||||
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -203,9 +203,10 @@ 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
|
||||
#: True = EMS smí řídit Deye reg 340 (max solar power); z SQL `fn_site_has_active_green_bonus_pv(site_id)` — není DB sloupec na invertoru.
|
||||
deye_reg340_pv_a_control_enabled: bool = False
|
||||
|
||||
|
||||
def compute_pv_a_reg340_max_solar_w(cap_w: int, forecast_w: int, curtail_w: int) -> int:
|
||||
@@ -1075,7 +1076,6 @@ 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,
|
||||
@@ -1093,6 +1093,8 @@ async def _load_inverter_config(
|
||||
ai.deye_tou_inactive_signature,
|
||||
COALESCE(ai.deye_zero_export_mode, 1) AS deye_zero_export_mode,
|
||||
COALESCE(ai.deye_gen_microinverter_cutoff_enabled, false) AS deye_gen_microinverter_cutoff_enabled,
|
||||
coalesce(ems.fn_site_has_active_green_bonus_pv(ai.site_id), false)
|
||||
AS deye_reg340_pv_a_control_enabled,
|
||||
COALESCE(
|
||||
ai.deye_register_max_charge_a,
|
||||
FLOOR(
|
||||
@@ -1177,8 +1179,10 @@ 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),
|
||||
deye_reg340_pv_a_control_enabled=bool(
|
||||
row["deye_reg340_pv_a_control_enabled"] or False
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1262,7 +1266,7 @@ def _build_setpoints(
|
||||
pi: asyncpg.Record | None,
|
||||
*,
|
||||
pv_a_cap_w: int = 0,
|
||||
inverter_manufacturer: str = "",
|
||||
reg340_pv_a_control_enabled: bool = False,
|
||||
) -> ControlSetpoints | None:
|
||||
code = mode.mode_code
|
||||
if code == "MANUAL":
|
||||
@@ -1286,7 +1290,7 @@ def _build_setpoints(
|
||||
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:
|
||||
if bool(reg340_pv_a_control_enabled) 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)
|
||||
@@ -1677,9 +1681,8 @@ async def write_inverter_setpoints(
|
||||
]
|
||||
)
|
||||
|
||||
mfr = (inv.manufacturer or "").strip().lower()
|
||||
if (
|
||||
mfr == "deye"
|
||||
bool(inv.deye_reg340_pv_a_control_enabled)
|
||||
and int(inv.pv_a_cap_w) > 0
|
||||
and setpoints_now.pv_a_allowed_w is not None
|
||||
):
|
||||
@@ -1826,7 +1829,8 @@ async def write_inverter_setpoints(
|
||||
|
||||
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, 340 (stejné TCP spojení jako telemetrie/export).
|
||||
Živé čtení holding registrů Deye 108, 109, 141–145, 178, 191 a volitelně 340
|
||||
(jen pokud `deye_reg340_pv_a_control_enabled`, jinak `reg340_max_solar_power_w` = null).
|
||||
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.
|
||||
"""
|
||||
@@ -1843,13 +1847,20 @@ 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)
|
||||
if inv.deye_reg340_pv_a_control_enabled:
|
||||
r340 = await mb.read_holding_registers(340, 1)
|
||||
else:
|
||||
r340 = None
|
||||
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
|
||||
r340v = (
|
||||
int(r340[0])
|
||||
if r340 is not None and len(r340) >= 1
|
||||
else None
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("read_deye_registers_live site=%s failed", site_id)
|
||||
raise
|
||||
@@ -1866,7 +1877,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),
|
||||
"reg340_max_solar_power_w": r340v,
|
||||
"read_at": read_at.isoformat(),
|
||||
}
|
||||
|
||||
@@ -2012,14 +2023,24 @@ async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None:
|
||||
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 ""
|
||||
reg340_en = (
|
||||
bool(inv_for_pv.deye_reg340_pv_a_control_enabled)
|
||||
if inv_for_pv is not None
|
||||
else False
|
||||
)
|
||||
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, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
|
||||
mode,
|
||||
pi_now,
|
||||
pv_a_cap_w=cap_pv,
|
||||
reg340_pv_a_control_enabled=reg340_en,
|
||||
)
|
||||
sp_next = _build_setpoints(
|
||||
mode, pi_next, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
|
||||
mode,
|
||||
pi_next,
|
||||
pv_a_cap_w=cap_pv,
|
||||
reg340_pv_a_control_enabled=reg340_en,
|
||||
)
|
||||
|
||||
if mode.mode_code == "AUTO" and sp_now is None:
|
||||
|
||||
@@ -53,12 +53,12 @@ class ComputePvAReg340Tests(unittest.TestCase):
|
||||
|
||||
|
||||
class BuildSetpointsReg340Tests(unittest.TestCase):
|
||||
def test_deye_with_cap_sets_pv_a_allowed(self) -> None:
|
||||
def test_with_cap_sets_pv_a_allowed(self) -> None:
|
||||
sp = _build_setpoints(
|
||||
_auto_mode(),
|
||||
_pi_base(pv_a_forecast_solver_w=8000, pv_a_curtailed_w=2000),
|
||||
pv_a_cap_w=10_000,
|
||||
inverter_manufacturer="Deye",
|
||||
reg340_pv_a_control_enabled=True,
|
||||
)
|
||||
assert sp is not None
|
||||
self.assertEqual(sp.pv_a_allowed_w, 6000)
|
||||
@@ -68,17 +68,7 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
|
||||
_auto_mode(),
|
||||
_pi_base(),
|
||||
pv_a_cap_w=0,
|
||||
inverter_manufacturer="Deye",
|
||||
)
|
||||
assert sp is not None
|
||||
self.assertIsNone(sp.pv_a_allowed_w)
|
||||
|
||||
def test_skipped_for_non_deye(self) -> None:
|
||||
sp = _build_setpoints(
|
||||
_auto_mode(),
|
||||
_pi_base(),
|
||||
pv_a_cap_w=10_000,
|
||||
inverter_manufacturer="Foo",
|
||||
reg340_pv_a_control_enabled=True,
|
||||
)
|
||||
assert sp is not None
|
||||
self.assertIsNone(sp.pv_a_allowed_w)
|
||||
@@ -92,9 +82,7 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
|
||||
heat_pump_enabled_def=False,
|
||||
loxone_mode_value=0,
|
||||
)
|
||||
sp = _build_setpoints(
|
||||
mode, None, pv_a_cap_w=10_000, inverter_manufacturer="Deye"
|
||||
)
|
||||
sp = _build_setpoints(mode, None, pv_a_cap_w=10_000)
|
||||
assert sp is not None
|
||||
self.assertIsNone(sp.pv_a_allowed_w)
|
||||
|
||||
@@ -109,11 +97,21 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
|
||||
pv_a_curtailed_w=0,
|
||||
),
|
||||
pv_a_cap_w=3333,
|
||||
inverter_manufacturer="Deye",
|
||||
reg340_pv_a_control_enabled=True,
|
||||
)
|
||||
assert sp is not None
|
||||
self.assertEqual(sp.pv_a_allowed_w, 0)
|
||||
|
||||
def test_skipped_when_reg340_control_disabled(self) -> None:
|
||||
sp = _build_setpoints(
|
||||
_auto_mode(),
|
||||
_pi_base(pv_a_forecast_solver_w=8000, pv_a_curtailed_w=2000),
|
||||
pv_a_cap_w=10_000,
|
||||
reg340_pv_a_control_enabled=False,
|
||||
)
|
||||
assert sp is not None
|
||||
self.assertIsNone(sp.pv_a_allowed_w)
|
||||
|
||||
|
||||
class Reg340VerifyPolicyTests(unittest.TestCase):
|
||||
def test_reg340_not_critical_for_self_sustain(self) -> None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Cap pro Deye reg 340 (max solar power, W): součet nominal_power_wp řiditelných PV polí na invertoru.
|
||||
-- Cap pro reg 340 max solar power (W): součet nominal_power_wp řiditelných PV polí na invertoru.
|
||||
create or replace function ems.fn_inverter_pv_a_max_w(p_inverter_id int)
|
||||
returns int
|
||||
language sql
|
||||
@@ -11,4 +11,4 @@ as $$
|
||||
$$;
|
||||
|
||||
comment on function ems.fn_inverter_pv_a_max_w(int) is
|
||||
'Cap pro Deye reg 340 (max solar power, W) = součet nominal_power_wp řiditelných PV polí na daném invertoru. 0 = EMS reg 340 neaktivní (skip zápisu).';
|
||||
'Cap pro reg 340 (max solar power, W) = součet nominal_power_wp řiditelných PV polí na daném invertoru. 0 = EMS reg 340 neaktivní (skip zápisu).';
|
||||
|
||||
25
db/routines/R__084_fn_site_has_active_green_bonus_pv.sql
Normal file
25
db/routines/R__084_fn_site_has_active_green_bonus_pv.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- True pokud má lokalita aspoň jedno PV pole se zeleným bonusem v aktuálním kalendářním dni (Europe/Prague).
|
||||
create or replace function ems.fn_site_has_active_green_bonus_pv(p_site_id int)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from ems.asset_pv_array apa
|
||||
where apa.site_id = p_site_id
|
||||
and apa.green_bonus_czk_kwh is not null
|
||||
and apa.green_bonus_czk_kwh > 0
|
||||
and (
|
||||
apa.green_bonus_valid_from is null
|
||||
or (timezone('Europe/Prague', now()))::date >= apa.green_bonus_valid_from
|
||||
)
|
||||
and (
|
||||
apa.green_bonus_valid_to is null
|
||||
or (timezone('Europe/Prague', now()))::date < apa.green_bonus_valid_to
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
comment on function ems.fn_site_has_active_green_bonus_pv(int) is
|
||||
'EMS reg 340: true pokud existuje PV pole na site se zeleným bonusem (green_bonus_czk_kwh > 0) platné dnes v Europe/Prague.';
|
||||
@@ -125,7 +125,7 @@ CREATE TABLE asset_battery (
|
||||
### `asset_pv_array`
|
||||
Každé FVE pole zvlášť – důležité pro predikci (azimut, sklon). **Zelený bonus** (dotace za vyrobenou elektřinu) se váže na **úroveň pole**, ne na celou lokalitu: které pole má bonus, jaká je sazba Kč/kWh a platnost, určují sloupce `green_bonus_*`. Výpočet příjmu za interval probíhá funkcí `ems.fn_green_bonus_revenue` z výroby v Wh; není součástí efektivní prodejní ceny ze sítě.
|
||||
|
||||
**Deye reg 340 (max solar power, W):** strop pro řiditelné DC pole A na hybridu počítá `ems.fn_inverter_pv_a_max_w(inverter_id)` jako **součet `nominal_power_wp`** řádků s `controllable = true` vázaných na daný invertor. Exportér (`exporter_monolith.write_inverter_setpoints`) tento cap použije jen u `asset_inverter.manufacturer` = Deye a **pouze pokud součet > 0**; při součtu 0 se reg 340 z EMS nezapisuje (nezasahuje do ručního nastavení v invertoru).
|
||||
**Deye reg 340 (max solar power, W):** strop pro řiditelné DC pole A na hybridu počítá `ems.fn_inverter_pv_a_max_w(inverter_id)` jako **součet `nominal_power_wp`** řádků s `controllable = true` vázaných na daný invertor. Zápis z EMS je povolen jen na lokalitách se **zeleným bonusem na PV poli** (`ems.fn_site_has_active_green_bonus_pv(site_id)` — aktivní `asset_pv_array.green_bonus_*` v kalendářním dni Europe/Prague); jinak EMS reg 340 nemění (invertor zůstane na poslední hodnotě).
|
||||
|
||||
```sql
|
||||
CREATE TABLE asset_pv_array (
|
||||
|
||||
@@ -134,9 +134,9 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg
|
||||
|
||||
### PV A curtailment — zápis reg 340 (max solar power)
|
||||
|
||||
- **Implementace:** `backend/services/control/exporter_monolith.py` — `export_setpoints` načte cap v `_load_inverter_config` (`ems.fn_inverter_pv_a_max_w(ai.id)`), `_build_setpoints` v režimu **AUTO** dopočítá `ControlSetpoints.pv_a_allowed_w`, `write_inverter_setpoints` zařadí **reg 340**, pokud je výrobce invertoru Deye, cap > 0 a `pv_a_allowed_w` je vyplněné.
|
||||
- **Implementace:** `backend/services/control/exporter_monolith.py` — `export_setpoints` načte cap v `_load_inverter_config` (`ems.fn_inverter_pv_a_max_w(ai.id)`), `_build_setpoints` v režimu **AUTO** dopočítá `ControlSetpoints.pv_a_allowed_w`, `write_inverter_setpoints` zařadí **reg 340**, pokud je `fn_site_has_active_green_bonus_pv` aktivní, cap > 0 a `pv_a_allowed_w` je vyplněné.
|
||||
- **Data:** `pv_a_forecast_solver_w` / `pv_a_curtailed_w` z aktivního `planning_interval` (json z `ems.fn_planning_interval_at_offset`); cap = součet `nominal_power_wp` řiditelných polí na invertoru (bez nového sloupce v DB).
|
||||
- **Home-01 policy (PV A off):** pokud jsou v aktuálním slotu zároveň `effective_buy_price < 0` a `effective_sell_price < 0` a zároveň `pv_b_forecast_solver_w > 0` (PV B vyrábí), exporter nastaví `pv_a_allowed_w = 0` (reg 340) i když je forecast PV A nulový — cílem je držet headroom v baterii pro PV B / další záporný nákup.
|
||||
- **Policy PV A off (jen na site se zeleným bonusem na PV):** pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` a v aktuálním slotu zároveň `effective_buy_price < 0` a `effective_sell_price < 0` a `pv_b_forecast_solver_w > 0` (PV B vyrábí), exporter nastaví `pv_a_allowed_w = 0` (reg 340) i když je forecast PV A nulový — cílem je držet headroom v baterii pro PV B / další záporný nákup.
|
||||
- **Verify:** reg **340** není kritický → po 3× mismatch verify **bez** přepnutí do SELF_SUSTAIN (stejně jako reg 178); viz [`modbus-command-journal.md`](modbus-command-journal.md).
|
||||
|
||||
#### Ověření po nasazení (smoke)
|
||||
|
||||
@@ -19,7 +19,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
|
||||
| 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**. |
|
||||
| 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 `manufacturer` = Deye a `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (součet `nominal_power_wp` controllable polí). 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` |
|
||||
| 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 |
|
||||
@@ -30,9 +30,9 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
|
||||
### Reg 340 (max solar power)
|
||||
|
||||
- **FC 0x10**, jednotka **W**; firmware omezuje strop výroby z řiditelných stringů (pole A na hybridu).
|
||||
- **Kdy EMS zapisuje:** `asset_inverter.manufacturer` odpovídá Deye **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (součet `nominal_power_wp` z `asset_pv_array` kde `controllable = true`). Při součtu **0** EMS reg 340 **nezapisuje** (např. odpojené pole A s `nominal_power_wp = 0` v DB — ruční hodnota v invertoru zůstane).
|
||||
- **Kdy EMS zapisuje:** `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (součet `nominal_power_wp` z `asset_pv_array` kde `controllable = true`). Při součtu **0** nebo bez aktivního zeleného bonusu EMS reg 340 **nezapisuje** (ruční hodnota v invertoru zůstane).
|
||||
- **Hodnota:** z `ControlSetpoints.pv_a_allowed_w` (AUTO): bez curtailmentu = plný cap; při `pv_a_curtailed_w > 0` viz tabulka výše. Režimy **SELF_SUSTAIN / PRESERVE / CHARGE_CHEAP** mají `pv_a_allowed_w = None` → žádný zápis 340 z EMS v daném ticku.
|
||||
- **Živé čtení:** `read_deye_registers_live` vrací **`reg340_max_solar_power_w`**.
|
||||
- **Živé čtení:** `read_deye_registers_live` vrací **`reg340_max_solar_power_w`** (integer) jen pokud je přepínač zapnutý; jinak **`null`** (bez extra FC3 čtení reg 340).
|
||||
|
||||
### Reg 191 (výkon grid peak shaving)
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@ COMMENT ON COLUMN ems.planning_interval.pv_a_curtailed_w IS
|
||||
'Hodnota > 0 znamená že solver rozhodl omezit výrobu pole A přes Modbus.';
|
||||
```
|
||||
|
||||
**Fyzická realizace na Deye (bez změny solveru):** u hybridu s `manufacturer = Deye` a nenulovým součtem `nominal_power_wp` controllable polí na invertoru exportér mapuje `pv_a_forecast_solver_w` / `pv_a_curtailed_w` na zápis **holding registru 340** (max solar power, W) — viz [`control.md`](control.md) sekce *PV A curtailment* a [`modbus-registers.md`](modbus-registers.md) reg 340.
|
||||
**Fyzická realizace na hybridu (bez změny solveru):** při `ems.fn_site_has_active_green_bonus_pv(site_id)` a nenulovém součtu `nominal_power_wp` controllable polí na invertoru exportér mapuje `pv_a_forecast_solver_w` / `pv_a_curtailed_w` na zápis **holding registru 340** (max solar power, W) — viz [`control.md`](control.md) sekce *PV A curtailment* a [`modbus-registers.md`](modbus-registers.md) reg 340.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user