Нет описания

lrosales e61b818c97 Expand register coverage to all protocol v1.4.15 telemetry 1 месяц назад
docs e61b818c97 Expand register coverage to all protocol v1.4.15 telemetry 1 месяц назад
tools cee4eed3a7 Initial commit: MUST PV1800 (2024) Modbus RTU monitor 1 месяц назад
.gitignore cee4eed3a7 Initial commit: MUST PV1800 (2024) Modbus RTU monitor 1 месяц назад
README.md e61b818c97 Expand register coverage to all protocol v1.4.15 telemetry 1 месяц назад
must_pv1800_monitor.py e61b818c97 Expand register coverage to all protocol v1.4.15 telemetry 1 месяц назад

README.md

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

# 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 — 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.