TeltoCharge write-on-change: zápis jen při změně hodnoty (EEPROM wear)
Wallbox dostával zápisy 15/19/20 každý export tick (~8x/hod: control_export :14,:29,:44,:59 + rolling replan */15 s exportem), protože drop-unchanged stál na fn_modbus_last_verified_map — dokud verify čtení nedoběhlo/selhalo, mapa byla prázdná a celá trojice se psala pořád dokola. write_ev_arrival_hold navíc psal trojici nepodmíněně při každém píchnutí kabelu (docstring lhal). - nová ems.fn_modbus_device_state_map (R__100): nejnovější řádek journalu per registr, hodnota jen pro written/verified; failed/mismatch => registr chybí => po výpadku se konfigurace obnoví jedním zápisem - write_ev_setpoints + write_ev_arrival_hold filtrují přes tuto mapu: reg 15 jen při změně plánu, watchdog 19/20 jednou po startu/po výpadku - verify job EV chargery ověřuje už dnes (fn_modbus_written_command_ids bez filtru asset_type); registry 15/19/20 jsou dle oficiálního protokolu R/W - watchdog Telto sytí jakákoli validní komunikace vč. FC3 čtení telemetrie (60 s << 300 s) — periodické zápisy k udržení spojení nejsou potřeba, failsafe 8 A nastane jen při skutečném výpadku EMS - testy: tests/test_ev_write_on_change.py (drop, setpoints, arrival hold) - docs: modbus-registers-teltocharge.md (sekce Zápis už není "NEimplementováno", R/W tabulka, watchdog sémantika), modbus-command-journal.md (sekce EV wallbox), CLAUDE.md (fn_modbus_device_state_map) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,35 @@ async def _fetch_last_verified_registers(
|
||||
return {int(k): int(v) for k, v in data.items()}
|
||||
|
||||
|
||||
async def _fetch_device_state_registers(
|
||||
site_id: int,
|
||||
asset_id: int,
|
||||
db: asyncpg.Connection,
|
||||
*,
|
||||
asset_type: str,
|
||||
) -> dict[int, int]:
|
||||
"""
|
||||
Poslední známá hodnota na zařízení podle journalu — NEJNOVĚJŠÍ řádek per
|
||||
registr, hodnota jen pro status 'verified' nebo 'written' (zápis prošel,
|
||||
verify ještě nemusel doběhnout). Novější failed/mismatch => registr chybí
|
||||
=> volající zapíše znovu (obnova konfigurace po výpadku zařízení).
|
||||
|
||||
Pro write-on-change u EV wallboxů (EEPROM wear): na rozdíl od
|
||||
_fetch_last_verified_registers nevyžaduje úspěšný verify, takže se zápis
|
||||
neopakuje každý export tick, když verify čtení zaostává nebo selhává.
|
||||
"""
|
||||
raw = await db.fetchval(
|
||||
"""
|
||||
select ems.fn_modbus_device_state_map($1::int, $2::int, $3::text)
|
||||
""",
|
||||
site_id,
|
||||
asset_id,
|
||||
asset_type,
|
||||
)
|
||||
data = raw if isinstance(raw, dict) else json.loads(raw)
|
||||
return {int(k): int(v) for k, v in data.items()}
|
||||
|
||||
|
||||
async def _fetch_last_verified_inverter_registers(
|
||||
site_id: int, inverter_asset_id: int, db: asyncpg.Connection
|
||||
) -> dict[int, int]:
|
||||
|
||||
@@ -27,8 +27,13 @@ TELTO_WATCHDOG_FAILSAFE_A = 8
|
||||
def _telto_setpoint_registers(current_a: int) -> list[tuple[int, str, int]]:
|
||||
"""Registry pro jeden export tick: limit proudu + watchdog konfigurace.
|
||||
|
||||
Watchdog (19/20) se posílá s každým tickem, ale journal drop-unchanged ho
|
||||
po prvním verified zápisu přeskakuje — reálně se zapíše jednou.
|
||||
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).
|
||||
"""
|
||||
a = int(current_a)
|
||||
if a < 6:
|
||||
@@ -62,7 +67,7 @@ async def write_ev_setpoints(
|
||||
) -> str:
|
||||
from services.control.modbus_journal import (
|
||||
_drop_registers_matching_last_verified,
|
||||
_fetch_last_verified_registers,
|
||||
_fetch_device_state_registers,
|
||||
create_modbus_commands,
|
||||
execute_modbus_commands,
|
||||
)
|
||||
@@ -93,11 +98,13 @@ async def write_ev_setpoints(
|
||||
current_a = _current_limit_for_charger(code, setpoints)
|
||||
|
||||
registers = _telto_setpoint_registers(current_a)
|
||||
last_verified = await _fetch_last_verified_registers(
|
||||
# Write-on-change: poslední written/verified stav (ne jen verified) —
|
||||
# zápis se nesmí opakovat každý tick, když verify čtení zaostává.
|
||||
device_state = await _fetch_device_state_registers(
|
||||
site_id, asset_id, db, asset_type="ev_charger"
|
||||
)
|
||||
registers, skipped = _drop_registers_matching_last_verified(
|
||||
registers, last_verified
|
||||
registers, device_state
|
||||
)
|
||||
if not registers:
|
||||
logger.debug("EV setpoint [%s]: beze změny (%s A)", code, current_a)
|
||||
@@ -135,10 +142,13 @@ async def write_ev_arrival_hold(
|
||||
|
||||
TeltoCharge po připojení kabelu sám rozjede nabíjení svým defaultem —
|
||||
nabíjet smí až PLÁN (replan + export běží hned poté v _on_ev_arrival,
|
||||
takže držení trvá sekundy až ~1 min). Watchdog registry se zapíší spolu
|
||||
s 0 A (drop-unchanged je po prvním verified stejně přeskočí).
|
||||
takže držení trvá sekundy až ~1 min). Write-on-change: registry shodné
|
||||
s posledním written/verified stavem (typicky watchdog 19/20, často
|
||||
i 15=0) se přeskočí — žádný zbytečný zápis při každém píchnutí kabelu.
|
||||
"""
|
||||
from services.control.modbus_journal import (
|
||||
_drop_registers_matching_last_verified,
|
||||
_fetch_device_state_registers,
|
||||
create_modbus_commands,
|
||||
execute_modbus_commands,
|
||||
)
|
||||
@@ -159,21 +169,40 @@ async def write_ev_arrival_hold(
|
||||
)
|
||||
if row is None:
|
||||
return False
|
||||
asset_id = int(row["asset_id"])
|
||||
registers = _telto_setpoint_registers(0)
|
||||
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
|
||||
)
|
||||
if not registers:
|
||||
logger.info(
|
||||
"EV arrival hold [%s]: 0 A už na zařízení (skip %s)",
|
||||
charger_code,
|
||||
skipped,
|
||||
)
|
||||
return True
|
||||
cmd_ids = await create_modbus_commands(
|
||||
site_id,
|
||||
None,
|
||||
"ev_charger",
|
||||
int(row["asset_id"]),
|
||||
asset_id,
|
||||
str(row["code"]),
|
||||
str(row["host"]),
|
||||
int(row["port"] or 502),
|
||||
int(row["unit_id"] if row["unit_id"] is not None else 1),
|
||||
_telto_setpoint_registers(0),
|
||||
registers,
|
||||
db,
|
||||
)
|
||||
ok = await execute_modbus_commands(cmd_ids, db)
|
||||
logger.info(
|
||||
"EV arrival hold [%s]: 0 A %s", charger_code, "written" if ok else "FAILED"
|
||||
"EV arrival hold [%s]: 0 A (regs %s%s) %s",
|
||||
charger_code,
|
||||
[r for r, _, _ in registers],
|
||||
f", skip {skipped}" if skipped else "",
|
||||
"written" if ok else "FAILED",
|
||||
)
|
||||
return bool(ok)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user