fix ranniho neprodeje do site
This commit is contained in:
@@ -37,6 +37,7 @@ from services.control.modbus_journal import (
|
|||||||
from services.control.models import ControlSetpoints
|
from services.control.models import ControlSetpoints
|
||||||
from services.control.repository import _get_current_soc, _load_inverter_config
|
from services.control.repository import _get_current_soc, _load_inverter_config
|
||||||
from services.control.setpoints import (
|
from services.control.setpoints import (
|
||||||
|
_deye_reg142_limit_control,
|
||||||
_deye_reg143_export_w,
|
_deye_reg143_export_w,
|
||||||
_deye_system_time_register_rows,
|
_deye_system_time_register_rows,
|
||||||
_deye_time_point_rows,
|
_deye_time_point_rows,
|
||||||
@@ -66,7 +67,10 @@ async def write_inverter_setpoints(
|
|||||||
raw_bat = setpoints_now.battery_w
|
raw_bat = setpoints_now.battery_w
|
||||||
grid_w = int(setpoints_now.grid_setpoint_w or 0)
|
grid_w = int(setpoints_now.grid_setpoint_w or 0)
|
||||||
no_export = inv.no_export
|
no_export = inv.no_export
|
||||||
export_lim = _deye_reg143_export_w(no_export, inv.max_export_power_w)
|
export_lim_hw = _deye_reg143_export_w(no_export, inv.max_export_power_w)
|
||||||
|
export_lim = export_lim_hw
|
||||||
|
if int(setpoints_now.grid_export_limit or 0) > 0:
|
||||||
|
export_lim = min(export_lim_hw, int(setpoints_now.grid_export_limit))
|
||||||
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
|
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
|
||||||
tp_discharge_w = 0 if setpoints_now.lock_battery else max_batt_w_discharge
|
tp_discharge_w = 0 if setpoints_now.lock_battery else max_batt_w_discharge
|
||||||
tou_min_pct = _deye_tou_min_soc_pct(inv)
|
tou_min_pct = _deye_tou_min_soc_pct(inv)
|
||||||
@@ -88,7 +92,13 @@ async def write_inverter_setpoints(
|
|||||||
)
|
)
|
||||||
|
|
||||||
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
|
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
|
||||||
selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
|
selling_mode = _deye_reg142_limit_control(
|
||||||
|
deye_mode=deye_mode,
|
||||||
|
grid_w=grid_w,
|
||||||
|
export_ban=bool(setpoints_now.export_ban),
|
||||||
|
export_mode=setpoints_now.export_mode,
|
||||||
|
zero_export_mode=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
|
||||||
reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE
|
reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class ControlSetpoints:
|
|||||||
target_soc_pct: int | None = None
|
target_soc_pct: int | None = None
|
||||||
#: Explicitní fyzický režim z plánu (PASSIVE/SELL/CHARGE).
|
#: Explicitní fyzický režim z plánu (PASSIVE/SELL/CHARGE).
|
||||||
deye_physical_mode: str | None = None
|
deye_physical_mode: str | None = None
|
||||||
|
#: Záměr exportu z LP: NONE / PV_SURPLUS / BATTERY_SELL.
|
||||||
|
export_mode: str | None = None
|
||||||
#: True = zákaz exportu (BLOCK_EXPORT) pro daný slot.
|
#: True = zákaz exportu (BLOCK_EXPORT) pro daný slot.
|
||||||
export_ban: bool = False
|
export_ban: bool = False
|
||||||
#: True = odpojit GEN port (MI export cutoff) v tomto slotu dle plánu (reg 178 bits0-1).
|
#: True = odpojit GEN port (MI export cutoff) v tomto slotu dle plánu (reg 178 bits0-1).
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ def _build_setpoints(
|
|||||||
export_mode = str(export_mode_raw).strip().upper() if export_mode_raw is not None else None
|
export_mode = str(export_mode_raw).strip().upper() if export_mode_raw is not None else None
|
||||||
if export_mode == "NONE":
|
if export_mode == "NONE":
|
||||||
export_limit = 0
|
export_limit = 0
|
||||||
|
elif export_limit <= 0 and grid_sp < 0:
|
||||||
|
export_limit = abs(grid_sp)
|
||||||
# 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")
|
||||||
@@ -127,6 +129,7 @@ def _build_setpoints(
|
|||||||
ev2_power_w=ev2_w,
|
ev2_power_w=ev2_w,
|
||||||
target_soc_pct=target_soc,
|
target_soc_pct=target_soc,
|
||||||
deye_physical_mode=pm,
|
deye_physical_mode=pm,
|
||||||
|
export_mode=export_mode,
|
||||||
export_ban=bool(export_ban),
|
export_ban=bool(export_ban),
|
||||||
deye_gen_cutoff_enabled=bool(gen_cutoff),
|
deye_gen_cutoff_enabled=bool(gen_cutoff),
|
||||||
effective_sell_price_czk_kwh=sell_f,
|
effective_sell_price_czk_kwh=sell_f,
|
||||||
@@ -214,6 +217,30 @@ def _deye_reg143_export_w(no_export: bool, max_export_power_w: int | None) -> in
|
|||||||
return max(0, int(max_export_power_w or 0))
|
return max(0, int(max_export_power_w or 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _deye_reg142_limit_control(
|
||||||
|
*,
|
||||||
|
deye_mode: str,
|
||||||
|
grid_w: int,
|
||||||
|
export_ban: bool,
|
||||||
|
export_mode: str | None,
|
||||||
|
zero_export_mode: int,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Reg 142: 0 = selling first, 1/2 = zero export (load / CT).
|
||||||
|
Plán s exportem (záporný grid_setpoint, bez export_ban) musí povolit prodej FVE do sítě
|
||||||
|
i v PASSIVE — jinak CT instalace (deye_zero_export_mode=2) drží přebytek v baterii.
|
||||||
|
"""
|
||||||
|
if deye_mode == "SELL":
|
||||||
|
return 0
|
||||||
|
em = (export_mode or "").strip().upper()
|
||||||
|
if export_ban or em == "NONE" or grid_w >= 0:
|
||||||
|
return int(zero_export_mode)
|
||||||
|
if em in {"PV_SURPLUS", "BATTERY_SELL"}:
|
||||||
|
return 0
|
||||||
|
# starší řádky bez export_mode: záporný grid_setpoint = export záměr
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _clamp_deye_tou_soc_pct(pct: int) -> int:
|
def _clamp_deye_tou_soc_pct(pct: int) -> int:
|
||||||
return max(5, min(95, pct))
|
return max(5, min(95, pct))
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ from services.control.exporter_monolith import (
|
|||||||
get_deye_mode,
|
get_deye_mode,
|
||||||
)
|
)
|
||||||
from services.control.models import OperatingModeInfo
|
from services.control.models import OperatingModeInfo
|
||||||
from services.control.setpoints import _build_setpoints, _deye_zero_export_amps_for_passive
|
from services.control.setpoints import (
|
||||||
|
_build_setpoints,
|
||||||
|
_deye_reg142_limit_control,
|
||||||
|
_deye_zero_export_amps_for_passive,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _inv(*, min_soc: int | None = 12, reserve_soc: int | None = 20) -> InverterConfig:
|
def _inv(*, min_soc: int | None = 12, reserve_soc: int | None = 20) -> InverterConfig:
|
||||||
@@ -111,6 +115,43 @@ class DeyeTouParamsTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
self.assertEqual(get_deye_mode(sp), "PASSIVE")
|
||||||
|
|
||||||
|
def test_reg142_pv_surplus_on_ct_site_uses_selling_first(self) -> None:
|
||||||
|
"""KV1/BA81: PASSIVE + plánovaný export FVE nesmí nechat reg142=2 (zero export CT)."""
|
||||||
|
self.assertEqual(
|
||||||
|
_deye_reg142_limit_control(
|
||||||
|
deye_mode="PASSIVE",
|
||||||
|
grid_w=-3000,
|
||||||
|
export_ban=False,
|
||||||
|
export_mode="PV_SURPLUS",
|
||||||
|
zero_export_mode=2,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reg142_no_export_when_export_ban(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
_deye_reg142_limit_control(
|
||||||
|
deye_mode="PASSIVE",
|
||||||
|
grid_w=-1000,
|
||||||
|
export_ban=True,
|
||||||
|
export_mode="PV_SURPLUS",
|
||||||
|
zero_export_mode=2,
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reg142_legacy_negative_grid_without_export_mode(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
_deye_reg142_limit_control(
|
||||||
|
deye_mode="PASSIVE",
|
||||||
|
grid_w=-2500,
|
||||||
|
export_ban=False,
|
||||||
|
export_mode=None,
|
||||||
|
zero_export_mode=2,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
def test_build_setpoints_uses_explicit_export_limit(self) -> None:
|
def test_build_setpoints_uses_explicit_export_limit(self) -> None:
|
||||||
mode = OperatingModeInfo(
|
mode = OperatingModeInfo(
|
||||||
mode_code="AUTO",
|
mode_code="AUTO",
|
||||||
|
|||||||
10
db/migration/V078__planning_interval_export.sql
Normal file
10
db/migration/V078__planning_interval_export.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- export_mode / export_limit_w z LP — potřeba pro control exporter (reg 142/143)
|
||||||
|
|
||||||
|
alter table ems.planning_interval
|
||||||
|
add column if not exists export_mode text,
|
||||||
|
add column if not exists export_limit_w int;
|
||||||
|
|
||||||
|
comment on column ems.planning_interval.export_mode is
|
||||||
|
'Záměr exportu z solveru: NONE / PV_SURPLUS / BATTERY_SELL.';
|
||||||
|
comment on column ems.planning_interval.export_limit_w is
|
||||||
|
'Tvrdý limit exportu do sítě (W) v slotu; 0 = bez exportu.';
|
||||||
@@ -57,6 +57,8 @@ begin
|
|||||||
run_id, interval_start,
|
run_id, interval_start,
|
||||||
battery_setpoint_w, battery_soc_target_pct,
|
battery_setpoint_w, battery_soc_target_pct,
|
||||||
grid_setpoint_w,
|
grid_setpoint_w,
|
||||||
|
export_mode,
|
||||||
|
export_limit_w,
|
||||||
deye_physical_mode,
|
deye_physical_mode,
|
||||||
deye_gen_cutoff_enabled,
|
deye_gen_cutoff_enabled,
|
||||||
ev1_setpoint_w, ev2_setpoint_w, ev1_via_bat_w, ev2_via_bat_w,
|
ev1_setpoint_w, ev2_setpoint_w, ev1_via_bat_w, ev2_via_bat_w,
|
||||||
@@ -73,6 +75,8 @@ begin
|
|||||||
(r.value->>'battery_setpoint_w')::int,
|
(r.value->>'battery_setpoint_w')::int,
|
||||||
(r.value->>'battery_soc_target_pct')::numeric,
|
(r.value->>'battery_soc_target_pct')::numeric,
|
||||||
(r.value->>'grid_setpoint_w')::int,
|
(r.value->>'grid_setpoint_w')::int,
|
||||||
|
nullif(trim(r.value->>'export_mode'), ''),
|
||||||
|
nullif(r.value->>'export_limit_w', '')::int,
|
||||||
nullif(trim(r.value->>'deye_physical_mode'), ''),
|
nullif(trim(r.value->>'deye_physical_mode'), ''),
|
||||||
(r.value->>'deye_gen_cutoff_enabled')::boolean,
|
(r.value->>'deye_gen_cutoff_enabled')::boolean,
|
||||||
nullif(r.value->>'ev1_setpoint_w', '')::int,
|
nullif(r.value->>'ev1_setpoint_w', '')::int,
|
||||||
@@ -97,6 +101,8 @@ begin
|
|||||||
run_id, interval_start,
|
run_id, interval_start,
|
||||||
battery_setpoint_w, battery_soc_target_pct,
|
battery_setpoint_w, battery_soc_target_pct,
|
||||||
grid_setpoint_w,
|
grid_setpoint_w,
|
||||||
|
export_mode,
|
||||||
|
export_limit_w,
|
||||||
deye_physical_mode,
|
deye_physical_mode,
|
||||||
deye_gen_cutoff_enabled,
|
deye_gen_cutoff_enabled,
|
||||||
ev1_setpoint_w, ev2_setpoint_w, ev1_via_bat_w, ev2_via_bat_w,
|
ev1_setpoint_w, ev2_setpoint_w, ev1_via_bat_w, ev2_via_bat_w,
|
||||||
@@ -110,6 +116,8 @@ begin
|
|||||||
(r.value->>'battery_setpoint_w')::int,
|
(r.value->>'battery_setpoint_w')::int,
|
||||||
(r.value->>'battery_soc_target_pct')::numeric,
|
(r.value->>'battery_soc_target_pct')::numeric,
|
||||||
(r.value->>'grid_setpoint_w')::int,
|
(r.value->>'grid_setpoint_w')::int,
|
||||||
|
nullif(trim(r.value->>'export_mode'), ''),
|
||||||
|
nullif(r.value->>'export_limit_w', '')::int,
|
||||||
nullif(trim(r.value->>'deye_physical_mode'), ''),
|
nullif(trim(r.value->>'deye_physical_mode'), ''),
|
||||||
(r.value->>'deye_gen_cutoff_enabled')::boolean,
|
(r.value->>'deye_gen_cutoff_enabled')::boolean,
|
||||||
nullif(r.value->>'ev1_setpoint_w', '')::int,
|
nullif(r.value->>'ev1_setpoint_w', '')::int,
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
- Odešle potvrzovací setpointy do Loxone přes HTTP (Loxone jako exekutor fallback logiky)
|
- Odešle potvrzovací setpointy do Loxone přes HTTP (Loxone jako exekutor fallback logiky)
|
||||||
- Loguje každý write pro audit
|
- Loguje každý write pro audit
|
||||||
|
|
||||||
|
### `export_mode` / `export_limit_w` (V078+)
|
||||||
|
|
||||||
|
Solver ukládá do `planning_interval` záměr exportu (`NONE` / `PV_SURPLUS` / `BATTERY_SELL`) a cap `export_limit_w`. Control exporter podle toho nastaví **reg 142** a **reg 143** — u CT instalací (`deye_zero_export_mode = 2`) musí být při plánovaném exportu FVE **reg 142 = 0** (selling first), jinak invertor drží přebytek v bateri místo v síti.
|
||||||
|
|
||||||
|
Implementace: `setpoints._deye_reg142_limit_control`, `inverter.write_inverter_setpoints`. Ověření: log `reg142=0` při `export_mode=PV_SURPLUS`, audit `flow_pv_to_grid_wh` > 0.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architektura řízení
|
## Architektura řízení
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_s
|
|||||||
| **Deye mode** | CHARGE | PASSIVE | SELL | PASSIVE |
|
| **Deye mode** | CHARGE | PASSIVE | SELL | PASSIVE |
|
||||||
| **108** charge A | dle `bat_w` | **0** při exportu bez vybíjení | **0** | max nebo **0** dle varianty |
|
| **108** charge A | dle `bat_w` | **0** při exportu bez vybíjení | **0** | max nebo **0** dle varianty |
|
||||||
| **109** discharge A | **0** | max | **max** | **0** při importu bez nabíjení, jinak max |
|
| **109** discharge A | **0** | max | **max** | **0** při importu bez nabíjení, jinak max |
|
||||||
| **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) |
|
| **142** limit control | `deye_zero_export_mode` (1 nebo 2) | **0** (selling first) pokud plán exportuje (`export_mode` PV_SURPLUS / záporný `grid_setpoint_w`, bez `export_ban`); jinak `deye_zero_export_mode` | **0** (selling first) | `deye_zero_export_mode` (1 nebo 2) |
|
||||||
| **145** solar sell | **1** (enabled) | **1** (enabled) | **1** (enabled) | **1** (enabled) |
|
| **145** solar sell | **1** (enabled) | **1** (enabled) | **1** (enabled) | **1** (enabled) |
|
||||||
| **178** peak shaving | 48 (PASSIVE) | 48 (PASSIVE) | **32** (SELL) | 48 (PASSIVE) |
|
| **178** peak shaving | 48 (PASSIVE) | 48 (PASSIVE) | **32** (SELL) | 48 (PASSIVE) |
|
||||||
| **143** export limit | max export W z DB | max export W z DB | max export W z DB | max export W z DB |
|
| **143** export limit | max export W z DB | max export W z DB | max export W z DB | max export W z DB |
|
||||||
|
|||||||
@@ -342,7 +342,9 @@ function deyeModeBadge(i: PlanningIntervalDto): { label: string; klass: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
let variant = 'max/max'
|
let variant = 'max/max'
|
||||||
if (grid_w < 0 && battery_w >= 0) {
|
if (exportMode === 'PV_SURPLUS' && grid_w < 0) {
|
||||||
|
variant = 'FVE→síť'
|
||||||
|
} else if (grid_w < 0 && battery_w >= 0) {
|
||||||
variant = exportMode === 'PV_SURPLUS' ? 'FVE→síť' : 'export'
|
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)'
|
||||||
@@ -394,6 +396,7 @@ type ChartRow = {
|
|||||||
pv_a_w: number
|
pv_a_w: number
|
||||||
battery_soc_target_pct: number | null
|
battery_soc_target_pct: number | null
|
||||||
battery_setpoint_w: number
|
battery_setpoint_w: number
|
||||||
|
grid_setpoint_w: number
|
||||||
compare_battery_setpoint_w?: number | null
|
compare_battery_setpoint_w?: number | null
|
||||||
effective_buy_price: number | null
|
effective_buy_price: number | null
|
||||||
raw: PlanningIntervalDto
|
raw: PlanningIntervalDto
|
||||||
@@ -731,6 +734,7 @@ export default function Planning() {
|
|||||||
pv_a_w: pvChartFveW(i, nowMs),
|
pv_a_w: pvChartFveW(i, nowMs),
|
||||||
battery_soc_target_pct: i.battery_soc_target_pct,
|
battery_soc_target_pct: i.battery_soc_target_pct,
|
||||||
battery_setpoint_w: i.battery_setpoint_w ?? 0,
|
battery_setpoint_w: i.battery_setpoint_w ?? 0,
|
||||||
|
grid_setpoint_w: i.grid_setpoint_w ?? 0,
|
||||||
compare_battery_setpoint_w: compareMap.get(i.interval_start)?.battery_setpoint_w ?? null,
|
compare_battery_setpoint_w: compareMap.get(i.interval_start)?.battery_setpoint_w ?? null,
|
||||||
effective_buy_price: i.effective_buy_price,
|
effective_buy_price: i.effective_buy_price,
|
||||||
raw: i,
|
raw: i,
|
||||||
@@ -1213,6 +1217,16 @@ export default function Planning() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Bar>
|
</Bar>
|
||||||
|
<Line
|
||||||
|
yAxisId="power"
|
||||||
|
type="monotone"
|
||||||
|
dataKey="grid_setpoint_w"
|
||||||
|
name="Síť W (+odb / −exp)"
|
||||||
|
stroke="#f87171"
|
||||||
|
dot={false}
|
||||||
|
strokeWidth={2}
|
||||||
|
connectNulls
|
||||||
|
/>
|
||||||
<Line
|
<Line
|
||||||
yAxisId="power"
|
yAxisId="power"
|
||||||
type="monotone"
|
type="monotone"
|
||||||
|
|||||||
Reference in New Issue
Block a user