third version before modbus cleanup

This commit is contained in:
Dusan Vojacek
2026-04-03 16:03:06 +02:00
parent 9f4126946d
commit 182d5a37e1
18 changed files with 846 additions and 128 deletions

View File

@@ -25,6 +25,10 @@ BATT_VOLTAGE_V = 51.2
REG178_SELL = 0b00100000 # 32, grid peak shaving disable
REG178_PASSIVE = 0b00110000 # 48, grid peak shaving enable (PASSIVE i CHARGE)
# Neaktivní TOU bloky (36): „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).
DEYE_TOU_INACTIVE_HHMM = 2355
DEYE_REGISTER_NAMES: dict[int, str] = {
108: "max_charge_a (max nabíjecí proud baterie)",
109: "max_discharge_a (max vybíjecí proud baterie)",
@@ -97,6 +101,7 @@ class InverterConfig:
max_battery_charge_w: int | None
max_battery_discharge_w: int | None
reserve_soc_percent: int | None
max_soc_percent: int | None
usable_capacity_wh: int | None
max_charge_a: int
max_discharge_a: int
@@ -195,13 +200,14 @@ async def execute_modbus_commands(
)
if cmd is None:
continue
client = await get_modbus_client(
cmd["device_host"], int(cmd["device_port"]), int(cmd["device_unit_id"])
)
unit = int(cmd["device_unit_id"])
client = await get_modbus_client(cmd["device_host"], int(cmd["device_port"]))
for attempt in range(MAX_RETRIES):
try:
await client.write_registers(
int(cmd["register"]), [int(cmd["value_to_write"])]
int(cmd["register"]),
[int(cmd["value_to_write"])],
unit,
)
await db.execute(
"""
@@ -231,7 +237,7 @@ async def execute_modbus_commands(
e,
)
await asyncio.sleep(RETRY_DELAY)
client._client = None # force reconnect
await client.force_disconnect()
else:
await db.execute(
"""
@@ -290,30 +296,31 @@ async def verify_modbus_commands(
continue
try:
client = await get_modbus_client(
cmd["device_host"], int(cmd["device_port"]), int(cmd["device_unit_id"])
)
actual = await client.read_register(int(cmd["register"]))
unit = int(cmd["device_unit_id"])
client = await get_modbus_client(cmd["device_host"], int(cmd["device_port"]))
actual = await client.read_register(int(cmd["register"]), unit)
actual_i = int(actual)
expected_i = int(cmd["value_to_write"])
await db.execute(
"""
UPDATE ems.modbus_command
SET value_verified=$1, verified_at=now(),
status=CASE WHEN $1=$2 THEN 'verified' ELSE 'mismatch' END
WHERE id=$3
SET value_verified=$1::int, verified_at=now(),
status=CASE WHEN $1::int = $2::int THEN 'verified' ELSE 'mismatch' END
WHERE id=$3::int
""",
actual,
int(cmd["value_to_write"]),
actual_i,
expected_i,
cmd_id,
)
if actual != int(cmd["value_to_write"]):
if actual_i != expected_i:
logger.error(
"[cmd %s] MISMATCH %s 0x%04X: expected=%s actual=%s",
cmd_id,
cmd["asset_code"],
int(cmd["register"]),
cmd["value_to_write"],
actual,
expected_i,
actual_i,
)
row_ac = await db.fetchrow(
"SELECT attempt_count FROM ems.modbus_command WHERE id=$1", cmd_id
@@ -323,8 +330,8 @@ async def verify_modbus_commands(
cmd["asset_code"],
int(cmd["register"]),
cmd["register_name"] or "",
int(cmd["value_to_write"]),
actual,
expected_i,
actual_i,
attempts,
)
@@ -356,8 +363,8 @@ async def verify_modbus_commands(
site["code"],
(
f"Modbus mismatch: {cmd['asset_code']} "
f"0x{cmd['register']:04X} expected={cmd['value_to_write']} "
f"actual={actual}"
f"0x{cmd['register']:04X} expected={expected_i} "
f"actual={actual_i}"
),
)
all_ok = False
@@ -367,7 +374,7 @@ async def verify_modbus_commands(
cmd_id,
cmd["asset_code"],
int(cmd["register"]),
actual,
actual_i,
)
except Exception as e:
logger.error("[cmd %s] verify read failed: %s", cmd_id, e)
@@ -436,6 +443,7 @@ async def _load_inverter_config(
ai.max_battery_charge_w,
ai.max_battery_discharge_w,
ab.reserve_soc_percent,
ab.max_soc_percent,
ab.usable_capacity_wh,
LEAST(
COALESCE(ab.bms_max_charge_w, ai.max_battery_charge_w),
@@ -489,6 +497,9 @@ async def _load_inverter_config(
reserve_soc_percent=int(row["reserve_soc_percent"])
if row["reserve_soc_percent"] is not None
else None,
max_soc_percent=int(row["max_soc_percent"])
if row["max_soc_percent"] is not None
else None,
usable_capacity_wh=int(row["usable_capacity_wh"])
if row["usable_capacity_wh"] is not None
else None,
@@ -729,7 +740,8 @@ def _deye_tou_params(
if deye_mode == "CHARGE":
raw_bat = setpoints.battery_w
battery_w = int(raw_bat) if raw_bat is not None else 0
target_soc = min(95, setpoints.target_soc_pct or 80)
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)
return tp_charge_w, target_soc, True
return tp_discharge_w, reserve_soc, False
@@ -798,7 +810,7 @@ async def write_inverter_setpoints(
for idx in range(2, 6):
registers.extend(
_deye_time_point_rows(
idx, 2359, tp_discharge_w, reserve_soc, False
idx, DEYE_TOU_INACTIVE_HHMM, tp_discharge_w, reserve_soc, False
)
)
@@ -857,21 +869,26 @@ 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, 142, 143, 178, 191 (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.
"""
inv = await _load_inverter_config(site_id, db)
if inv is None:
raise ValueError("no controllable Modbus inverter for site")
client = await get_modbus_client(inv.host, inv.port, inv.unit_id)
uid = int(inv.unit_id)
client = await get_modbus_client(inv.host, inv.port)
read_at = datetime.now(timezone.utc)
try:
r108 = await client.read_register(108)
r109 = await client.read_register(109)
r141 = await client.read_register(141)
r142 = await client.read_register(142)
r143 = await client.read_register(143)
r178 = await client.read_register(178)
r191 = await client.read_register(191)
async with client.batch(uid) as mb:
b108 = await mb.read_holding_registers(108, 2)
b141 = await mb.read_holding_registers(141, 3)
r178 = await mb.read_holding_registers(178, 1)
r191 = await mb.read_holding_registers(191, 1)
r108, r109 = b108[0], b108[1]
r141, r142, r143 = b141[0], b141[1], b141[2]
r178 = r178[0]
r191 = r191[0]
except Exception:
logger.exception("read_deye_registers_live site=%s failed", site_id)
raise