Merge branch 'fix/ev-teltocharge-reg15-and-session-visibility' into dev
Some checks failed
CI and deploy / migration-check (push) Failing after 7m26s
CI and deploy / deploy (push) Has been skipped

# Conflicts:
#	docs/planning-changelog.md
This commit is contained in:
Dusan Vojacek
2026-06-13 22:41:14 +02:00
14 changed files with 453 additions and 211 deletions

View File

@@ -18,33 +18,52 @@ logger = logging.getLogger(__name__)
TELTO_REG_AMPS_TO_USE = 15 # 0 = stop, 632 A
TELTO_REG_COMM_TIMEOUT_S = 19 # watchdog: bez komunikace → failsafe
TELTO_REG_FAILSAFE_CURRENT_A = 20
#: Výpadek EMS: po 5 min bez zápisu wallbox přejde na failsafe proud —
#: auto se přes noc nabije i bez EMS (pomalu), místo aby stálo na 0 A.
#: Výpadek EMS: po watchdog_comm_timeout_s bez komunikace wallbox přejde na
#: failsafe proud — auto se přes noc nabije i bez EMS (pomalu), místo aby
#: stálo na 0 A. Defaulty (fallback, když řádek chargeru nemá vlastní hodnoty).
TELTO_WATCHDOG_TIMEOUT_S = 300
TELTO_WATCHDOG_FAILSAFE_A = 8
def _telto_setpoint_registers(current_a: int) -> list[tuple[int, str, int]]:
def _telto_setpoint_registers(
current_a: int,
*,
comm_timeout_s: int = TELTO_WATCHDOG_TIMEOUT_S,
failsafe_a: int = TELTO_WATCHDOG_FAILSAFE_A,
) -> list[tuple[int, str, int]]:
"""Registry pro jeden export tick: limit proudu + watchdog konfigurace.
Write-on-change: volající VŽDY filtruje přes drop-unchanged proti
fn_modbus_device_state_map (poslední written/verified per registr) —
watchdog 19/20 se reálně zapíše jen po startu / po výpadku zařízení,
amps (15) jen při změně plánu. Watchdog timer TeltoCharge sytí jakákoli
validní Modbus komunikace (i FC3 čtení telemetrie každých 60 s), takže
periodické zápisy k udržení spojení NEJSOU potřeba (oficiální protokol,
docs/04-modules/modbus-registers-teltocharge.md).
**Reg 15 (amps to use) NENÍ write-on-change** — viz `_assert_amps_register`.
Je to volatilní řídicí registr: TeltoCharge ho po výpadku komunikace sám
přepíše na failsafe (reg 20), aniž by o tom vznikl journal řádek. Kdyby se
reg 15 dropoval proti journalu (poslední „0 verified"), EMS by tichý drift
0 → 8 A NIKDY nezahlédlo (verify čte zpět jen `written` řádky) a nikdy ho
neopravilo. Proto se reg 15 re-asertuje KAŽDÝ export tick (≤ 8×/hod) —
EEPROM wear se týká jen konfiguračních 19/20, které write-on-change zůstávají.
Watchdog timer TeltoCharge sytí jakákoli validní Modbus komunikace (i FC3
čtení telemetrie každých 60 s), takže periodické zápisy k udržení spojení
NEJSOU potřeba; failsafe/timeout (19/20) per charger z DB.
"""
a = int(current_a)
if a < 6:
a = 0
return [
(TELTO_REG_AMPS_TO_USE, "telto_amps_to_use", min(a, 32)),
(TELTO_REG_COMM_TIMEOUT_S, "telto_comm_timeout_s", TELTO_WATCHDOG_TIMEOUT_S),
(TELTO_REG_FAILSAFE_CURRENT_A, "telto_failsafe_a", TELTO_WATCHDOG_FAILSAFE_A),
(TELTO_REG_COMM_TIMEOUT_S, "telto_comm_timeout_s", int(comm_timeout_s)),
(TELTO_REG_FAILSAFE_CURRENT_A, "telto_failsafe_a", max(0, min(int(failsafe_a), 32))),
]
def _split_amps_and_watchdog(
registers: list[tuple[int, str, int]],
) -> tuple[list[tuple[int, str, int]], list[tuple[int, str, int]]]:
"""Rozdělí registry na (reg 15 = vždy zapsat) a (19/20 = write-on-change)."""
amps = [r for r in registers if r[0] == TELTO_REG_AMPS_TO_USE]
watchdog = [r for r in registers if r[0] != TELTO_REG_AMPS_TO_USE]
return amps, watchdog
def _current_limit_for_charger(charger_code: str, sp: ControlSetpoints) -> int:
c = (charger_code or "").strip().lower()
if c == "ev-charger-1":
@@ -74,7 +93,8 @@ async def write_ev_setpoints(
rows = await db.fetch(
"""
SELECT ec.id AS asset_id, ec.code, se.host, se.port, se.unit_id
SELECT ec.id AS asset_id, ec.code, se.host, se.port, se.unit_id,
ec.watchdog_failsafe_a, ec.watchdog_comm_timeout_s
FROM ems.asset_ev_charger ec
JOIN ems.site_endpoint se ON se.id = ec.endpoint_id
WHERE ec.site_id = $1
@@ -97,16 +117,31 @@ async def write_ev_setpoints(
unit_id = int(row["unit_id"] if row["unit_id"] is not None else 1)
current_a = _current_limit_for_charger(code, setpoints)
registers = _telto_setpoint_registers(current_a)
# Write-on-change: poslední written/verified stav (ne jen verified) —
# zápis se nesmí opakovat každý tick, když verify čtení zaostává.
registers = _telto_setpoint_registers(
current_a,
comm_timeout_s=int(
row["watchdog_comm_timeout_s"]
if row["watchdog_comm_timeout_s"] is not None
else TELTO_WATCHDOG_TIMEOUT_S
),
failsafe_a=int(
row["watchdog_failsafe_a"]
if row["watchdog_failsafe_a"] is not None
else TELTO_WATCHDOG_FAILSAFE_A
),
)
amps_regs, watchdog_regs = _split_amps_and_watchdog(registers)
# Reg 15 = vždy (re-asert proti tichému watchdog failsafe driftu na
# zařízení, který nemá journal řádek). Reg 19/20 = write-on-change
# proti fn_modbus_device_state_map (poslední written/verified stav).
device_state = await _fetch_device_state_registers(
site_id, asset_id, db, asset_type="ev_charger"
)
registers, skipped = _drop_registers_matching_last_verified(
registers, device_state
watchdog_regs, skipped = _drop_registers_matching_last_verified(
watchdog_regs, device_state
)
if not registers:
to_write = amps_regs + watchdog_regs
if not to_write:
logger.debug("EV setpoint [%s]: beze změny (%s A)", code, current_a)
continue
@@ -119,7 +154,7 @@ async def write_ev_setpoints(
host,
port,
unit_id,
registers,
to_write,
db,
)
ok = await execute_modbus_commands(cmd_ids, db)
@@ -128,7 +163,7 @@ async def write_ev_setpoints(
"EV setpoint [%s]: %s A (regs %s%s) -> %s",
code,
current_a,
[r for r, _, _ in registers],
[r for r, _, _ in to_write],
f", skip {skipped}" if skipped else "",
"written" if ok else "FAILED",
)
@@ -155,7 +190,8 @@ async def write_ev_arrival_hold(
row = await db.fetchrow(
"""
SELECT ec.id AS asset_id, ec.code, se.host, se.port, se.unit_id
SELECT ec.id AS asset_id, ec.code, se.host, se.port, se.unit_id,
ec.watchdog_failsafe_a, ec.watchdog_comm_timeout_s
FROM ems.asset_ev_charger ec
JOIN ems.site_endpoint se ON se.id = ec.endpoint_id
WHERE ec.site_id = $1
@@ -170,20 +206,29 @@ async def write_ev_arrival_hold(
if row is None:
return False
asset_id = int(row["asset_id"])
registers = _telto_setpoint_registers(0)
registers = _telto_setpoint_registers(
0,
comm_timeout_s=int(
row["watchdog_comm_timeout_s"]
if row["watchdog_comm_timeout_s"] is not None
else TELTO_WATCHDOG_TIMEOUT_S
),
failsafe_a=int(
row["watchdog_failsafe_a"]
if row["watchdog_failsafe_a"] is not None
else TELTO_WATCHDOG_FAILSAFE_A
),
)
amps_regs, watchdog_regs = _split_amps_and_watchdog(registers)
# Reg 15 = 0 A se zapíše VŽDY (tvrdé zastavení po píchnutí kabelu; wallbox
# po připojení sám rozjíždí nabíjení defaultem). Reg 19/20 write-on-change.
device_state = await _fetch_device_state_registers(
site_id, asset_id, db, asset_type="ev_charger"
)
registers, skipped = _drop_registers_matching_last_verified(
registers, device_state
watchdog_regs, skipped = _drop_registers_matching_last_verified(
watchdog_regs, device_state
)
if not registers:
logger.info(
"EV arrival hold [%s]: 0 A už na zařízení (skip %s)",
charger_code,
skipped,
)
return True
to_write = amps_regs + watchdog_regs
cmd_ids = await create_modbus_commands(
site_id,
None,
@@ -193,14 +238,14 @@ async def write_ev_arrival_hold(
str(row["host"]),
int(row["port"] or 502),
int(row["unit_id"] if row["unit_id"] is not None else 1),
registers,
to_write,
db,
)
ok = await execute_modbus_commands(cmd_ids, db)
logger.info(
"EV arrival hold [%s]: 0 A (regs %s%s) %s",
charger_code,
[r for r, _, _ in registers],
[r for r, _, _ in to_write],
f", skip {skipped}" if skipped else "",
"written" if ok else "FAILED",
)