Skip to main content

Private investigator for Magnum Energy inverter/charger RS-485 networks

Project description

magnum-pi

PyPI Python License: MIT

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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

magnum_pi-2026.3.1.1.tar.gz (49.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

magnum_pi-2026.3.1.1-py3-none-any.whl (34.9 kB view details)

Uploaded Python 3

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

Hashes for magnum_pi-2026.3.1.1.tar.gz
Algorithm Hash digest
SHA256 59aeca35f9b0d0911a9296f3393b91457113dbfbd3c2b417b8a52634af8d7ca2
MD5 e324657b6352eed68556497e1d6c4098
BLAKE2b-256 dcefb38a05c0920cf383b2729213674415224b256c856d428fa87e014912422c

See more details on using hashes here.

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

Hashes for magnum_pi-2026.3.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5dadc4f4578306bc7ef495887301c771a07ef188ba50412f8a18bf1cb3aa387f
MD5 72eb9f4dee0136a4f8592d139a848b9b
BLAKE2b-256 7f4229995f770ad18dec63cdd635270c6e98d807fa8a64aaa45996851700176c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page