diff --git a/backend/services/telemetry_collector.py b/backend/services/telemetry_collector.py index 10e6a97..db476b5 100644 --- a/backend/services/telemetry_collector.py +++ b/backend/services/telemetry_collector.py @@ -418,6 +418,43 @@ async def poll_ev_chargers(site_id: int, db: asyncpg.Connection) -> None: asyncio.create_task(_on_ev_departure(site_id, str(code))) +# Samsung MIM-B19N(T) — Modbus RTU slave za RS485→TCP (9600 8E1!). +# Adresace: vnitřní jednotka IU má blok base = 50 + IU*50; zde IU 0 → 50..99. +# Plný popis: docs/04-modules/modbus-registers-mim-b19n.md +MIM_IU_BASE = 50 # blok vnitřní jednotky 0 +MIM_OFF_COMM_STATUS = 0 # b0 exist, b1 type OK, b2 ready, b3 comm error +MIM_OFF_UNIT_TYPE = 1 # lower byte: 110=HE, 115-117=EHS, 120=HT +MIM_OFF_ONOFF = 2 +MIM_OFF_MODE = 3 # 0 auto, 1 cool, 4 heat +MIM_OFF_ROOM_TEMP = 9 # °C×10 signed +MIM_OFF_ERROR_CODE = 13 # 0 = OK, 100-999 kód +MIM_OFF_WATER_IN = 15 # °C×10 signed +MIM_OFF_WATER_OUT = 16 # °C×10 signed +MIM_OFF_DHW_ONOFF = 22 +MIM_OFF_DHW_TEMP = 25 # °C×10 (zásobník TUV) +MIM_REG_DEFROST = 2 # modulový registr: 0=off, jinak defrost +MIM_MODE_NAMES = {0: "auto", 1: "cool", 2: "dry", 3: "fan", 4: "heat"} + + +def _mim_temp_c(raw: int) -> float | None: + """°C×10, signed 16bit; MIM drží 0 dokud jednotka hodnotu nedodá.""" + v = raw - 65536 if raw > 32767 else raw + return round(v / 10.0, 1) + + +def mim_operating_mode(on: int, mode: int, dhw_on: int, comm_ready: bool, error: int) -> str: + if not comm_ready: + return "offline" + if error: + return "error" + parts = [] + if int(on) == 1: + parts.append(MIM_MODE_NAMES.get(int(mode), f"mode{mode}")) + if int(dhw_on) == 1: + parts.append("dhw") + return "+".join(parts) if parts else "off" + + async def poll_heat_pump(site_id: int, db: asyncpg.Connection) -> None: rows = await db.fetch( """ @@ -430,18 +467,54 @@ async def poll_heat_pump(site_id: int, db: asyncpg.Connection) -> None: measured_at = datetime.now(timezone.utc) for row in rows: code = row["code"] - logger.info("TODO: heat pump Modbus registry pending (heat_pump=%s)", code) + 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: + iu = await mb.read_holding_registers(MIM_IU_BASE, 26) + defrost_raw = await mb.read_register(MIM_REG_DEFROST) + except Exception as e: + logger.warning("heat_pump %s: Modbus poll failed (%s)", code, e) + continue + + comm = int(iu[MIM_OFF_COMM_STATUS]) + comm_ready = (comm & 0b111) == 0b111 + error_code = int(iu[MIM_OFF_ERROR_CODE]) + mode_txt = mim_operating_mode( + iu[MIM_OFF_ONOFF], iu[MIM_OFF_MODE], iu[MIM_OFF_DHW_ONOFF], + comm_ready, error_code, + ) + if not comm_ready: + # MIM odpovídá, ale jednotka není ztrackovaná (b0-b2) — telemetrii + # nezapisovat (samé nuly), jen log; trvalý stav = špatná adresa IU + # nebo SEG5 "Use of central control" vypnuté. + logger.warning( + "heat_pump %s: jednotka není ready (comm_status=%s) — vzorek přeskočen", + code, bin(comm), + ) + continue + await db.execute( - "select ems.fn_telemetry_heat_pump_sample($1::int, $2::int, $3::timestamptz, $4::int, $5::float8, $6::float8, $7::float8, $8::text)", + "select ems.fn_telemetry_heat_pump_sample(" + "$1::int, $2::int, $3::timestamptz, $4::int, $5::float8, $6::float8," + " $7::float8, $8::text, $9::float8, $10::float8, $11::boolean, $12::int)", site_id, row["id"], measured_at, - 0, - 10.0, - 45.0, - 55.0, - "standby", + None, # příkon: MIM neměří — doplní elektroměr (Shelly/Chint) + None, # venkovní teplota: v MIM mapě není + _mim_temp_c(iu[MIM_OFF_WATER_OUT]), + _mim_temp_c(iu[MIM_OFF_DHW_TEMP]), + mode_txt, + _mim_temp_c(iu[MIM_OFF_WATER_IN]), + _mim_temp_c(iu[MIM_OFF_ROOM_TEMP]), + bool(defrost_raw), + error_code, ) + if error_code: + logger.warning("heat_pump %s: error code %s", code, error_code) async def poll_loxone_sensors(site_id: int, db: asyncpg.Connection) -> None: diff --git a/db/migration/V096__heat_pump_mim_b19n.sql b/db/migration/V096__heat_pump_mim_b19n.sql new file mode 100644 index 0000000..1908bc8 --- /dev/null +++ b/db/migration/V096__heat_pump_mim_b19n.sql @@ -0,0 +1,21 @@ +-- Samsung TČ (EHS) přes Modbus interface MIM-B19N(T): skutečný RS485→TCP +-- převodník (Waveshare RS485 TO POE ETH (B)) na 172.16.1.17 nahrazuje +-- placeholder 192.168.1.103 ze seedu. MIM = Modbus RTU slave, 9600 8E1, +-- adresa dle DIP/rotary (zde 1). Registry: docs/04-modules/modbus-registers-mim-b19n.md. + +update ems.site_endpoint e + set host = '172.16.1.17', + port = 502, + notes = 'Waveshare RS485 TO POE ETH (B) pro Samsung EHS přes MIM-B19N(T). Sériová linka 9600 8E1 (parita EVEN!), Modbus TCP server :502, unit_id = adresa MIM dle DIP (1).' + where e.id = ( + select hp.endpoint_id + from ems.asset_heat_pump hp + join ems.site s on s.id = hp.site_id + where s.code = 'home-01' + ); + +alter table ems.telemetry_heat_pump + add column if not exists room_temp_c numeric(5,2); + +comment on column ems.telemetry_heat_pump.room_temp_c is + 'Prostorová teplota hlášená vnitřní jednotkou (MIM reg base+9, °C×10). Vstup budoucího termálního modelu domu.'; diff --git a/db/routines/R__048_fn_telemetry_heat_pump_sample.sql b/db/routines/R__048_fn_telemetry_heat_pump_sample.sql index e5035be..de148a2 100644 --- a/db/routines/R__048_fn_telemetry_heat_pump_sample.sql +++ b/db/routines/R__048_fn_telemetry_heat_pump_sample.sql @@ -1,3 +1,8 @@ +-- Insert 1min vzorku telemetrie TČ (MIM-B19N). Bez overloadů — při změně +-- signatury drop bez parametrů (konvence CLAUDE.md). + +drop function if exists ems.fn_telemetry_heat_pump_sample; + create or replace function ems.fn_telemetry_heat_pump_sample( p_site_id int, p_heat_pump_id int, @@ -6,7 +11,11 @@ create or replace function ems.fn_telemetry_heat_pump_sample( p_outdoor_temp_c double precision, p_water_outlet_temp_c double precision, p_tuv_tank_temp_c double precision, - p_operating_mode text + p_operating_mode text, + p_water_inlet_temp_c double precision default null, + p_room_temp_c double precision default null, + p_defrost_active boolean default null, + p_alarm_code int default null ) returns void language sql @@ -19,7 +28,11 @@ as $fn$ outdoor_temp_c, water_outlet_temp_c, tuv_tank_temp_c, - operating_mode + operating_mode, + water_inlet_temp_c, + room_temp_c, + defrost_active, + alarm_code ) values ( p_site_id, @@ -29,10 +42,14 @@ as $fn$ p_outdoor_temp_c, p_water_outlet_temp_c, p_tuv_tank_temp_c, - p_operating_mode + p_operating_mode, + p_water_inlet_temp_c, + p_room_temp_c, + p_defrost_active, + p_alarm_code ) on conflict (heat_pump_id, measured_at) do nothing; $fn$; comment on function ems.fn_telemetry_heat_pump_sample is - 'Insert telemetrie TČ (placeholder Modbus).'; + 'Insert telemetrie TČ z MIM-B19N pollu (voda in/out, TUV, prostorová teplota, defrost, alarm). power_w je NULL — MIM příkon neměří (nutný elektroměr).'; diff --git a/docs/04-modules/modbus-registers-mim-b19n.md b/docs/04-modules/modbus-registers-mim-b19n.md new file mode 100644 index 0000000..98b0350 --- /dev/null +++ b/docs/04-modules/modbus-registers-mim-b19n.md @@ -0,0 +1,84 @@ +# Samsung MIM-B19N(T) — Modbus registry (TČ EHS, home-01) + +Modbus interface modul Samsungu (DVM/EHS). EMS k němu mluví přes Waveshare +**RS485 TO POE ETH (B)** na **172.16.1.17:502** (V096; dříve placeholder +192.168.1.103). Zdroj: instalační manuál MIM-B19N(T) (DB68-07538A-03). + +## Sériová linka a převodník — POVINNÉ nastavení + +| Parametr | Hodnota | +|---|---| +| Baud rate | **9600** | +| Data bits | 8 | +| Parita | **EVEN** (nejčastější chyba — Waveshare default je None!) | +| Stop bit | 1 | +| Protokol | Modbus RTU, MIM = **slave** | +| Adresa MIM | DIP SW4/SW5 + rotary SW1, rozsah 1–247 (čte se jen při zapnutí!); EMS `unit_id` = tato adresa (home-01: 1) | +| Waveshare work mode | TCP Server, local port **502**, protokol **Modbus TCP ↔ RTU** (jinak EMS nespojí — port 502 zavřený) | +| FC podporované | 0x03, 0x04 čtení; 0x06, 0x10 zápis | +| Mezera mezi dotazy | ≥ 10 ms po poslední odpovědi | + +Polarita A/B: při prohození MIM neodpovídá vůbec (timeout na FC3), Y-GRN LED +na MIM nebliká k BMS. 7segment na MIM: `E6`+`16` střídavě = ztracená +komunikace, `E6`+`04` = tracking nedoběhl, `E6`+`34` = chybná adresa. + +## Adresace registrů + +Vnitřní jednotka IU (adresa 0–47, nastavená na jednotce) má blok +**base = 50 + IU×50**. home-01 EHS = IU 0 → blok 50–99. Hodnoty teplot +**°C×10, signed**, big endian. Po startu MIM jsou všechny registry 0, dokud +nedoběhne tracking (~minuty). + +### Modulové registry (PDU 0–3) + +| Reg | Význam | R/W | +|---|---|---| +| 0 | Stav modulu: b0 address error, b1 comm error R1/R2, b2 tracking error | R | +| 1 | Chybový kód venkovní jednotky (0 = OK, 100–999) | R | +| 2 | Defrost (0/0xFF off, jinak on) | R | +| 3 | Bzučák (0 on / 1 off) | W | + +### Blok vnitřní jednotky (base+offset; EHS sloupec) + +| Off | Význam | R/W | Poznámka | +|---|---|---|---| +| +0 | Comm status: b0 exist, b1 type OK, b2 ready, b3 comm error | R | **gate pollu: (v&7)==7** | +| +1 | Typ jednotky (lower byte): 110 HE, 115–117 EHS, 120 HT | R | | +| +2 | Zapnuto/vypnuto (0/1) | R/W | | +| +3 | Režim: 0 auto, 1 cool, 4 heat | R/W | | +| +8 | Set teplota ×10 (cool 18–30, heat 16–30) | R/W | | +| +9 | Prostorová teplota ×10 | R | → `room_temp_c` | +| +13 | Chybový kód jednotky (0 OK, 100–999) | R | → `alarm_code` | +| +14 | Blokace dálkového ovládání (0x0000 / 0x6363) | R/W | | +| +15 | Teplota vody vstup ×10 | R | → `water_inlet_temp_c` | +| +16 | Teplota vody výstup ×10 | R | → `water_outlet_temp_c` | +| +18 | Set teplota výstupní vody ×10 (EHS heat 15–65 °C) | R/W | budoucí řízení | +| +22 | TUV zapnuto/vypnuto | R/W | | +| +23 | TUV režim: 0 Eco, 1 Standard, 2 Power, 3 Force (jen EHS) | R/W | | +| +24 | TUV set teplota ×10 (EHS 30–70 °C) | R/W | budoucí řízení | +| +25 | TUV teplota zásobníku ×10 | R | → `tuv_tank_temp_c` | +| +28 | Tichý režim (0/1) | R/W | | +| +29 | Away (0/1) | R/W | | + +## Telemetrie EMS (poll 60 s, `poll_heat_pump`) + +Jeden FC3 blok base+0..+25 (26 registrů) + modulový reg 2 (defrost) → +`ems.fn_telemetry_heat_pump_sample`. `operating_mode`: `off` / `heat` / +`cool` / `auto` / `dhw` / `heat+dhw` / `error` / (`offline` se nezapisuje — +vzorek se přeskočí, jednotka bez trackingu hlásí samé nuly). + +**Příkon (`power_w`) MIM neposkytuje** — zůstává NULL, dokud nebude +elektroměr (Shelly EM / Chint na RS485). Bazální spotřeba (CLAUDE.md §15) +do té doby TČ neodečítá. + +**Zápisy (on/off, set teploty, TUV)**: zatím neimplementováno; půjdou přes +control exporter + `modbus_command` journal jako u Deye (FC 0x06/0x10). +Pozn. manuálu: každý write MIM přepošle jednotce, i když hodnota nemění — +zapisovat jen při skutečné změně. + +## Stav zapojení (2026-06-12) + +Převodník na 172.16.1.17 odpovídá (ping, web UI :80 „RS485 TO POE ETH (B)"), +**port 502 zatím zavřený** → ve web UI nastavit TCP Server :502 + Modbus +TCP↔RTU převod a sériovku 9600 8E1. Pak ověřit FC3 čtení bloků 0–2 a 50–75 +(`/tmp/probe_mim.py` vzor).