Skip to main content
Glama
aydho

sigenergy-mcp

by aydho
README.md
# sigenergy-mcp

An [MCP](https://modelcontextprotocol.io) server for monitoring and controlling a
**Sigenergy** energy system (SigenStor, Sigen hybrid inverters) locally over
**Modbus TCP** — no cloud account needed.

It exposes live PV, battery, grid and load data, inverter details and Sigen EV charger
status as MCP tools, and (optionally) control tools for remote EMS mode, battery
charge/discharge limits, grid export/import limits, SOC limits and EV charging.

## Prerequisites

1. **Enable Modbus TCP** on your Sigenergy system. In the mySigen app (installer
   account) go to the device → *Settings* → *Communication* → *Modbus TCP Server*
   and turn it on. Your installer can do this for you if you don't have access.
2. Give the system a **static IP / DHCP reservation** on your LAN.
3. Note the Modbus **device ids**: the plant (whole system, aggregated by the EMS)
   is `247`; inverters start at `1` (multiple inverters are `1`, `2`, …). A Sigen
   **AC charger** normally takes id `1`, so the inverter is moved to another id
   (e.g. `2`). A **DC charger** answers on its inverter's id. If you're unsure, run
   the `discover_devices` tool once — it reports the ids and the settings to use.
4. Python 3.10+.

## Install

```bash
pip install git+https://github.com/aydho/sigenergy-mcp
# or, from a checkout
pip install -e .
```

Or run without installing using [uv](https://docs.astral.sh/uv/):

```bash
uvx --from git+https://github.com/aydho/sigenergy-mcp sigenergy-mcp --host 192.168.1.50
```

## Configuration

| Environment variable      | CLI flag          | Default | Description                                 |
|---------------------------|-------------------|---------|---------------------------------------------|
| `SIGENERGY_HOST`          | `--host`          | —       | IP address of the Sigenergy system (required) |
| `SIGENERGY_PORT`          | `--port`          | `502`   | Modbus TCP port                              |
| `SIGENERGY_PLANT_ID`      | `--plant-id`      | `247`   | Plant device id                              |
| `SIGENERGY_INVERTER_IDS`  | `--inverter-ids`  | `1`     | Comma-separated inverter device ids          |
| `SIGENERGY_AC_CHARGER_IDS`| `--ac-charger-ids`| —       | AC charger (EVAC) device ids, usually `1`    |
| `SIGENERGY_DC_CHARGER_IDS`| `--dc-charger-ids`| —       | DC charger (EVDC) ids = the inverter's id    |
| `SIGENERGY_ALLOW_WRITES`  | `--allow-writes`  | `false` | Enable control (write) tools                 |
| `SIGENERGY_AUDIT_LOG`     | `--audit-log`     | —       | File to append a JSON line to for every write |
| `SIGENERGY_HTTP_TOKEN`    |                   | —       | Bearer token required by the HTTP transport  |
| `SIGENERGY_HTTP_ALLOWED_HOSTS` |              | —       | Allowed `Host` headers for HTTP, e.g. `pi.local:8000,192.168.1.20:8000` |
| `SIGENERGY_TIMEOUT`       |                   | `10`    | Modbus request timeout, seconds              |
| `SIGENERGY_LOG_LEVEL`     |                   | `WARNING` | Python log level                           |

Writes are **off by default**: the control tools aren't even listed to the client until you opt in.

### Claude Desktop / Claude Code

```json
{
  "mcpServers": {
    "sigenergy": {
      "command": "sigenergy-mcp",
      "env": {
        "SIGENERGY_HOST": "192.168.1.50",
        "SIGENERGY_INVERTER_IDS": "2",
        "SIGENERGY_AC_CHARGER_IDS": "1",
        "SIGENERGY_ALLOW_WRITES": "false"
      }
    }
  }
}
```

With Claude Code:

```bash
claude mcp add sigenergy -e SIGENERGY_HOST=192.168.1.50 -- sigenergy-mcp
```

### HTTP transport

To run it as a network service (e.g. on a Raspberry Pi next to the inverter):

```bash
export SIGENERGY_HTTP_TOKEN=$(openssl rand -hex 32)
export SIGENERGY_HTTP_ALLOWED_HOSTS=pi.local:8000,192.168.1.20:8000
sigenergy-mcp --host 192.168.1.50 --transport streamable-http --http-host 0.0.0.0 --http-port 8000
# MCP endpoint: http://<pi>:8000/mcp  — clients send "Authorization: Bearer <token>"
```

```bash
claude mcp add --transport http sigenergy http://pi.local:8000/mcp \
  --header "Authorization: Bearer $SIGENERGY_HTTP_TOKEN"
```

The server refuses to listen on a non-loopback address without a token (override with
`--allow-unauthenticated` only if an authenticating proxy sits in front).

## Security

**Modbus TCP itself has no authentication or encryption.** Anything that can reach the
Sigenergy system on port 502 can read every register and change its settings; the device id
is just an address, not a secret. This server can't change that, so:

* **Never port-forward 502** to the inverter. Keep it on your LAN, ideally on its own
  VLAN/IoT network with firewall rules so only the machine running this server (or Home
  Assistant) can reach port 502.
* `SIGENERGY_ALLOW_WRITES` only controls what *this server* will do. It doesn't stop other
  devices on your network writing to the inverter directly.

What the server does to protect its own interface:

* **Read-only by default.** Without `SIGENERGY_ALLOW_WRITES=true` the control tools are
  removed from the server, so a client can't see or call them. Every write is still range- and
  enum-checked.
* **Audit log.** Set `SIGENERGY_AUDIT_LOG=~/sigenergy-audit.jsonl` to record every write
  attempt (time, device, register, previous value, new value, success or error), so you can
  see and undo what was changed.
* **HTTP transport.** It binds to `127.0.0.1` by default, with Host/Origin checks against DNS
  rebinding. When exposed on the network it requires a bearer token (`SIGENERGY_HTTP_TOKEN`),
  compared in constant time; set `SIGENERGY_HTTP_ALLOWED_HOSTS` to keep the Host/Origin checks
  too. Pass the token in the environment rather than on the command line so it doesn't show in
  `ps`.
* **TLS.** The server speaks plain HTTP. For HTTPS or access from outside your home, put it
  behind [Tailscale](https://tailscale.com) / WireGuard or a reverse proxy such as Caddy rather
  than opening a port.

## Tools

**Monitoring (read-only)**

| Tool | Description |
|------|-------------|
| `get_plant_status` | PV, battery, grid and load power, SOC, EMS mode, running state, decoded active alarms |
| `get_battery_status` | SOC, SOH, power, rated/available energy, power limits, cut-off SOCs |
| `get_grid_status` | Grid power (total and per phase), on/off-grid status |
| `get_energy_totals` | kWh counters: PV today/yesterday/lifetime, load, battery charge/discharge, grid import/export |
| `get_inverter_status` | Per-inverter power, battery, energy counters, temperatures, AC phases, PV strings |
| `get_inverter_info` | Model, serial number, firmware, ratings |
| `get_ev_charger_status` | AC charger state (no vehicle / connected / charging…), power, energy, alarms; DC charger state, power, vehicle SOC, session energy |
| `discover_devices` | Scan device ids to find the plant, inverters and EV chargers, with suggested settings |
| `get_control_settings` | Current remote EMS settings, power limits and SOC limits |
| `list_registers` | All named registers with address, type, unit and gain |
| `read_registers` | Read named registers |
| `read_raw_registers` | Read any address (for registers not in the map) |

**Control (requires `SIGENERGY_ALLOW_WRITES=true`)**

| Tool | Description |
|------|-------------|
| `set_remote_ems_mode` | Enable remote EMS and set mode (standby, self-consumption, command charge/discharge…) |
| `set_remote_ems_enabled` | Enable/disable remote EMS (disable to hand control back to the built-in EMS) |
| `set_battery_power_limits` | Max charge / discharge power (kW) |
| `set_grid_limits` | Max grid export / import (kW) |
| `set_pv_power_limit` | Limit PV output (kW) |
| `set_soc_limits` | Backup SOC, charge cut-off and discharge cut-off (%) |
| `start_ev_charging` / `stop_ev_charging` | Start or stop charging on the AC or DC charger |
| `set_ac_charger_current` | AC charging current (A), checked against the charger's rated and breaker current |
| `set_dc_charger_power_limits` | DC charger max charge / discharge (V2G) power (kW) |
| `write_register` | Write any writable register in the map by name |

### Sign conventions

* `grid_active_power` > 0 = importing from the grid, < 0 = exporting.
* `ess_power` > 0 = battery charging, < 0 = discharging.
* `load_power` is read from the system's load register (30282) on newer firmware, otherwise derived as
  plant AC output + grid power + third-party PV − AC charger power (so it excludes EV charging).

### Example: force-charge the battery from the grid at 5 kW

1. `set_remote_ems_mode(mode=3)` — command charging (grid first)
2. `set_battery_power_limits(max_charge_kw=5)`
3. Later, `set_remote_ems_enabled(false)` to return to normal operation.

## Register map

The register map lives in [`src/sigenergy_mcp/registers.py`](src/sigenergy_mcp/registers.py)
and follows the Sigenergy Modbus Protocol (V2.x). Available registers can vary with
firmware version; registers the device rejects are reported per-register in an
`errors` field instead of failing the whole call. Registers that answer "illegal data
address" are remembered per device and skipped on later calls. Use `read_raw_registers`
for anything not yet mapped.

Addresses, types and gains are cross-checked against
[TypQxQ/Sigenergy-Local-Modbus](https://github.com/TypQxQ/Sigenergy-Local-Modbus), the
field-tested Home Assistant integration; alarm bit descriptions are adapted from it (MIT).

## Development

```bash
pip install -e '.[dev]'
pytest
```

The tests run the MCP tools end-to-end against an in-process pymodbus simulator.

## Disclaimer

Not affiliated with Sigenergy. Writing Modbus registers changes how your system
operates — use the control tools at your own risk and check settings in the
mySigen app afterwards.

TDQS

A3.8/5.0

Scored across 12 tools

Disambiguation4/5

Each tool targets a distinct subsystem (plant, grid, battery, inverter, EV charger, energy totals, registers), so boundaries are mostly clear. The only mild overlap is that get_plant_status summarizes data also exposed by the individual status tools, but descriptions clarify the live-overview vs subsystem-detail distinction.

Naming Consistency5/5

Names follow a consistent verb_noun snake_case pattern (get_*_status, get_*_info, list_registers, read_registers, read_raw_registers, discover_devices). The verbs (get/list/read/discover) map predictably to action type.

Tool Count5/5

12 tools is well within the sweet spot for a monitoring/inspection domain, and each earns its place by covering a distinct device or data access layer.

Completeness4/5

The read surface is thorough: live status, static info, energy counters, settings, named-register and raw-register access, device discovery, and EV charging. The notable gap is write/control operations (e.g. setting power or SOC limits) implied by get_control_settings, though read_raw_registers partially mitigates unmapped reads.

Maintenance

ActivityMaintained
ResponsivenessNo issues