Modbus Proxy (vendor → SunSpec translation)

The simulator can run a Modbus TCP translation proxy alongside its own SunSpec server. The proxy presents a vendor-specific register map to upstream clients (a SCADA, an EMS, an aggregator), translates each read / write into the matching SunSpec register on the backing simulator, and applies type / scale / byte-order conversions where the two layouts differ.

This is what lets you run an aggregator's "Vendor X inverter adapter" against the simulator without touching the aggregator's code: the proxy speaks Vendor X on the front and SunSpec on the back, and the simulator never knows the difference.

The data path is two hops:

Hop What Port / address Wire format
1 Upstream client → Modbus Proxy port 5020 (JSON-driven map) Vendor-specific register layout
2 Modbus Proxy → dersim port 8502, unit 1 (40000+ register space) SunSpec

The proxy is the only piece that knows both formats — the client sees its native vendor map, dersim sees clean SunSpec.

Quick start

Pass --proxy PATH pointing at a register-map JSON. The proxy binds the port the map declares (default 5020) and connects to the simulator's SunSpec server on --proxy_host:--proxy_port (both default to the local simulator):

./sim --device_type PV-3Phase \
      --proxy maps/vendor_x.json

The proxy starts up by scanning the simulator's SunSpec model chain to build an address lookup table, then exposes the vendor map. Stop the simulator and the proxy comes down with it.

CLI flag reference

Flag Default Notes
--proxy (off) Path to a register-map JSON. Required to start the proxy
--proxy_host 0.0.0.0 Proxy bind address
--proxy_port 502 Proxy TCP port. Overridden by the map's proxy_port if set

The map's proxy_port and proxy_unit_id take precedence over the flags. The flags are convenience for single-map runs; the map fields are authoritative for shipped maps in version control.

Register map JSON

A map declares the vendor's register layout and how each register backs to a SunSpec point. The minimal shape:

{
  "name": "Vendor X Inverter",
  "description": "Vendor X register map for testing aggregator integrations",
  "proxy_port": 5020,
  "proxy_unit_id": 1,
  "downstream_host": "127.0.0.1",
  "downstream_port": 8502,
  "downstream_unit_id": 1,
  "registers": [
    {
      "address": 0,
      "count": 16,
      "name": "Manufacturer",
      "type": "string",
      "access": "r",
      "static": "VendorX Corp"
    },
    {
      "address": 100,
      "count": 2,
      "name": "Active_Power_W",
      "type": "uint32",
      "access": "r",
      "sunspec": {
        "model": 703,
        "point": "W"
      }
    },
    {
      "address": 200,
      "count": 1,
      "name": "Power_Setpoint_Pct",
      "type": "uint16",
      "access": "rw",
      "sunspec": {
        "model": 705,
        "point": "WSetEna"
      }
    }
  ]
}

Each entry can be one of:

Kind static sunspec Effect
Static read-only string / number Returns the static value on every read; writes rejected
SunSpec-backed read {model, point} Reads pull from the live SunSpec point with scale-factor applied
SunSpec-backed read-write {model, point} + "access": "rw" Writes translate back into the SunSpec point

Type conversion handles the common shape mismatches:

  • string ↔ ASCII byte sequence (padded with NUL to count words)
  • uint16 / int16 ↔ single-word integer
  • uint32 / int32 ↔ two-word integer (high word first by default)
  • float32 ↔ two-word IEEE 754

Scale factors from the backing SunSpec model are applied automatically — if a register declares the active-power point and the SunSpec model says W_SF = -2, the proxy multiplies / divides by 10^-2 on the wire so the vendor map reads engineering units even when the SunSpec side stores raw integers.

When to use the proxy

  • Aggregator interop testing. Drive a real aggregator's vendor-specific adapter against a simulated DER without writing a custom mock.
  • Migration testing. Confirm a SunSpec migration path by running both the vendor map and the SunSpec server side-by-side and comparing reads from each.
  • Recording vendor maps. Run the proxy against a real device through a separate scanner, log the bytes, and capture an initial map for a vendor that doesn't publish one.

Bundled maps

The dersim repo ships a couple of example maps under src/modbus-proxy/modbus_proxy/maps/:

  • example_inverter.json — annotated reference map covering every supported register kind. Use as a starting template.
  • L233_Edge_EMS.json — a real-world map for the L233 Edge EMS product, exercised in dersim's integration tests.

The raw vendor product references that informed those maps live under src/modbus-proxy/raw_product_maps/ for historical context.

Stopping cleanly

The proxy runs as a sidecar coroutine inside the simulator process — stop the simulator and the proxy port releases cleanly. For "is the proxy alive" probes, a SunSpec scan against the vendor map's proxy_port is the canonical check.