Private investigator for Magnum Energy inverter/charger RS-485 networks
Project description
magnum-pi
I know what you're thinking, and you're right.
Your Magnum Energy MS-Series inverter/charger has been broadcasting its secrets on a proprietary RS-485 bus since the day it was installed. Every 100 milliseconds, it transmits DC voltage, AC output, fault codes, battery temperature, charger state — the full dossier. The remote panel answers back with your configured setpoints. The AGS and BMK chime in from the back seat with generator status and battery state-of-charge. All of it flowing across two wires at 19200 baud, unencrypted, no authentication, just trust.
Someone needs to investigate.
╭──────────────────────────────────╮
│ ╭───────────────╮ │
│ ╰──╮ ╭──╯ MAGNUM │
│ ╰─────────╯ P . I . │
│ │
│ Private Investigator for │
│ Magnum Energy RS-485 Networks │
╰──────────────────────────────────╯
Async Python library for sniffing, decoding, and transmitting packets on the Magnum Network bus. Plug in any RS-485 adapter — USB dongle, Raspberry Pi HAT, FTDI cable — and start investigating. Built on asyncio with Pydantic models, typed enums, gap-based protocol framing, and a CLI that puts the data on your terminal in seconds.
Install
pip install magnum-pi
Requires Python 3.11+. Dependencies (pyserial, pyserial-asyncio, pydantic) are installed automatically.
Hardware
You need an RS-485 adapter connected to the Network RJ-11 port on the front of your Magnum inverter (or the daisy-chain port on your ME-RC remote). The bus uses two wires: pin 1 (Data+) and pin 4 (Data−), 19200 baud, 8N1.
Any adapter that presents a serial port to the OS will work:
| Adapter | Interface | Notes |
|---|---|---|
| USB-to-RS-485 dongle | /dev/ttyUSB0 |
Most common. CH340, FTDI, CP2102 chipsets all work. |
| Waveshare RS-485 CAN HAT | /dev/ttyAMA0 |
Raspberry Pi GPIO. Excellent for headless monitoring. |
| FTDI cable (TTL-RS485) | /dev/ttyUSB0 |
Industrial-grade, screw terminals. |
Auto-detection checks /dev/ttyUSB*, /dev/ttyAMA*, /dev/ttyS0, and /dev/ttyACM* in order. Pass -d /dev/yourdevice to any CLI command to skip auto-detection.
CLI
Sniff — raw hex packets
magnum-pi sniff
magnum-pi sniff -d /dev/ttyUSB0 -n 20 # Capture 20 packets
One line per packet, showing the identified type, length, and raw hex:
[INVERTER ] (21 bytes) 400000F60016770001003D1133246B010005025800
[REMOTE ] (21 bytes) 00002808640A2800009B840C14122014007300A0
[AGS_STATUS] ( 6 bytes) A102343A007F
[BMK ] (18 bytes) 814C09F10074...
[RTR ] ( 2 bytes) 9120
Monitor — decoded live data
magnum-pi monitor --pretty
magnum-pi monitor # JSON lines (for piping)
magnum-pi monitor -n 5 # Stop after 5 cycles
Pretty mode prints a dashboard updated every cycle (~100ms):
── Inverter ──────────────────────────────
Status: INVERT DC: 24.6V 22A
AC Out: 119V 5A 60.0Hz AC In: 0V 0A
Temps: bat=17°C xfmr=51°C fet=36°C
Model: MS4024PAE Stack: PARALLEL_MASTER
── BMK Battery Monitor ───────────────────
SOC: 76% 25.45V 11.6A (charging)
Range: 20.16V min / 30.80V max
── AGS Generator ─────────────────────────
Status: READY 12.7V 58°F Runtime: 0.0h
JSON mode outputs one object per cycle — pipe it to jq, log it, or feed it to your monitoring stack.
Send — transmit commands
magnum-pi send --inverter toggle # Toggle inverter on/off
magnum-pi send --charger toggle # Toggle charger on/off
magnum-pi send --voltage 24 # Manual system voltage override
The send command reads the current remote configuration from the bus first, applies your change, then transmits the modified packet. This read-modify-write approach preserves all your existing setpoints — shore amps, battery size, charger percentage, everything.
Python API
Read bus cycles
import asyncio
from magnum_pi import MagnumBus
async def main():
async with MagnumBus("/dev/ttyUSB0") as bus:
cycle = await bus.read_cycle()
if cycle.inverter:
inv = cycle.inverter
print(f"{inv.status.name} {inv.dc_volts}V {inv.dc_amps}A")
print(f"AC out: {inv.ac_volts_out}V @ {inv.ac_freq_hz}Hz")
print(f"Model: {inv.model.name}")
if cycle.bmk:
print(f"SOC: {cycle.bmk.soc_pct}% {cycle.bmk.dc_volts}V")
if cycle.ags_status:
print(f"AGS: {cycle.ags_status.status.name}")
asyncio.run(main())
Continuous monitoring
async with MagnumBus("/dev/ttyUSB0") as bus:
async for cycle in bus.listen():
data = cycle.to_dict() # Flat dict, JSON-serializable
send_to_influxdb(data) # Your monitoring pipeline
Send commands
from magnum_pi import MagnumBus
from magnum_pi.models.remote import RemoteBase, RemotePacket
async with MagnumBus("/dev/ttyUSB0") as bus:
# Read current config, modify, transmit
cycle = await bus.read_cycle()
updated = cycle.remote.base.model_copy(update={"inverter_toggle": True})
await bus.send_remote_packet(RemotePacket(base=updated))
Convenience properties
async with MagnumBus("/dev/ttyUSB0") as bus:
await bus.read_cycle() # Populates internal state
bus.inverter # Most recent InverterPacket
bus.bmk # Most recent BMKPacket
bus.remote # Most recent RemotePacket
bus.voltage_multiplier # Auto-detected: 1 (12V), 2 (24V), or 4 (48V)
Mock transport for testing
from magnum_pi import MagnumBus, MockTransport
transport = MockTransport(inter_packet_ms=5)
async with MagnumBus(transport=transport, gap_ms=15) as bus:
transport.inject(my_inverter_bytes)
transport.inject(my_remote_bytes)
transport.inject(next_inverter_bytes) # Triggers cycle boundary
cycle = await bus.read_cycle()
assert cycle.inverter is not None
Devices and packets
The Magnum Network bus carries packets from up to five device types. Each packet is a Pydantic model with from_bytes() / to_bytes() round-trip serialization.
| Device | Packet class | Header | Size | Role |
|---|---|---|---|---|
| Inverter | InverterPacket |
— (identified by length + revision byte) | 14-21 bytes | Bus master. Broadcasts status every ~100ms. |
| Remote | RemotePacket |
— (identified by cycle position) | 21 bytes | Slave. Carries user setpoints + muxed footer. |
| AGS | AGSStatusPacket |
0xA1 |
6 bytes | Generator auto-start controller. |
| AGS Counts | AGSCountsPacket |
0xA2 |
6 bytes | Generator runtime counters. |
| BMK | BMKPacket |
0x81 |
18 bytes | Battery monitor (SOC, voltage, current). |
| RTR | RTRPacket |
0x91 |
2 bytes | Router/terminal (firmware version only). |
Inverter fields
The inverter packet is the heartbeat of the bus. Core fields are always present (14+ bytes); extended fields require firmware revision 4.0+ (21 bytes).
| Field | Type | Example | Notes |
|---|---|---|---|
status |
InverterStatus |
INVERT |
Operating mode (standby, charge, invert, search) |
fault |
InverterFault |
NONE |
Fault code (0x00 = no fault) |
dc_volts |
float |
24.6 |
Battery voltage (0.1V resolution) |
dc_amps |
int |
22 |
Battery current |
ac_volts_out |
int |
119 |
AC output voltage |
ac_volts_in |
int |
0 |
AC input voltage (0 = no shore power) |
inverter_led |
bool |
True |
Inverter operating indicator |
charger_led |
bool |
False |
Charger operating indicator |
revision |
float |
6.1 |
Firmware version |
battery_temp_c |
int |
17 |
Battery temperature (°C) |
transformer_temp_c |
int |
51 |
Transformer temperature (°C) |
fet_temp_c |
int |
36 |
FET temperature (°C) |
model |
InverterModel |
MS4024PAE |
Model ID (extended, None if < rev 4.0) |
stack_mode |
StackMode |
PARALLEL_MASTER |
Stacking config (extended) |
ac_amps_in |
int |
0 |
AC input current (extended) |
ac_amps_out |
int |
5 |
AC output current (extended) |
ac_freq_hz |
float |
60.0 |
AC frequency (extended) |
BMK fields
| Field | Type | Example | Notes |
|---|---|---|---|
soc_pct |
int |
76 |
State of charge (%, 255 = calculating) |
dc_volts |
float |
25.45 |
Battery voltage (0.01V resolution) |
dc_amps |
float |
11.6 |
Current (signed — negative = discharge) |
min_volts |
float |
20.16 |
Lifetime minimum voltage |
max_volts |
float |
30.80 |
Lifetime maximum voltage |
amp_hours |
int |
Cumulative amp-hours (signed) | |
fault |
BMKFault |
NORMAL |
Status code |
Remote base fields
Every remote packet carries a base configuration block (bytes 0-15) plus one of six possible footer types, rotated each cycle.
| Field | Type | Range | Notes |
|---|---|---|---|
inverter_toggle |
bool |
Toggle inverter on/off | |
charger_toggle |
bool |
Toggle charger on/off | |
eq_toggle |
bool |
Enable equalization (auto-sets charger_toggle) |
|
search_watts |
int |
0-50 | Search mode threshold (watts) |
battery_size_ah |
int |
0-2550 | Battery capacity (step 10) |
charger_amps_pct |
int |
0-100 | Charger current limit (%) |
shore_amps |
int |
-128 to 127 | Shore power limit (signed) |
float_v |
float |
Float voltage (scaled by system voltage) | |
lbco_v |
float |
Low battery cutoff (scaled by system voltage) | |
force_bulk |
bool |
Force bulk charge mode | |
force_float |
bool |
Force float charge mode | |
force_silent |
bool |
Force silent mode |
Footer types cycle through BASE (0x00), AGS_LEGACY (0xA0), AGS_EXT_A-D (0xA1-0xA4), and BMK (0x80) — carrying AGS scheduling, SOC thresholds, warm-up/cool-down timers, and BMK configuration in the trailing bytes.
Supported models
The library recognizes all MS-Series models from the original Magnum protocol specification. The model ID (byte 14 of extended inverter packets) determines the voltage multiplier used to scale voltage fields throughout the protocol.
| Family | Models | System voltage |
|---|---|---|
| 12V | MM612, MM1212, MMS1012, ME1512, ME2012, ME2512, ME3112, MS2012, MS2812, MS2712E, and more | 12V (1x multiplier) |
| 24V | MM1324E, MM1524, RD1824, RD2624E, RD4024E, MS4124E, MS2024 | 24V (2x multiplier) |
| 48V | MS4024, MS4024AE, MS4024PAE, MS4448AE, MS4448PAE, MS4048, MS4348PE, and more | 48V (4x multiplier) |
How it works
The Magnum RS-485 protocol has no delimiters, no length prefix, and no CRC. Packets are framed entirely by silence on the wire — a gap of ~2ms between the last byte of one packet and the first byte of the next. The GapFramer watches the inter-character timing and emits a complete packet when the gap exceeds the threshold.
The CycleTracker groups packets into ~100ms bus cycles. Each cycle starts with an inverter packet (the bus master), followed by the remote's response, then optional AGS, BMK, and RTR packets. Cycle boundaries are detected by recognizing when a new inverter packet arrives while the current cycle already has data.
Voltage multiplier auto-detection happens on the first extended inverter packet — the model byte reveals whether this is a 12V, 24V, or 48V system, and all subsequent voltage fields in remote and AGS packets are scaled accordingly.
For the full protocol specification — byte-level packet tables, timing diagrams, value scaling lookups, and known ambiguities — see magnum-protocol.warehack.ing.
Prior art
pymagnum by Charles Godwin (BSD-3) — the original Python implementation, maintained since 2019. Read-only, synchronous, outputs JSON via the magdump CLI tool. The packet identification heuristics and scaling factors in magnum-pi build directly on Charles's work.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file magnum_pi-2026.3.1.1.tar.gz.
File metadata
- Download URL: magnum_pi-2026.3.1.1.tar.gz
- Upload date:
- Size: 49.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59aeca35f9b0d0911a9296f3393b91457113dbfbd3c2b417b8a52634af8d7ca2
|
|
| MD5 |
e324657b6352eed68556497e1d6c4098
|
|
| BLAKE2b-256 |
dcefb38a05c0920cf383b2729213674415224b256c856d428fa87e014912422c
|
File details
Details for the file magnum_pi-2026.3.1.1-py3-none-any.whl.
File metadata
- Download URL: magnum_pi-2026.3.1.1-py3-none-any.whl
- Upload date:
- Size: 34.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5dadc4f4578306bc7ef495887301c771a07ef188ba50412f8a18bf1cb3aa387f
|
|
| MD5 |
72eb9f4dee0136a4f8592d139a848b9b
|
|
| BLAKE2b-256 |
7f4229995f770ad18dec63cdd635270c6e98d807fa8a64aaa45996851700176c
|