# must Real-time monitor for the **MUST PV1800 (2024)** solar inverter/charger via Modbus RTU. The PV1800 firmware is non-compliant — it ignores the `count` field and often dumps a 39-register block in response to any read. This script handles that by reading the **whole block in one go** and extracting the registers we care about, which is the only way to unambiguously identify which response belongs to which request. ## Hardware - **Inverter:** MUST PV1800 (2024 variant, 24V battery / 120V AC). - **USB-serial adapter:** CH340 (vendor `1a86:7523`), shows up as `/dev/ttyUSB0` on the remote host. - **Remote host:** Proxmox LXC at `192.168.44.94`, user `root`. The CH340 is plugged into this host directly. - **Baud:** 19200 8N1, Modbus RTU, slave ID 4. ## Files | File | Purpose | |------|---------| | `must_pv1800_monitor.py` | The monitor script. Self-contained — embeds the pyserial-based TCP proxy it runs on the remote host, plus the full PV1800 register map. | | `docs/register-map.md` | The PV1800 register map, derived from `taHC81/MUST-ESPhome/PH1800 PV1800 EP1800 PV3500 EP3500 RS485 Modbus RTU communication Protocol 1.4.15.xlsx`. | | `docs/quirks.md` | Things I learned the hard way about the PV1800 firmware and the CH340 driver. Read this before debugging "why isn't it working". | | `tools/test_proxy.py` | A standalone copy of the TCP<->serial proxy, useful for ad-hoc debugging with `nc` or `socat`. | ## Quick start ```bash # Live updates every 3 s (default) ./must_pv1800_monitor.py # One snapshot, for scripting ./must_pv1800_monitor.py --once # JSON output, one line per snapshot ./must_pv1800_monitor.py --json # If you're on the LXC with the USB cable attached directly ./must_pv1800_monitor.py --serial # If you already have a socat/ser2net bridge running on a remote host ./must_pv1800_monitor.py --tcp --tcp-host 192.168.44.94 --tcp-port 8502 ``` ## How it works ``` ┌─────────────────┐ SSH ┌────────────────────┐ pyserial ┌──────────────┐ │ this script │ ─────────► │ remote host LXC │ ────────────► │ /dev/ttyUSB0 │ │ (your laptop) │ ◄───────── │ 192.168.44.94 │ ◄──────────── │ (CH340) │ └─────────────────┘ └────────────────────┘ └──────┬───────┘ │ RS-485 ▼ ┌──────────────┐ │ MUST PV1800 │ │ slave ID 4 │ └──────────────┘ ``` 1. The script opens an SSH session to the remote host. 2. It uploads a tiny pyserial-based TCP<->serial proxy (`/tmp/.must_pv1800_proxy.py`) over SFTP and runs it as a detached background process. 3. The proxy pre-flushes stale bytes from the kernel TTY buffer (critical — see `docs/quirks.md`), then opens `/dev/ttyUSB0` and bridges it to `127.0.0.1:9500` on the remote. 4. The script tunnels back through SSH (direct-tcpip channel) to that localhost port and runs Modbus RTU over the resulting byte stream. 5. Each cycle polls three register blocks (CHARGER, INVERTER, SETTINGS), prints a snapshot, sleeps `--interval` seconds, and loops. ## Dependencies - Local: `pyserial`, `paramiko`. Install with: ``` pip install --break-system-packages pyserial paramiko ``` - Remote: `python3-serial` (apt package). The script auto-installs it if missing — just needs `apt-get` available. ## Output Example snapshot (panels disconnected, inverter in ByPass mode feeding a 350W load from grid): ``` === 2026-09-10 16:51:55 === --------------- CHARGER --------------- Charger workstate = Standby MPPT state = Stop Charging state = Stop PV voltage = 0.0V Battery voltage = 27.7V Charger current = 0.0A Charger power = 0.0W --------------- INVERTER --------------- Inverter Work state = ByPass Battery voltage = 28.2V Inverter voltage = 115.7V Grid voltage = 117.0V Inverter power = -17W Grid power = -369W Load power = 351W System load = 18.0% AC radiator temp = 33.0°C Transformer temp = 52.0°C Battery power = 13W Battery current = -3A ``` ## References - [taHC81/MUST-ESPhome](https://github.com/taHC81/MUST-ESPhome) — ESPHome config for a similar inverter. Their `PV18-output-example.log` shows the register map in action. - `PH1800 PV1800 EP1800 PV3500 EP3500 RS485 Modbus RTU communication Protocol 1.4.15.xlsx` (in the same repo) — the authoritative register map for the inverter family. Parsed copy in `docs/register-map.md`. ## Caveats / known issues - The PV1800 occasionally drops a byte during a long Modbus dump. The script retries on CRC mismatch and logs `[poll error: ...]` to stderr. The next cycle almost always succeeds. - DC radiator temp reads as 0.0°C on units that don't have that sensor installed. Not a bug. - Inverter power shows a small negative value in ByPass mode — that's the measurement offset of the inverter just passing grid through. - The script keeps a proxy running on the remote host between invocations. If `Ctrl-C` doesn't clean it up, run on the remote: `pkill -9 -f must_pv1800_proxy`.