fix mismatch rs485
This commit is contained in:
@@ -34,6 +34,12 @@ BATT_VOLTAGE_V = 51.2
|
||||
# Reg 178 – pevné hodnoty (bit4–5); bez read-modify-write (kolize s Loxone / transaction ID)
|
||||
REG178_SELL = 0b00100000 # 32, grid peak shaving disable
|
||||
REG178_PASSIVE = 0b00110000 # 48, grid peak shaving enable (PASSIVE i CHARGE)
|
||||
# Verify: jen bity 4–5 (horní byte layout v dokumentaci); ostatní bity mohou mít firmware / Loxone
|
||||
REG178_VERIFY_MASK = 0x0030
|
||||
|
||||
|
||||
def _deye_reg178_verify_match(expected_i: int, actual_i: int) -> bool:
|
||||
return (int(expected_i) & REG178_VERIFY_MASK) == (int(actual_i) & REG178_VERIFY_MASK)
|
||||
|
||||
# Neaktivní TOU bloky (3–6): „konec dne“ — Deye často 23:59 (2359) neuloží a vrátí např. 2355,
|
||||
# verify pak hlásí mismatch. 23:55 je na zařízeních stabilní (viz HHMM jako desítkové číslo).
|
||||
@@ -90,6 +96,17 @@ def battery_watts_to_amps(power_w: int, max_amps: int) -> int:
|
||||
return min(max(0, max_amps), max(0, round(abs(power_w) / BATT_VOLTAGE_V)))
|
||||
|
||||
|
||||
def _effective_battery_current_caps(inv: InverterConfig) -> tuple[int, int]:
|
||||
"""Efektivní stropy pro reg 108/109 po zohlednění volitelných Deye limitů v DB."""
|
||||
ca = int(inv.max_charge_a)
|
||||
da = int(inv.max_discharge_a)
|
||||
if inv.deye_register_max_charge_a is not None:
|
||||
ca = min(ca, int(inv.deye_register_max_charge_a))
|
||||
if inv.deye_register_max_discharge_a is not None:
|
||||
da = min(da, int(inv.deye_register_max_discharge_a))
|
||||
return ca, da
|
||||
|
||||
|
||||
def current_slot_hhmm() -> int:
|
||||
"""Začátek probíhajícího 15min slotu v Europe/Prague, formát HHMM (např. 1415)."""
|
||||
now = datetime.now(ZoneInfo("Europe/Prague"))
|
||||
@@ -129,6 +146,8 @@ class InverterConfig:
|
||||
usable_capacity_wh: int | None
|
||||
max_charge_a: int
|
||||
max_discharge_a: int
|
||||
deye_register_max_charge_a: int | None = None
|
||||
deye_register_max_discharge_a: int | None = None
|
||||
deye_last_system_time_sync_minute: datetime | None = None
|
||||
deye_last_system_time_sync_at: datetime | None = None
|
||||
deye_last_tou_inactive_write_prague_date: date | None = None
|
||||
@@ -664,30 +683,78 @@ async def verify_modbus_commands(
|
||||
"""
|
||||
from services.notification_service import notify_modbus_mismatch
|
||||
|
||||
async def _apply_verify_result(cmd: asyncpg.Record, actual_i: int) -> bool:
|
||||
async def _apply_verify_result(
|
||||
cmd: asyncpg.Record,
|
||||
actual_i: int,
|
||||
*,
|
||||
client: Any,
|
||||
unit: int,
|
||||
) -> bool:
|
||||
"""Vrátí True při shodě, False při mismatch (a obslouží retry / SELF_SUSTAIN)."""
|
||||
reg = int(cmd["register"])
|
||||
cmd_id = int(cmd["id"])
|
||||
|
||||
if reg in DEYE_CLOCK_REGS:
|
||||
asset_id = int(cmd["asset_id"])
|
||||
host = str(cmd["device_host"])
|
||||
port_i = int(cmd["device_port"])
|
||||
uid = int(cmd["device_unit_id"])
|
||||
bundle = await _fetch_written_deye_clock_commands(
|
||||
site_id, asset_id, host, port_i, uid, db
|
||||
)
|
||||
if not bundle:
|
||||
bundle = [cmd]
|
||||
try:
|
||||
cvals = await client.read_holding_registers(62, 3, uid)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"verify clock guard read 62–64 failed (reg 0x%04X): %s", reg, e
|
||||
)
|
||||
return False
|
||||
if len(cvals) != 3:
|
||||
logger.error(
|
||||
"verify clock guard: expected 3 regs, got %s", len(cvals)
|
||||
)
|
||||
return False
|
||||
logger.warning(
|
||||
"Clock register 0x%04X reached strict verify path; using tolerant 62–64 bundle",
|
||||
reg,
|
||||
)
|
||||
return await _verify_deye_clock_written_bundle(
|
||||
site_id,
|
||||
bundle,
|
||||
int(cvals[0]),
|
||||
int(cvals[1]),
|
||||
int(cvals[2]),
|
||||
db,
|
||||
)
|
||||
|
||||
expected_i = int(cmd["value_to_write"])
|
||||
matches = actual_i == expected_i
|
||||
if reg == 178:
|
||||
matches = _deye_reg178_verify_match(expected_i, actual_i)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE ems.modbus_command
|
||||
SET value_verified=$1::int, verified_at=now(),
|
||||
status=CASE WHEN $1::int = $2::int THEN 'verified' ELSE 'mismatch' END
|
||||
status=CASE WHEN $2::boolean THEN 'verified' ELSE 'mismatch' END
|
||||
WHERE id=$3::int
|
||||
""",
|
||||
actual_i,
|
||||
expected_i,
|
||||
matches,
|
||||
cmd_id,
|
||||
)
|
||||
|
||||
if actual_i != expected_i:
|
||||
if not matches:
|
||||
logger.error(
|
||||
"[cmd %s] MISMATCH %s 0x%04X: expected=%s actual=%s",
|
||||
"[cmd %s] MISMATCH %s 0x%04X: expected=%s actual=%s%s",
|
||||
cmd_id,
|
||||
cmd["asset_code"],
|
||||
int(cmd["register"]),
|
||||
reg,
|
||||
expected_i,
|
||||
actual_i,
|
||||
" (reg178 mask 0x%04X)" % REG178_VERIFY_MASK if reg == 178 else "",
|
||||
)
|
||||
row_ac = await db.fetchrow(
|
||||
"SELECT attempt_count FROM ems.modbus_command WHERE id=$1", cmd_id
|
||||
@@ -695,7 +762,7 @@ async def verify_modbus_commands(
|
||||
attempts = int(row_ac["attempt_count"] or 0) if row_ac else 0
|
||||
await notify_modbus_mismatch(
|
||||
cmd["asset_code"],
|
||||
int(cmd["register"]),
|
||||
reg,
|
||||
cmd["register_name"] or "",
|
||||
expected_i,
|
||||
actual_i,
|
||||
@@ -719,18 +786,28 @@ async def verify_modbus_commands(
|
||||
db,
|
||||
reason=(
|
||||
f"Modbus mismatch po 3 pokusech: {cmd['asset_code']} "
|
||||
f"reg 0x{cmd['register']:04X}"
|
||||
f"reg 0x{reg:04X}"
|
||||
),
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(
|
||||
"[cmd %s] verified OK: %s 0x%04X=%s",
|
||||
cmd_id,
|
||||
cmd["asset_code"],
|
||||
int(cmd["register"]),
|
||||
actual_i,
|
||||
)
|
||||
if reg == 178 and actual_i != expected_i:
|
||||
logger.info(
|
||||
"[cmd %s] verified OK (reg178 masked): %s 0x%04X value_to_write=%s actual=%s",
|
||||
cmd_id,
|
||||
cmd["asset_code"],
|
||||
reg,
|
||||
expected_i,
|
||||
actual_i,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"[cmd %s] verified OK: %s 0x%04X=%s",
|
||||
cmd_id,
|
||||
cmd["asset_code"],
|
||||
reg,
|
||||
actual_i,
|
||||
)
|
||||
return True
|
||||
|
||||
cmds: list[asyncpg.Record] = []
|
||||
@@ -807,7 +884,9 @@ async def verify_modbus_commands(
|
||||
all_ok = False
|
||||
continue
|
||||
for cmd, actual in zip(run, values):
|
||||
matched = await _apply_verify_result(cmd, int(actual))
|
||||
matched = await _apply_verify_result(
|
||||
cmd, int(actual), client=client, unit=unit
|
||||
)
|
||||
if not matched:
|
||||
all_ok = False
|
||||
|
||||
@@ -891,6 +970,8 @@ async def _load_inverter_config(
|
||||
ai.deye_last_system_time_sync_at,
|
||||
ai.deye_last_tou_inactive_write_prague_date,
|
||||
ai.deye_tou_inactive_signature,
|
||||
ai.deye_register_max_charge_a,
|
||||
ai.deye_register_max_discharge_a,
|
||||
LEAST(
|
||||
COALESCE(ab.bms_max_charge_w, ai.max_battery_charge_w),
|
||||
ai.max_battery_charge_w
|
||||
@@ -954,6 +1035,12 @@ async def _load_inverter_config(
|
||||
else None,
|
||||
max_charge_a=max_charge_a,
|
||||
max_discharge_a=max_discharge_a,
|
||||
deye_register_max_charge_a=int(row["deye_register_max_charge_a"])
|
||||
if row["deye_register_max_charge_a"] is not None
|
||||
else None,
|
||||
deye_register_max_discharge_a=int(row["deye_register_max_discharge_a"])
|
||||
if row["deye_register_max_discharge_a"] is not None
|
||||
else None,
|
||||
deye_last_system_time_sync_minute=row["deye_last_system_time_sync_minute"],
|
||||
deye_last_system_time_sync_at=row["deye_last_system_time_sync_at"],
|
||||
deye_last_tou_inactive_write_prague_date=row[
|
||||
@@ -1197,12 +1284,17 @@ def get_deye_mode(setpoints: ControlSetpoints) -> str:
|
||||
def _deye_tou_params(
|
||||
setpoints: ControlSetpoints,
|
||||
inv: InverterConfig,
|
||||
*,
|
||||
max_charge_a_cap: int | None = None,
|
||||
max_discharge_a_cap: int | None = None,
|
||||
) -> tuple[int, int, bool]:
|
||||
"""
|
||||
Parametry jednoho Deye time pointu: výkon W, SOC min %, grid_charge.
|
||||
Musí odpovídat logice get_deye_mode / lock_battery v write_inverter_setpoints.
|
||||
"""
|
||||
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
|
||||
md = int(max_discharge_a_cap) if max_discharge_a_cap is not None else int(inv.max_discharge_a)
|
||||
mc = int(max_charge_a_cap) if max_charge_a_cap is not None else int(inv.max_charge_a)
|
||||
max_batt_w_discharge = int(md * BATT_VOLTAGE_V)
|
||||
tp_discharge_w = 0 if setpoints.lock_battery else max_batt_w_discharge
|
||||
tou_min = _deye_tou_min_soc_pct(inv)
|
||||
tou_reserve = _deye_tou_reserve_soc_pct(inv)
|
||||
@@ -1214,7 +1306,7 @@ def _deye_tou_params(
|
||||
battery_w = int(raw_bat) if raw_bat is not None else 0
|
||||
cap = int(inv.max_soc_percent) if inv.max_soc_percent is not None else 95
|
||||
target_soc = max(10, min(95, cap))
|
||||
tp_charge_w = battery_watts_to_amps(battery_w, inv.max_charge_a) * int(BATT_VOLTAGE_V)
|
||||
tp_charge_w = battery_watts_to_amps(battery_w, mc) * int(BATT_VOLTAGE_V)
|
||||
return tp_charge_w, target_soc, True
|
||||
if deye_mode == "SELL":
|
||||
return tp_discharge_w, tou_reserve, False
|
||||
@@ -1236,7 +1328,8 @@ async def write_inverter_setpoints(
|
||||
grid_w = int(setpoints_now.grid_setpoint_w or 0)
|
||||
no_export = inv.no_export
|
||||
export_lim = _deye_reg143_export_w(no_export, inv.max_export_power_w)
|
||||
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
|
||||
eff_ca, eff_da = _effective_battery_current_caps(inv)
|
||||
max_batt_w_discharge = int(eff_da * BATT_VOLTAGE_V)
|
||||
tp_discharge_w = 0 if setpoints_now.lock_battery else max_batt_w_discharge
|
||||
tou_min_pct = _deye_tou_min_soc_pct(inv)
|
||||
tou_reserve_pct = _deye_tou_reserve_soc_pct(inv)
|
||||
@@ -1251,11 +1344,11 @@ async def write_inverter_setpoints(
|
||||
discharge_a = 0
|
||||
elif deye_mode == "CHARGE":
|
||||
battery_w = int(raw_bat) if raw_bat is not None else 0
|
||||
charge_a = battery_watts_to_amps(battery_w, inv.max_charge_a)
|
||||
charge_a = battery_watts_to_amps(battery_w, eff_ca)
|
||||
discharge_a = 0
|
||||
else:
|
||||
charge_a = int(inv.max_charge_a)
|
||||
discharge_a = int(inv.max_discharge_a)
|
||||
charge_a = int(eff_ca)
|
||||
discharge_a = int(eff_da)
|
||||
|
||||
selling_mode = 0 if deye_mode == "SELL" else 1
|
||||
export_limit = export_lim
|
||||
@@ -1303,8 +1396,12 @@ async def write_inverter_setpoints(
|
||||
sp_tp2 = setpoints_next if setpoints_next is not None else setpoints_now
|
||||
hh_cur = current_slot_hhmm()
|
||||
hh_nxt = next_slot_hhmm()
|
||||
p1, s1, g1 = _deye_tou_params(setpoints_now, inv)
|
||||
p2, s2, g2 = _deye_tou_params(sp_tp2, inv)
|
||||
p1, s1, g1 = _deye_tou_params(
|
||||
setpoints_now, inv, max_charge_a_cap=eff_ca, max_discharge_a_cap=eff_da
|
||||
)
|
||||
p2, s2, g2 = _deye_tou_params(
|
||||
sp_tp2, inv, max_charge_a_cap=eff_ca, max_discharge_a_cap=eff_da
|
||||
)
|
||||
registers.extend(_deye_time_point_rows(0, hh_cur, p1, s1, g1))
|
||||
registers.extend(_deye_time_point_rows(1, hh_nxt, p2, s2, g2))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user