DEV Community

Oleksandr
Oleksandr

Posted on

Why I Built a Modbus Simulator After a Year of Testing on Production

While leading engineering at an energy management system startup, I ran into a problem that kept getting worse the longer I ignored it.

Whenever my engineer and I needed to test something — even something as basic as reading telemetry from a battery — someone had to physically connect a device, pull a Docker image to the field hardware, run the code against it, and check whether the data made sense. Either by inspecting logs locally or watching values flow through the queue.

This is done even for the simplest operation like collecting telemetry. A read operation. Of course with write operation it was getting even more complicated.

We practically were testing on production. The assets weren't prequalified yet, every test cycle took longer than it should have, and I kept thinking about what happens when these batteries are actually running 24/7. You can't afford to pull a Docker image to a production BESS at 2am to check if your register parser is correct.

We were a small team moving fast and building a proper test environment kept getting pushed. So we kept going the way we had been — carefully, slowly, and with more anxiety than necessary.


The testing loop without a simulator looked pretty much like this:

No simulator testing loop

What We Were Building

For context: we were building an Energy Management System from scratch. The stack was real — GKE, MQTT, TimescaleDB, Pub/Sub, BigQuery, Dagster, Grafana, Raspberry Pis in the field talking Modbus to actual batteries. Up to five engineers at peak. Real assets, real protocols, real production pressure.

The core job of an EMS is to talk to energy devices — batteries, inverters, meters — read their state, and send commands. In our case we were preparing to participate in the Swedish frequency regulation market — FCR-D up, FCR-D down, FCR-N. These are ancillary services where your battery responds to disturbances in grid frequency, charging or discharging within seconds to help stabilise the grid. The protocol between EMS and device is usually Modbus TCP: a simple binary protocol over TCP where you read and write registers at specific addresses.

The prequalification process for these markets is a formal test — roughly an hour where the TSO verifies your asset actually responds correctly to frequency disturbances. You can unit test the algebra behind your control logic all day. But at some point you have to run your code against a real device and watch every register react correctly to a simulated disturbance event. One hour, one shot, no room for a parsing bug you didn't catch.

The problem with testing this kind of system is that the other side of the connection is a physical device. You can't mock a Modbus server in a unit test and call it done — register maps differ between manufacturers, scale factors are easy to get wrong, and the only way to know your telemetry parsing is correct is to see real values come back from something that behaves like the actual hardware.

Without a simulator, every test required the physical device. And physical devices are expensive, shared, and inconveniently located.


The Idea

So over the last few weeks I built what I wished we'd had: a simulator for distributed energy resources that speaks real Modbus TCP, runs on your laptop, and behaves like actual hardware.

Instead of connecting your EMS to a real battery, you connect it to a simulator that:

  • Accepts Modbus TCP connections on configurable ports
  • Exposes the same register map as the real device
  • Actually models the physics — SOC changes when you charge, power ramps up at the configured rate, PV output follows an irradiance model
  • Lets you inject grid events — voltage sags, frequency deviations — to test fault handling
  • Runs deterministically so you can reproduce the same scenario twice

You define your site in a JSON config file — which devices, which ports, what parameters — and the simulator builds the whole thing. Want a dual-BESS site with a PV inverter, energy meter, and a grid frequency model starting at solar noon? That's a config file.

The project is called DER Twin — github.com/AlexSpivak/dertwin — and the rest of this article walks through what it looks like in practice.


What It Looks Like

Start the simulator:

python -m dertwin.main -c configs/simple_config.json
Enter fullscreen mode Exit fullscreen mode
INFO | Building site: local-dev-site
INFO | Starting Modbus server | 0.0.0.0:55001 | unit=1
INFO | Simulation engine started | step=0.100s
Enter fullscreen mode Exit fullscreen mode

Connect your EMS — or the included example — from another terminal:

python examples/main_simple.py
Enter fullscreen mode Exit fullscreen mode
[EMS] Connected to BESS
[EMS] Starting in CHARGE mode
[EMS] STATUS=1 | SOC= 42.30% | P=  -20.00 kW | MODE=charge
[EMS] STATUS=1 | SOC= 44.10% | P=  -20.00 kW | MODE=charge
[EMS] Reached 60% → switching to DISCHARGE
Enter fullscreen mode Exit fullscreen mode

The EMS is talking Modbus TCP to a simulated BESS. SOC is changing. Power is ramping. It works exactly like the real thing — because from the EMS's perspective, it is the real thing.

For a full site with PV producing at noon, dual BESS cycling independently, and an energy meter tracking net grid power:

[SITE] Grid= -34.63 kW | PV= 24.60 kW (producing) | Freq=50.000 Hz
  [BESS-1] RUN  | SOC= 59.8% | P= +50.00 kW | MODE=discharge
  [BESS-2] RUN  | SOC= 40.2% | P= -25.00 kW | MODE=charge
Enter fullscreen mode Exit fullscreen mode

Grid exporting 34.6 kW because PV is generating 24.6 kW and BESS-1 is discharging at 50 kW while BESS-2 charges at 25 kW. The math is right. The physics is right.


Why This Changes the Development Loop

With a simulator, the workflow becomes straightforward:

  1. Write your Modbus client code
  2. Start the simulator
  3. Run your code against it
  4. If something is wrong, fix it immediately — no hardware, no waiting, no pulling Docker images at 2am
  5. Write integration tests that run the simulator headlessly and assert on telemetry values
  6. Merge with confidence

That last point is the one I care about most. The simulator supports a headless mode where there is no real-time clock — you drive it step by step from your test code, the same way test_site_controller.py does in the project. That means you can write integration tests that spin up a full simulated site, run thousands of simulation steps in milliseconds, and assert that your EMS made the right decisions at every point. No hardware, no timing dependencies, no flaky tests.

The fragile, anxiety-inducing "test on production" loop is gone. You know your code works before it touches a real device.


Now the testing loop with a simulator looks like this:

Testing loop with simulator


The Project

DER Twin is open source and MIT licensed.

In the next article I'll walk through the architecture — how the engine, device models, and protocol layer are separated, and why that separation matters if you want to add new device types or protocols.

If you're building EMS software or working with Modbus — let me know if this is useful.

Top comments (0)