Metadata-Version: 2.5
Name: proxon-modbus
Version: 0.1.2
Summary: Gerätebibliothek für Proxon FWT 2.0 und T300 über Modbus RTU
License: MIT
Requires-Python: >=3.12
Requires-Dist: modbus-connection>=4.10.0
Provides-Extra: pymodbus
Requires-Dist: modbus-connection[pymodbus]>=4.10.0; extra == 'pymodbus'
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Provides-Extra: tmodbus
Requires-Dist: modbus-connection[tmodbus]>=4.10.0; extra == 'tmodbus'
Description-Content-Type: text/markdown

# proxon-modbus

Gerätebibliothek für **Proxon FWT 2.0** (Lüftungs-Wärmepumpe) und **T300**
(Warmwasser-Wärmepumpe) über Modbus RTU.

Die Bibliothek besitzt den Transport **nicht**. Anwendungen reichen eine
`modbus_connection.ModbusUnit` herein — Backend `tmodbus` oder `pymodbus` —
und bekommen dafür das Registerwissen der Anlage. Damit lässt sich sie ohne
Hardware testen und teilt sich eine Verbindung mit anderen Verbrauchern
desselben RS485-Strangs.

```python
from modbus_connection import ModbusTcpParams
from modbus_connection.tmodbus import ModbusConnection
from proxon_modbus import ProxonDevice

conn = ModbusConnection(
    ModbusTcpParams(host="192.0.2.10", port=502, framer="rtu"),
    message_spacing=0.15,   # Pause zwischen Blöcken
    connect_delay=0.30,     # Wartezeit nach Verbindungsaufbau
)
device = ProxonDevice(conn.for_unit(41))
await device.async_update()

print(device.t300_input.t21_behaelter_mitte)      # 57.5
print(device.fwt_holding.bypass_min_frischluft)   # 8.0
print([d.temperatur for d in device.nbe_temperatures.devices])
```

## Warum eigene Feldtypen

Zwei Eigenheiten der Anlage kann `modbus_connection` nicht von sich aus:

**Skalierung und Offset sind anders gemeint.** Proxon rechnet
`(roh + offset) / scale`, die Bibliothek `roh * scale + offset`; Proxons
`scale` ist ein Divisor, ihrer ein Multiplikator. `proxon_number()` nimmt die
Proxon-Zahlen entgegen und rechnet um — Registerzeilen lassen sich also 1:1
aus der Anlagendokumentation übernehmen. Ohne diese Umrechnung liest der
Behälterfühler T21 statt **57,5 °C** ganze **−842,5 °C**.

**Fremde Frames müssen verworfen werden.** Die USR-RS485-Bridge spiegelt den
gesamten Busverkehr in die TCP-Verbindung; einzelne Register enthalten dadurch
Werte anderer Geräte (klassisch: 128 °C Warmwasser). 46 der 94 Register tragen
deshalb einen gültigen Rohbereich. `nan=` der Bibliothek trifft nur einzelne
Werte, nicht Bereiche — `RangedNumberField` prüft `min_raw`/`max_raw` auf dem
vorzeichenbehafteten Rohwert und liefert sonst `None`.

Dazu kommt `Latin1StringField`: die Raumnamen liegen als zwei Latin-1-Bytes je
Register, das `StringField` der Bibliothek ist ASCII — „Küche" käme kaputt an.

## Aufbau

| Baugruppe | Register |
|---|---|
| `FwtInput` | Input 0–51, 154–265 |
| `FwtHolding` | Holding 16–22, 41–143, 187, 438, 460, 467–469, 613–619 |
| `T300Input` / `T300Holding` | Input 811–900, Holding 2000–2025 |
| `NbeTemperatures` | Input 590 + n·3, sieben Geräte |
| `NbeSettingsBank` | Holding 213/233/253 + n |
| `RoomNames` | Holding 620 + n·10, acht Slots |

Namens-Slot 0 ist die HBDE und hat **kein** NBE-Gerät; Slot 1–7 gehören zu den
Geräten 0–6 (`physical_index()`).

## Freischaltung

Register 438 = 55555 schaltet die vollen Modbus-Rechte frei. Ohne das antworten
460 und 467–469 nicht und Schreibzugriffe werden abgelehnt. `ProxonDevice`
erledigt das einmal je Verbindung — es ist ein Flash-Write im Gerät, gehört
also nicht in jeden Zyklus — und setzt es nach einem Verbindungsabriss neu.

## Teilausfall

Einzelne Blöcke scheitern im Normalbetrieb, ohne dass die Anlage weg wäre.
`async_update()` gibt die Namen der gescheiterten Baugruppen zurück und lässt
deren zuletzt gelesene Werte stehen; `ProxonUnreachable` fliegt erst, wenn
**keine** Baugruppe gelesen werden konnte.

## Tests

Vollständig ohne Hardware, über den Mock aus `modbus_connection`:

```bash
pip install -e ".[tmodbus,test]"
pytest
```

Die Testdaten sind echte Registerwerte, am 2026-09-02 von der Anlage gelesen.
