# 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 ~350 W load from grid): ``` === 2026-09-10 17:13:36 === --------------- CHARGER --------------- --- State --- Charger workstate = SelfTest mode MPPT state = Stop Charging state = Stop --- Live readings --- PV voltage = 0.0V Battery voltage (charger) = 27.9V Charger current = 0.0A Charger power = 0.0W --- Sensors --- Charger radiator temp = 26.0°C External temp = 0.0°C --- Relays --- Battery Relay (charger) = Open PV Relay (charger) = Open --- Configuration --- Battery voltage grade = 24V system Charger rated current = 60.0A --- Energy totals --- Acc PV energy (total) = 0.1kWh --- Runtime --- Acc runtime days = 0.0d Acc runtime hours = 0.0h Acc runtime minutes = 0.0m --------------- INVERTER --------------- --- State --- Inverter Work state = ByPass --- Configuration --- AC voltage grade = 120V variant Rated power = 2000.0VA Battery voltage grade = 24V system --- Voltages --- Battery voltage (inverter) = 28.2V Inverter voltage = 116.0V Grid voltage = 117.5V BUS voltage = 254.9V --- Currents --- Control current = 0.0A Inverter current = 0.0A Grid current = 3.3A Load current = 3.2A --- Real power (P) --- PInverter = -14W PGrid = -363W PLoad = 352W Load percent = 18.0% --- Apparent power (S) --- SInverter = 0VA SGrid = 395VA SLoad = 352VA --- Reactive power (Q) --- QInverter = 0var QGrid = 152var QLoad = 127var --- Frequencies --- Inverter frequency = 60.0Hz Grid frequency = 60.0Hz --- Temperatures --- AC radiator temp = 38.0°C Transformer temp = 60.0°C DC radiator temp = 0.0°C --- Relays --- Inverter relay = Open Grid relay = Closed Load relay = Closed N_Line relay = Open DC relay = Open Earth relay = Open --- Energy totals --- Acc charger (total) = 0.0kWh Acc discharger (total) = 0.1kWh Acc grid-buy (total) = 17.0kWh Acc grid-sell (total) = 0.0kWh Acc load (total) = 16.0kWh Acc self-use (total) = 0.1kWh Acc PV-sell (total) = 0.0kWh Acc grid-charge (total) = 1.0kWh --- Battery telemetry --- Battery power = 16W Battery current = -3A --------------- SETTINGS --------------- Float voltage = 27.2V Absorb voltage = 28.2V Battery stop discharging voltage = 23.0V Battery stop charging voltage = 26.4V Battery low voltage = 20.4V Battery high voltage = 30.0V Charger current = 60.0A ``` ## 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`.