EV telemetrie: skutečné čtení Teltonika TeltoCharge (konec stub-u)
poll_ev_chargers četl placeholder ('available'/0 W) — EV spotřeba se nikdy
neodečítala z bazálu a session detekce nefungovala. Nyní: blok registrů 0-40
jedním FC 3 (oficiální protokol rev 0.5), parse_teltocharge_frame (status z
reg 6 → available/preparing/charging/..., výkon reg 38, energie session reg 39,
proud max L1-L3 reg 3-5). Při selhání čtení se vzorek NEzapisuje (fabrikovaný
available by falešně ukončoval session).
fn_telemetry_ev_charger_sample: + p_current_a (drop staré 7-arg signatury).
6 nových testů parseru; plná sada beze změny. Docs: modbus-registers-teltocharge.md.
Po deployi: home-01 ev-charger-1/2 začnou posílat reálná data; bazál se začne
čistit od EV (EMA 00:30); rebuild stats má smysl až po ~2 týdnech čisté historie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Sběr telemetrie z Modbus (Deye) a placeholder záznamy pro EV / TČ."""
|
||||
"""Sběr telemetrie z Modbus: Deye (střídač), Teltonika TeltoCharge (EV); TČ zatím placeholder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -34,6 +34,49 @@ DEYE_REG_PV2_POWER = 673
|
||||
DEYE_REG_SOLAR_SELL = 145
|
||||
DEYE_REG_CONTROL_BOARD_SPECIAL1 = 178
|
||||
|
||||
# Teltonika TeltoCharge – holding registry (oficiální „Modbus RTU Communication
|
||||
# protocol" rev 0.5, 2024-07-23; přes Waveshare RS485→TCP, FC 3 čtení).
|
||||
# Blok 0–40: 0–2 napětí L1–L3 (V), 3–5 proud L1–L3 (×10 A), 6 EVSE status (DLM),
|
||||
# 27 charge point state, 33 IEC61851, 34/35 warning/error bity,
|
||||
# 38 okamžitý výkon (W), 39 energie session (kWh×100), 40 trvání session (s).
|
||||
# Zápisové (řízení, zatím nepoužité): 15 Amps to use (0=stop, 6–32), 16 start/stop.
|
||||
TELTO_REG_BLOCK_START = 0
|
||||
TELTO_REG_BLOCK_COUNT = 41
|
||||
|
||||
#: EVSE status (reg 6) → interní stav; session detekce stojí na 'available' vs ≠'available'
|
||||
#: (fn_ev_session_transition), proto každý stav s připojeným EV musí být ≠ 'available'.
|
||||
TELTO_STATUS_MAP = {
|
||||
0: "charging", # C – nabíjí
|
||||
1: "preparing", # B1 – EV připojeno, čeká na EV
|
||||
2: "preparing", # B2 – dříve nabíjelo, nedostatek výkonu
|
||||
3: "preparing", # B3 – nenabíjelo, nedostatek výkonu
|
||||
4: "suspended_ev", # D1 – zastaveno vozidlem
|
||||
5: "suspended_evse", # D2 – bez autorizace
|
||||
6: "suspended_evse", # D3 – nabíjení nepovoleno
|
||||
7: "available", # A – bez EV
|
||||
8: "faulted", # F – chyba
|
||||
9: "unknown", # E
|
||||
}
|
||||
|
||||
|
||||
def parse_teltocharge_frame(regs: list[int]) -> dict[str, object]:
|
||||
"""Čistý parser bloku registrů 0–40 TeltoCharge (testovatelné bez Modbus)."""
|
||||
if len(regs) < TELTO_REG_BLOCK_COUNT:
|
||||
raise ValueError(f"TeltoCharge frame too short: {len(regs)}")
|
||||
status = TELTO_STATUS_MAP.get(int(regs[6]), "unknown")
|
||||
current_a = max(int(regs[3]), int(regs[4]), int(regs[5])) / 10.0
|
||||
return {
|
||||
"status": status,
|
||||
"power_w": int(regs[38]),
|
||||
"session_energy_kwh": int(regs[39]) / 100.0,
|
||||
"current_a": current_a,
|
||||
"voltage_v": int(regs[0]),
|
||||
"warning_bits": int(regs[34]),
|
||||
"error_bits": int(regs[35]),
|
||||
"evse_status_raw": int(regs[6]),
|
||||
"charge_point_state_raw": int(regs[27]),
|
||||
}
|
||||
|
||||
|
||||
def aggregate_pv_production_w(pv1_w: int, pv2_w: int, gen_port_w: int) -> int:
|
||||
"""
|
||||
@@ -152,8 +195,29 @@ async def poll_ev_chargers(site_id: int, db: asyncpg.Connection) -> None:
|
||||
for row in rows:
|
||||
code = row["code"]
|
||||
charger_id = row["id"]
|
||||
logger.info("TODO: EV charger Modbus registry pending | %s", code)
|
||||
current_status = "available"
|
||||
host = row["host"]
|
||||
port = int(row["port"] or 502)
|
||||
unit_id = int(row["unit_id"] if row["unit_id"] is not None else 1)
|
||||
|
||||
try:
|
||||
client = await get_modbus_client(host, port)
|
||||
async with client.batch(unit_id) as mb:
|
||||
regs = await mb.read_holding_registers(
|
||||
TELTO_REG_BLOCK_START, TELTO_REG_BLOCK_COUNT
|
||||
)
|
||||
frame = parse_teltocharge_frame(regs)
|
||||
except Exception as e:
|
||||
# Při výpadku čtení NIC nezapisovat — fabrikovaný 'available' by
|
||||
# falešně ukončoval EV session a špinil bazál (power 0).
|
||||
logger.warning("EV charger %s (%s:%s) read failed: %s", code, host, port, e)
|
||||
continue
|
||||
|
||||
current_status = str(frame["status"])
|
||||
if frame["error_bits"]:
|
||||
logger.warning(
|
||||
"EV charger %s error bits=0x%04x warning=0x%04x",
|
||||
code, frame["error_bits"], frame["warning_bits"],
|
||||
)
|
||||
|
||||
previous_status = await db.fetchval(
|
||||
"""
|
||||
@@ -168,14 +232,15 @@ async def poll_ev_chargers(site_id: int, db: asyncpg.Connection) -> None:
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"select ems.fn_telemetry_ev_charger_sample($1::int, $2::int, $3::timestamptz, $4::int, $5::text, $6::int, $7::float8)",
|
||||
"select ems.fn_telemetry_ev_charger_sample($1::int, $2::int, $3::timestamptz, $4::int, $5::text, $6::int, $7::float8, $8::float8)",
|
||||
site_id,
|
||||
charger_id,
|
||||
measured_at,
|
||||
connector_id,
|
||||
current_status,
|
||||
0,
|
||||
0.0,
|
||||
int(frame["power_w"]),
|
||||
float(frame["session_energy_kwh"]),
|
||||
float(frame["current_a"]),
|
||||
)
|
||||
|
||||
if previous_status is not None:
|
||||
|
||||
Reference in New Issue
Block a user