Metadata-Version: 2.4
Name: dertwin
Version: 0.1.8
Summary: Digital twin simulator for distributed energy resources — BESS, PV inverters, energy meters
Author-email: Oleksandr Spivak <oleksdertwin@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Oleksandr Spivak
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/AlexSpivak/dertwin
Project-URL: Repository, https://github.com/AlexSpivak/dertwin
Project-URL: Bug Tracker, https://github.com/AlexSpivak/dertwin/issues
Keywords: modbus,digital-twin,bess,energy,ems,der,simulator
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pymodbus<3.13,>=3.8
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Dynamic: license-file

# DER Twin

Digital Twin infrastructure for modern energy systems.

**DER Twin** is a lightweight simulator for Distributed Energy Resources (DER) — BESS, PV inverters, CHP units, energy meters, and grid models — exposed via Modbus TCP and Modbus RTU. Use it for EMS development, protocol testing, integration validation, and control algorithm sandboxing without touching real hardware.

---

## ⚡ Quickstart

### Option A — pip install

```bash
pip install dertwin
```

Bring your own site config and register maps:

```bash
dertwin -c path/to/your/config.json
```

You should see:
```
INFO | Building site: my-site
INFO | Starting Modbus TCP server | 0.0.0.0:55001 | unit=1
INFO | Simulation engine started | step=0.100s
```

The simulator is now accepting Modbus TCP connections on the ports defined in your config.

### Option B — Run from source

```bash
git clone https://github.com/AlexSpivak/dertwin.git
cd dertwin
python -m venv .venv && source .venv/bin/activate
pip install -e .
python -m dertwin.main -c configs/simple_config.json
```

### Option C — Run with Docker

```bash
git clone https://github.com/AlexSpivak/dertwin.git
cd dertwin
python generate_compose.py configs/simple_config.json
docker compose up --build
```

