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 tocountwords)uint16/int16↔ single-word integeruint32/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.