`generate_compose.py` reads the config and generates a `docker-compose.yml` with the correct ports automatically. No manual port configuration needed. For mixed-protocol configs with RTU, see [Docker with RTU](#docker-with-rtu) below.

---

## 🔌 Connect an EMS

With the simulator running, start the example EMS from a second terminal:

```bash
cd examples
python main_simple.py
```

You'll see the EMS connecting over Modbus and cycling the BESS between 40–60% SOC:

```
[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
```

For a full multi-device site (dual BESS + PV + CHP + energy meter + external models):

```bash
python -m dertwin.main -c configs/full_site_config.json
# in another terminal:
python examples/main_full.py
```

You'll see the CHP go through its realistic startup sequence (~2 minutes) before reaching `RUNNING` and dispatching power, while the BESS units cycle and the energy meter aggregates the full site balance.

For a mixed-protocol site (BESS on TCP + PV and meter on RTU), see [Mixed Protocol Example](#-mixed-protocol-example-tcp--rtu) below.

---

## 🧱 Features

- Async Modbus TCP and RTU servers built on `pymodbus`
- Mixed-protocol support — TCP and RTU devices on the same site
- Full 16-bit Modbus address space on all four datastores (discrete inputs, coils, input registers, holding registers)
- Per-register function code routing — FC02 discrete inputs for binary flags, FC03/04 for analog telemetry
- Per-register endianness — big-endian default, little-endian for Sungrow/Carlo Gavazzi-style devices
- Realistic CHP simulation — MWM TEM Evolution-compatible state machine, configurable startup timings, thermal physics, heat output
- Config-driven site topology — add devices by editing JSON
- Irradiance, ambient temperature, grid frequency, and grid voltage models
- Multi-device support across independent ports
- External model events (voltage sags, frequency deviations)
- Simulation start time control (`start_time_h`) — start at noon, peak load, etc.
- Docker support with auto-generated Compose files and RTU-over-TCP bridging
- Deterministic simulation with seeded random models
- Fully tested with `pytest`

---

## 📦 Repo Structure

```
dertwin/
├── configs/
│   ├── register_maps/              # Modbus register definitions (YAML)
│   ├── simple_config.json          # Single BESS — good starting point
│   ├── demo_config.json            # BESS + PV + meter
│   ├── full_site_config.json       # BESS + BESS + PV + CHP + meter + external models
│   └── mixed_protocol_config.json  # BESS (TCP) + PV (RTU) + meter (RTU)
├── dertwin/
│   ├── core/                # Clock, engine, register map loader
│   ├── controllers/         # Site and device orchestration
│   ├── devices/             # BESS, PV, CHP, energy meter, external models
│   ├── protocol/            # Modbus TCP + RTU servers
│   ├── telemetry/           # Telemetry dataclasses
│   └── main.py
├── examples/
│   ├── simple/              # Single BESS EMS example
│   ├── full/                # Multi-device EMS example with CHP (TCP)
│   ├── mixed/               # Mixed-protocol EMS example (TCP + RTU)
│   └── protocol/            # Shared Modbus TCP and RTU clients
├── tests/                   # Full test suite
├── generate_compose.py      # Docker Compose generator
├── docker-entrypoint.sh     # Container entrypoint (socat + RTU bridge)
└── Dockerfile
```

---

## ⚙️ Configuration

Sites are defined in JSON. Each asset declares its type, parameters, and protocol bindings:

```json
{
  "site_name": "my-site",
  "step": 0.1,
  "real_time": true,
  "start_time_h": 12.0,
  "register_map_root": "register_maps",
  "external_models": {
    "irradiance": { "peak": 1000.0, "sunrise": 6.0, "sunset": 18.0 },
    "grid_frequency": { "nominal_hz": 50.0, "noise_std": 0.002, "seed": 42 }
  },
  "assets": [
    {
      "type": "bess",
      "capacity_kwh": 100.0,
      "initial_soc": 60.0,
      "protocols": [{ "kind": "modbus_tcp", "ip": "0.0.0.0", "port": 55001, "unit_id": 1, "register_map": "bess_modbus.yaml" }]
    },
    {
      "type": "chp",
      "rated_kw": 4000.0,
      "heat_to_power_ratio": 1.0,
      "min_load_percent": 30.0,
      "max_load_percent": 110.0,
      "protocols": [{ "kind": "modbus_tcp", "ip": "0.0.0.0", "port": 55002, "unit_id": 1, "register_map": "chp_modbus.yaml" }]
    }
  ]
}
```

**`real_time: true`** — engine runs its own loop, use for `dertwin` CLI and EMS examples
**`real_time: false`** — caller drives the clock via `step_once()`, use for tests
**`start_time_h`** — sets simulation clock on startup (e.g. `12.0` for noon). All external models start from this time.
**`register_map_root`** — path to register map directory, resolved relative to the working directory where you run `dertwin`
**`ip: "0.0.0.0"`** — required when running inside Docker so port mapping works. Use `127.0.0.1` for local-only.

### Supported asset types

| `type` | Class | Notable parameters |
|---|---|---|
| `bess` | `BESSSimulator` | `capacity_kwh`, `initial_soc`, `max_charge_kw`, `max_discharge_kw`, `ramp_rate_kw_per_s` |
| `inverter` | `PVSimulator` | `rated_kw`, `module_efficiency`, `area_m2` |
| `chp` | `CHPSimulator` | `rated_kw`, `heat_to_power_ratio`, `min_load_percent`, `max_load_percent` |
| `energy_meter` | `EnergyMeterSimulator` | (no parameters — observes the site power model) |

### Protocol Configuration

Each asset's `protocols` array supports both Modbus TCP and Modbus RTU. A single device can expose multiple protocols simultaneously.

**Modbus TCP:**
```json
{ "kind": "modbus_tcp", "ip": "0.0.0.0", "port": 55001, "unit_id": 1, "register_map": "bess_modbus.yaml" }
```

**Modbus RTU:**
```json
{ "kind": "modbus_rtu", "port": "/tmp/dertwin_device", "baudrate": 9600, "parity": "N", "stopbits": 1, "unit_id": 1, "register_map": "bess_modbus.yaml" }
```

RTU parameters `baudrate`, `parity`, `stopbits`, `bytesize`, and `timeout` all have sensible defaults (9600/N/1/8/1.0) and can be omitted.

**Register map fields:**

| Field | Required | Description |
|---|---|---|
| `name` | yes | Human-readable label, used in logs and the EMS client |
| `internal_name` | yes | Maps to the device's internal telemetry or command field — must match the attribute name in the corresponding telemetry class (see [`dertwin/telemetry/README.md`](dertwin/telemetry/README.md)) |
| `address` | yes | Modbus register address (0–65535) |
| `type` | yes | `uint16`, `int16`, `uint32`, `int32` |
| `scale` | yes | Multiplier applied on read, divisor applied on write |
| `count` | yes | Number of registers (1 for 16-bit, 2 for 32-bit) |
| `func` | yes | Function code: `0x02` discrete input read, `0x03` holding read, `0x04` input read, `0x06` single write, `0x10` multi-register write |
| `direction` | yes | `read` or `write` |
| `endian` | no | `big` (default) or `little` — for devices like Sungrow BESS and Carlo Gavazzi meters that use little-endian 32-bit register layout |
| `unit` | no | Physical unit label (V, kW, Hz, etc.) |
| `description` | no | Free-text note |
| `options` | no | Enum mapping for status/mode registers |

`name` and `internal_name` can differ — `name` is what the EMS client sees, `internal_name` is what the device simulator uses internally. For example, `on_grid_power_setpoint` (name) maps to `active_power_setpoint` (internal_name) on the BESS device.

For detailed architecture and per-package docs, see [`dertwin/README.md`](dertwin/README.md).

---

## 🔀 Mixed Protocol Example (TCP + RTU)

This example runs a site with BESS on Modbus TCP and PV + energy meter on Modbus RTU. The EMS controls the BESS over TCP and monitors the RTU devices for observability.

### Prerequisites

Install `socat` to create virtual serial port pairs:

```bash
# macOS
brew install socat

# Ubuntu / Debian
sudo apt install socat
```

### Running the example

**Terminal 1** — create virtual serial pairs and start the simulator:

```bash
# Create virtual serial port pairs (simulator <-> EMS client)
socat -d -d pty,raw,echo=0,link=/tmp/dertwin_pv pty,raw,echo=0,link=/tmp/dertwin_pv_client &
socat -d -d pty,raw,echo=0,link=/tmp/dertwin_meter pty,raw,echo=0,link=/tmp/dertwin_meter_client &

# Start the simulator (from repo root)
dertwin -c configs/mixed_protocol_config.json
```

You should see:
```
INFO | Building site: mixed-protocol-site
INFO | Starting Modbus TCP server | 0.0.0.0:55001 | unit=1
INFO | Starting Modbus RTU server | port=/tmp/dertwin_pv | baudrate=9600 | unit=1
INFO | Starting Modbus RTU server | port=/tmp/dertwin_meter | baudrate=9600 | unit=1
INFO | Simulation engine started | step=0.100s
```

**Terminal 2** — run the mixed-protocol EMS:

```bash
cd examples
python main_mixed.py
```

Expected output:
```
[BESS-1] TCP connected
[PV] RTU connected
[METER] RTU connected
[BESS-1] Starting in CHARGE mode

[EMS] Mixed-protocol EMS running
  [BESS-1] RUN  | SOC= 50.0% | P= -30.00 kW | MODE=charge
  [PV]    P= 16.40 kW (producing)
  [METER] Grid= +23.57 kW (importing) | Freq=50.000 Hz | Import=0.1 kWh | Export=0.0 kWh
```

The key point: socat creates a **pair** of linked pseudo-terminals for each connection. The simulator opens one end (`/tmp/dertwin_pv`) and the EMS client opens the other (`/tmp/dertwin_pv_client`). Both sides must use different ends of the pair.

If RTU serial ports are unavailable, the EMS will still run with BESS-only control — PV and meter telemetry will show as unavailable.

---

## 🐳 Docker

### TCP-only configs

For configs that only use Modbus TCP, Docker setup is straightforward:

```bash
python generate_compose.py configs/full_site_config.json
docker compose up --build
```

TCP ports are mapped automatically from the config. Connect your EMS to `localhost:<port>`.

### Docker with RTU

RTU serial devices use pseudo-terminals (`/dev/pts/`) which exist only inside the container's kernel namespace and can't be accessed from the host via volume mounts. The entrypoint solves this by bridging each RTU serial port to a TCP port inside the container using a Python asyncio relay. From the host, `socat` converts the TCP connection back into a local PTY that the EMS opens as a normal serial port.

**Step 1** — generate the compose file and start the container:

```bash
python generate_compose.py configs/mixed_protocol_config.json
docker compose up --build
```

You should see:
```
[entrypoint] Creating serial pair: /tmp/dertwin_pv <-> /tmp/dertwin_pv_bridge
[entrypoint] Bridging /tmp/dertwin_pv_bridge -> TCP port 56001
[bridge] /tmp/dertwin_pv_bridge <-> TCP :56001
[entrypoint] Creating serial pair: /tmp/dertwin_meter <-> /tmp/dertwin_meter_bridge
[entrypoint] Bridging /tmp/dertwin_meter_bridge -> TCP port 56002
[bridge] /tmp/dertwin_meter_bridge <-> TCP :56002
[entrypoint] RTU bridges ready
[entrypoint] Starting DERTwin simulator
INFO | Starting Modbus TCP server | 0.0.0.0:55001 | unit=1
INFO | Starting Modbus RTU server | port=/tmp/dertwin_pv | baudrate=9600 | unit=1
INFO | Starting Modbus RTU server | port=/tmp/dertwin_meter | baudrate=9600 | unit=1
```

`generate_compose.py` detects RTU ports in the config and automatically exposes the bridge TCP ports (56001, 56002, ...) alongside the regular Modbus TCP ports in the generated `docker-compose.yml`.

**Step 2** — in a separate terminal on the host, create local PTY endpoints (requires `socat`):

```bash
socat pty,raw,echo=0,link=/tmp/dertwin_pv_client tcp:localhost:56001 &
socat pty,raw,echo=0,link=/tmp/dertwin_meter_client tcp:localhost:56002 &
```

This creates `/tmp/dertwin_pv_client` and `/tmp/dertwin_meter_client` on the host — the same paths the EMS expects.

**Step 3** — run the EMS (same command as the local setup):

```bash
cd examples
python main_mixed.py
```

The EMS uses the exact same config for both local and Docker setups — the serial paths (`/tmp/dertwin_pv_client`, `/tmp/dertwin_meter_client`) are identical. The only difference is how those paths are created: locally via direct socat pairs, or via Docker with TCP bridging.

### Override config at runtime

```bash
docker run \
  -v /path/to/my/configs:/app/configs:ro \
  -e CONFIG_PATH=/app/configs/my_site.json \
  -p 55001:55001 \
  dertwin-simulator
```

---

## 🧪 Tests

```bash
pytest
```

The test suite covers device physics (BESS, PV, CHP, energy meter), register encoding with per-register endianness, FC02 discrete input routing, external models, protocol parity (TCP and RTU), mixed-protocol engine integration, and full end-to-end site integration via Modbus. See `tests/` for structure.

---

## 📈 Roadmap

- [ ] Scenario engine — scripted event sequences
- [ ] REST API + web dashboard
- [ ] IEC 61850 support
- [ ] MQTT integration
- [x] CHP support with realistic state machine
- [x] Modbus FC02 discrete input support
- [x] Per-register endianness (big/little)
- [x] Modbus RTU support
- [x] Mixed-protocol sites (TCP + RTU)
- [x] Docker RTU-over-TCP bridging
- [x] Published PyPI package

---

## 🧠 Use Cases

- EMS algorithm development and validation
- SCADA/HMI integration testing
- Protocol compliance testing (TCP and RTU)
- DER fleet orchestration prototyping
- Frequency and voltage response simulation
- Mixed-protocol site simulation
- CHP dispatch and startup-sequence testing against EMS

---

## 🤝 Contributing

Contributions are welcome. Before diving in, read [`dertwin/README.md`](dertwin/README.md) — it covers the simulator architecture, how devices are modeled, the engine and clock design, and how to add new device types or protocols.

See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines, including how to add new protocols and test RTU without hardware.

---

## 📜 License

MIT License

---

## 👤 Author

Oleksandr Spivak
