dertwin 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dertwin-0.1.0/LICENSE +21 -0
- dertwin-0.1.0/PKG-INFO +274 -0
- dertwin-0.1.0/README.md +224 -0
- dertwin-0.1.0/dertwin/__init__.py +0 -0
- dertwin-0.1.0/dertwin/controllers/__init__.py +0 -0
- dertwin-0.1.0/dertwin/controllers/device_controller.py +77 -0
- dertwin-0.1.0/dertwin/controllers/site_controller.py +206 -0
- dertwin-0.1.0/dertwin/core/__init__.py +0 -0
- dertwin-0.1.0/dertwin/core/clock.py +24 -0
- dertwin-0.1.0/dertwin/core/device.py +23 -0
- dertwin-0.1.0/dertwin/core/engine.py +100 -0
- dertwin-0.1.0/dertwin/core/registers.py +156 -0
- dertwin-0.1.0/dertwin/devices/__init__.py +0 -0
- dertwin-0.1.0/dertwin/devices/bess/__init__.py +0 -0
- dertwin-0.1.0/dertwin/devices/bess/battery.py +238 -0
- dertwin-0.1.0/dertwin/devices/bess/bess.py +58 -0
- dertwin-0.1.0/dertwin/devices/bess/controller.py +155 -0
- dertwin-0.1.0/dertwin/devices/bess/inverter.py +64 -0
- dertwin-0.1.0/dertwin/devices/bess/simulator.py +206 -0
- dertwin-0.1.0/dertwin/devices/energy_meter/__init__.py +0 -0
- dertwin-0.1.0/dertwin/devices/energy_meter/model.py +65 -0
- dertwin-0.1.0/dertwin/devices/energy_meter/simulator.py +66 -0
- dertwin-0.1.0/dertwin/devices/external/__init__.py +0 -0
- dertwin-0.1.0/dertwin/devices/external/ambient_temperature.py +57 -0
- dertwin-0.1.0/dertwin/devices/external/external_models.py +138 -0
- dertwin-0.1.0/dertwin/devices/external/grid_frequency.py +131 -0
- dertwin-0.1.0/dertwin/devices/external/grid_voltage.py +132 -0
- dertwin-0.1.0/dertwin/devices/external/irradiance.py +60 -0
- dertwin-0.1.0/dertwin/devices/external/power_flow.py +63 -0
- dertwin-0.1.0/dertwin/devices/pv/__init__.py +0 -0
- dertwin-0.1.0/dertwin/devices/pv/controller.py +44 -0
- dertwin-0.1.0/dertwin/devices/pv/inverter.py +106 -0
- dertwin-0.1.0/dertwin/devices/pv/panel.py +69 -0
- dertwin-0.1.0/dertwin/devices/pv/pv.py +47 -0
- dertwin-0.1.0/dertwin/devices/pv/simulator.py +143 -0
- dertwin-0.1.0/dertwin/logging_config.py +32 -0
- dertwin-0.1.0/dertwin/main.py +56 -0
- dertwin-0.1.0/dertwin/protocol/__init__.py +0 -0
- dertwin-0.1.0/dertwin/protocol/modbus.py +207 -0
- dertwin-0.1.0/dertwin/telemetry/__init__.py +0 -0
- dertwin-0.1.0/dertwin/telemetry/base.py +14 -0
- dertwin-0.1.0/dertwin/telemetry/bess.py +90 -0
- dertwin-0.1.0/dertwin/telemetry/energy_meter.py +46 -0
- dertwin-0.1.0/dertwin/telemetry/pv.py +36 -0
- dertwin-0.1.0/dertwin.egg-info/PKG-INFO +274 -0
- dertwin-0.1.0/dertwin.egg-info/SOURCES.txt +50 -0
- dertwin-0.1.0/dertwin.egg-info/dependency_links.txt +1 -0
- dertwin-0.1.0/dertwin.egg-info/entry_points.txt +2 -0
- dertwin-0.1.0/dertwin.egg-info/requires.txt +6 -0
- dertwin-0.1.0/dertwin.egg-info/top_level.txt +3 -0
- dertwin-0.1.0/pyproject.toml +47 -0
- dertwin-0.1.0/setup.cfg +4 -0
dertwin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleksandr Spivak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dertwin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dertwin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Digital twin simulator for distributed energy resources โ BESS, PV inverters, energy meters
|
|
5
|
+
Author-email: Oleksandr Spivak <oleksdertwin@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Oleksandr Spivak
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/AlexSpivak/dertwin
|
|
29
|
+
Project-URL: Repository, https://github.com/AlexSpivak/dertwin
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/AlexSpivak/dertwin/issues
|
|
31
|
+
Keywords: modbus,digital-twin,bess,energy,ems,der,simulator
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Scientific/Engineering
|
|
40
|
+
Classifier: Topic :: Software Development :: Testing
|
|
41
|
+
Requires-Python: >=3.11
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: pymodbus<4.0,>=3.5
|
|
45
|
+
Requires-Dist: PyYAML>=6.0
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
48
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# DER Twin
|
|
52
|
+
|
|
53
|
+
Digital Twin infrastructure for modern energy systems.
|
|
54
|
+
|
|
55
|
+
**DER Twin** is a lightweight simulator for Distributed Energy Resources (DER) โ BESS, PV inverters, energy meters, and grid models โ exposed via Modbus TCP. Use it for EMS development, protocol testing, integration validation, and control algorithm sandboxing without touching real hardware.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## โก Quickstart
|
|
60
|
+
|
|
61
|
+
### Option A โ Run locally
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/AlexSpivak/dertwin.git
|
|
65
|
+
cd dertwin
|
|
66
|
+
python -m venv .venv && source .venv/bin/activate
|
|
67
|
+
pip install -e .
|
|
68
|
+
python -m dertwin.main -c configs/simple_config.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You should see:
|
|
72
|
+
```
|
|
73
|
+
2026-03-05 12:00:00 | INFO | dertwin.controllers.site_controller | Building site: local-dev-site
|
|
74
|
+
2026-03-05 12:00:00 | INFO | dertwin.protocol.modbus | Starting Modbus server | 0.0.0.0:55001 | unit=1
|
|
75
|
+
2026-03-05 12:00:00 | INFO | dertwin.core.engine | Simulation engine started | step=0.100s
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The simulator is now accepting Modbus TCP connections on port `55001`.
|
|
79
|
+
|
|
80
|
+
### Option B โ Run with Docker
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/AlexSpivak/dertwin.git
|
|
84
|
+
cd dertwin
|
|
85
|
+
python generate_compose.py configs/simple_config.json
|
|
86
|
+
docker compose up --build
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`generate_compose.py` reads the config and generates a `docker-compose.yml` with the correct ports automatically. No manual port configuration needed.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ๐ Connect an EMS
|
|
94
|
+
|
|
95
|
+
With the simulator running, start the example EMS from a second terminal:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd examples
|
|
99
|
+
python main_simple.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You'll see the EMS connecting over Modbus and cycling the BESS between 40โ60% SOC:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
[EMS] Connected to BESS
|
|
106
|
+
[EMS] Starting in CHARGE mode
|
|
107
|
+
[EMS] STATUS=1 | SOC= 42.30% | P= -20.00 kW | MODE=charge
|
|
108
|
+
[EMS] STATUS=1 | SOC= 44.10% | P= -20.00 kW | MODE=charge
|
|
109
|
+
...
|
|
110
|
+
[EMS] Reached 60% โ switching to DISCHARGE
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For a full multi-device site (dual BESS + PV + energy meter + external models):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python -m dertwin.main -c configs/full_site_config.json
|
|
117
|
+
# in another terminal:
|
|
118
|
+
python examples/main_full.py
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## ๐งฑ Features
|
|
124
|
+
|
|
125
|
+
- Async Modbus TCP server built on `pymodbus`
|
|
126
|
+
- Config-driven site topology โ add devices by editing JSON
|
|
127
|
+
- Irradiance, ambient temperature, grid frequency, and grid voltage models
|
|
128
|
+
- Multi-device support across independent ports
|
|
129
|
+
- External model events (voltage sags, frequency deviations)
|
|
130
|
+
- Simulation start time control (`start_time_h`) โ start at noon, peak load, etc.
|
|
131
|
+
- Docker support with auto-generated Compose files
|
|
132
|
+
- Deterministic simulation with seeded random models
|
|
133
|
+
- Fully tested with `pytest`
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## ๐ฆ Repo Structure
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
dertwin/
|
|
141
|
+
โโโ configs/
|
|
142
|
+
โ โโโ register_maps/ # Modbus register definitions (YAML)
|
|
143
|
+
โ โโโ simple_config.json # Single BESS โ good starting point
|
|
144
|
+
โ โโโ demo_config.json # Full three-device site
|
|
145
|
+
โ โโโ full_site_config.json# Dual BESS + PV + meter + external models
|
|
146
|
+
โโโ dertwin/
|
|
147
|
+
โ โโโ core/ # Clock, engine, register map loader
|
|
148
|
+
โ โโโ controllers/ # Site and device orchestration
|
|
149
|
+
โ โโโ devices/ # BESS, PV, energy meter, external models
|
|
150
|
+
โ โโโ protocol/ # Modbus TCP server
|
|
151
|
+
โ โโโ telemetry/ # Telemetry dataclasses
|
|
152
|
+
โโโ examples/
|
|
153
|
+
โ โโโ simple/ # Single BESS EMS example
|
|
154
|
+
โ โโโ full/ # Multi-device EMS example
|
|
155
|
+
โ โโโ protocol/ # Shared Modbus client
|
|
156
|
+
โโโ tests/ # Full test suite
|
|
157
|
+
โโโ generate_compose.py # Docker Compose generator
|
|
158
|
+
โโโ Dockerfile
|
|
159
|
+
โโโ main.py
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## โ๏ธ Configuration
|
|
165
|
+
|
|
166
|
+
Sites are defined in JSON. Each asset declares its type, parameters, and Modbus protocol binding:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"site_name": "my-site",
|
|
171
|
+
"step": 0.1,
|
|
172
|
+
"real_time": true,
|
|
173
|
+
"start_time_h": 12.0,
|
|
174
|
+
"register_map_root": "configs/register_maps",
|
|
175
|
+
"external_models": {
|
|
176
|
+
"irradiance": { "peak": 1000.0, "sunrise": 6.0, "sunset": 18.0 },
|
|
177
|
+
"grid_frequency": { "nominal_hz": 50.0, "noise_std": 0.002, "seed": 42 }
|
|
178
|
+
},
|
|
179
|
+
"assets": [
|
|
180
|
+
{
|
|
181
|
+
"type": "bess",
|
|
182
|
+
"capacity_kwh": 100.0,
|
|
183
|
+
"initial_soc": 60.0,
|
|
184
|
+
"protocols": [{ "kind": "modbus_tcp", "ip": "0.0.0.0", "port": 55001, "unit_id": 1, "register_map": "bess_modbus.yaml" }]
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**`real_time: true`** โ engine runs its own loop, use for `main.py` and EMS examples
|
|
191
|
+
**`real_time: false`** โ caller drives the clock via `step_once()`, use for tests
|
|
192
|
+
**`start_time_h`** โ sets simulation clock on startup (e.g. `12.0` for noon). All external models start from this time.
|
|
193
|
+
**`ip: "0.0.0.0"`** โ required when running inside Docker so port mapping works. Use `127.0.0.1` for local-only.
|
|
194
|
+
|
|
195
|
+
**Register map fields:**
|
|
196
|
+
|
|
197
|
+
| Field | Required | Description |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `name` | yes | Human-readable label, used in logs and the EMS client |
|
|
200
|
+
| `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)) |
|
|
201
|
+
| `address` | yes | Modbus register address |
|
|
202
|
+
| `type` | yes | `uint16`, `int16`, `uint32`, `int32` |
|
|
203
|
+
| `scale` | yes | Multiplier applied on read, divisor applied on write |
|
|
204
|
+
| `count` | yes | Number of registers (1 for 16-bit, 2 for 32-bit) |
|
|
205
|
+
| `func` | yes | Function code: `0x04` input read, `0x03` holding read, `0x06` single write, `0x10` multi-register write |
|
|
206
|
+
| `direction` | yes | `read` or `write` |
|
|
207
|
+
| `unit` | no | Physical unit label (V, kW, Hz, etc.) |
|
|
208
|
+
| `description` | no | Free-text note |
|
|
209
|
+
| `options` | no | Enum mapping for status/mode registers |
|
|
210
|
+
|
|
211
|
+
`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.
|
|
212
|
+
|
|
213
|
+
For detailed architecture and per-package docs, see [`dertwin/README.md`](dertwin/README.md).
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## ๐ณ Docker
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Generate docker-compose.yml from any config
|
|
221
|
+
python generate_compose.py configs/full_site_config.json
|
|
222
|
+
|
|
223
|
+
# Ports are read automatically from the config โ no manual editing
|
|
224
|
+
docker compose up --build
|
|
225
|
+
|
|
226
|
+
# Override config at runtime without rebuilding
|
|
227
|
+
docker run \
|
|
228
|
+
-v /path/to/my/configs:/app/configs:ro \
|
|
229
|
+
-e CONFIG_PATH=/app/configs/my_site.json \
|
|
230
|
+
-p 55001:55001 \
|
|
231
|
+
dertwin-simulator
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## ๐งช Tests
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pytest
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The test suite covers device physics, register encoding, external models, and full end-to-end site integration via Modbus TCP. See `tests/` for structure.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## ๐ Roadmap
|
|
247
|
+
|
|
248
|
+
- [ ] Scenario engine โ scripted event sequences
|
|
249
|
+
- [ ] REST API + web dashboard
|
|
250
|
+
- [ ] IEC 61850 support
|
|
251
|
+
- [ ] MQTT integration
|
|
252
|
+
- [ ] Published PyPI package
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## ๐ง Use Cases
|
|
257
|
+
|
|
258
|
+
- EMS algorithm development and validation
|
|
259
|
+
- SCADA/HMI integration testing
|
|
260
|
+
- Protocol compliance testing
|
|
261
|
+
- DER fleet orchestration prototyping
|
|
262
|
+
- Frequency and voltage response simulation
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## ๐ License
|
|
267
|
+
|
|
268
|
+
MIT License
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## ๐ค Author
|
|
273
|
+
|
|
274
|
+
Oleksandr Spivak
|
dertwin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# DER Twin
|
|
2
|
+
|
|
3
|
+
Digital Twin infrastructure for modern energy systems.
|
|
4
|
+
|
|
5
|
+
**DER Twin** is a lightweight simulator for Distributed Energy Resources (DER) โ BESS, PV inverters, energy meters, and grid models โ exposed via Modbus TCP. Use it for EMS development, protocol testing, integration validation, and control algorithm sandboxing without touching real hardware.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## โก Quickstart
|
|
10
|
+
|
|
11
|
+
### Option A โ Run locally
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/AlexSpivak/dertwin.git
|
|
15
|
+
cd dertwin
|
|
16
|
+
python -m venv .venv && source .venv/bin/activate
|
|
17
|
+
pip install -e .
|
|
18
|
+
python -m dertwin.main -c configs/simple_config.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You should see:
|
|
22
|
+
```
|
|
23
|
+
2026-03-05 12:00:00 | INFO | dertwin.controllers.site_controller | Building site: local-dev-site
|
|
24
|
+
2026-03-05 12:00:00 | INFO | dertwin.protocol.modbus | Starting Modbus server | 0.0.0.0:55001 | unit=1
|
|
25
|
+
2026-03-05 12:00:00 | INFO | dertwin.core.engine | Simulation engine started | step=0.100s
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The simulator is now accepting Modbus TCP connections on port `55001`.
|
|
29
|
+
|
|
30
|
+
### Option B โ Run with Docker
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/AlexSpivak/dertwin.git
|
|
34
|
+
cd dertwin
|
|
35
|
+
python generate_compose.py configs/simple_config.json
|
|
36
|
+
docker compose up --build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`generate_compose.py` reads the config and generates a `docker-compose.yml` with the correct ports automatically. No manual port configuration needed.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ๐ Connect an EMS
|
|
44
|
+
|
|
45
|
+
With the simulator running, start the example EMS from a second terminal:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd examples
|
|
49
|
+
python main_simple.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You'll see the EMS connecting over Modbus and cycling the BESS between 40โ60% SOC:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
[EMS] Connected to BESS
|
|
56
|
+
[EMS] Starting in CHARGE mode
|
|
57
|
+
[EMS] STATUS=1 | SOC= 42.30% | P= -20.00 kW | MODE=charge
|
|
58
|
+
[EMS] STATUS=1 | SOC= 44.10% | P= -20.00 kW | MODE=charge
|
|
59
|
+
...
|
|
60
|
+
[EMS] Reached 60% โ switching to DISCHARGE
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For a full multi-device site (dual BESS + PV + energy meter + external models):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m dertwin.main -c configs/full_site_config.json
|
|
67
|
+
# in another terminal:
|
|
68
|
+
python examples/main_full.py
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ๐งฑ Features
|
|
74
|
+
|
|
75
|
+
- Async Modbus TCP server built on `pymodbus`
|
|
76
|
+
- Config-driven site topology โ add devices by editing JSON
|
|
77
|
+
- Irradiance, ambient temperature, grid frequency, and grid voltage models
|
|
78
|
+
- Multi-device support across independent ports
|
|
79
|
+
- External model events (voltage sags, frequency deviations)
|
|
80
|
+
- Simulation start time control (`start_time_h`) โ start at noon, peak load, etc.
|
|
81
|
+
- Docker support with auto-generated Compose files
|
|
82
|
+
- Deterministic simulation with seeded random models
|
|
83
|
+
- Fully tested with `pytest`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ๐ฆ Repo Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
dertwin/
|
|
91
|
+
โโโ configs/
|
|
92
|
+
โ โโโ register_maps/ # Modbus register definitions (YAML)
|
|
93
|
+
โ โโโ simple_config.json # Single BESS โ good starting point
|
|
94
|
+
โ โโโ demo_config.json # Full three-device site
|
|
95
|
+
โ โโโ full_site_config.json# Dual BESS + PV + meter + external models
|
|
96
|
+
โโโ dertwin/
|
|
97
|
+
โ โโโ core/ # Clock, engine, register map loader
|
|
98
|
+
โ โโโ controllers/ # Site and device orchestration
|
|
99
|
+
โ โโโ devices/ # BESS, PV, energy meter, external models
|
|
100
|
+
โ โโโ protocol/ # Modbus TCP server
|
|
101
|
+
โ โโโ telemetry/ # Telemetry dataclasses
|
|
102
|
+
โโโ examples/
|
|
103
|
+
โ โโโ simple/ # Single BESS EMS example
|
|
104
|
+
โ โโโ full/ # Multi-device EMS example
|
|
105
|
+
โ โโโ protocol/ # Shared Modbus client
|
|
106
|
+
โโโ tests/ # Full test suite
|
|
107
|
+
โโโ generate_compose.py # Docker Compose generator
|
|
108
|
+
โโโ Dockerfile
|
|
109
|
+
โโโ main.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## โ๏ธ Configuration
|
|
115
|
+
|
|
116
|
+
Sites are defined in JSON. Each asset declares its type, parameters, and Modbus protocol binding:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"site_name": "my-site",
|
|
121
|
+
"step": 0.1,
|
|
122
|
+
"real_time": true,
|
|
123
|
+
"start_time_h": 12.0,
|
|
124
|
+
"register_map_root": "configs/register_maps",
|
|
125
|
+
"external_models": {
|
|
126
|
+
"irradiance": { "peak": 1000.0, "sunrise": 6.0, "sunset": 18.0 },
|
|
127
|
+
"grid_frequency": { "nominal_hz": 50.0, "noise_std": 0.002, "seed": 42 }
|
|
128
|
+
},
|
|
129
|
+
"assets": [
|
|
130
|
+
{
|
|
131
|
+
"type": "bess",
|
|
132
|
+
"capacity_kwh": 100.0,
|
|
133
|
+
"initial_soc": 60.0,
|
|
134
|
+
"protocols": [{ "kind": "modbus_tcp", "ip": "0.0.0.0", "port": 55001, "unit_id": 1, "register_map": "bess_modbus.yaml" }]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`real_time: true`** โ engine runs its own loop, use for `main.py` and EMS examples
|
|
141
|
+
**`real_time: false`** โ caller drives the clock via `step_once()`, use for tests
|
|
142
|
+
**`start_time_h`** โ sets simulation clock on startup (e.g. `12.0` for noon). All external models start from this time.
|
|
143
|
+
**`ip: "0.0.0.0"`** โ required when running inside Docker so port mapping works. Use `127.0.0.1` for local-only.
|
|
144
|
+
|
|
145
|
+
**Register map fields:**
|
|
146
|
+
|
|
147
|
+
| Field | Required | Description |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `name` | yes | Human-readable label, used in logs and the EMS client |
|
|
150
|
+
| `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)) |
|
|
151
|
+
| `address` | yes | Modbus register address |
|
|
152
|
+
| `type` | yes | `uint16`, `int16`, `uint32`, `int32` |
|
|
153
|
+
| `scale` | yes | Multiplier applied on read, divisor applied on write |
|
|
154
|
+
| `count` | yes | Number of registers (1 for 16-bit, 2 for 32-bit) |
|
|
155
|
+
| `func` | yes | Function code: `0x04` input read, `0x03` holding read, `0x06` single write, `0x10` multi-register write |
|
|
156
|
+
| `direction` | yes | `read` or `write` |
|
|
157
|
+
| `unit` | no | Physical unit label (V, kW, Hz, etc.) |
|
|
158
|
+
| `description` | no | Free-text note |
|
|
159
|
+
| `options` | no | Enum mapping for status/mode registers |
|
|
160
|
+
|
|
161
|
+
`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.
|
|
162
|
+
|
|
163
|
+
For detailed architecture and per-package docs, see [`dertwin/README.md`](dertwin/README.md).
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## ๐ณ Docker
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Generate docker-compose.yml from any config
|
|
171
|
+
python generate_compose.py configs/full_site_config.json
|
|
172
|
+
|
|
173
|
+
# Ports are read automatically from the config โ no manual editing
|
|
174
|
+
docker compose up --build
|
|
175
|
+
|
|
176
|
+
# Override config at runtime without rebuilding
|
|
177
|
+
docker run \
|
|
178
|
+
-v /path/to/my/configs:/app/configs:ro \
|
|
179
|
+
-e CONFIG_PATH=/app/configs/my_site.json \
|
|
180
|
+
-p 55001:55001 \
|
|
181
|
+
dertwin-simulator
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ๐งช Tests
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The test suite covers device physics, register encoding, external models, and full end-to-end site integration via Modbus TCP. See `tests/` for structure.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## ๐ Roadmap
|
|
197
|
+
|
|
198
|
+
- [ ] Scenario engine โ scripted event sequences
|
|
199
|
+
- [ ] REST API + web dashboard
|
|
200
|
+
- [ ] IEC 61850 support
|
|
201
|
+
- [ ] MQTT integration
|
|
202
|
+
- [ ] Published PyPI package
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## ๐ง Use Cases
|
|
207
|
+
|
|
208
|
+
- EMS algorithm development and validation
|
|
209
|
+
- SCADA/HMI integration testing
|
|
210
|
+
- Protocol compliance testing
|
|
211
|
+
- DER fleet orchestration prototyping
|
|
212
|
+
- Frequency and voltage response simulation
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## ๐ License
|
|
217
|
+
|
|
218
|
+
MIT License
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## ๐ค Author
|
|
223
|
+
|
|
224
|
+
Oleksandr Spivak
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import List, Dict
|
|
2
|
+
from dertwin.core.device import SimulatedDevice
|
|
3
|
+
from dertwin.core.registers import RegisterMap
|
|
4
|
+
from dertwin.protocol.modbus import (
|
|
5
|
+
collect_write_instructions,
|
|
6
|
+
write_telemetry_registers,
|
|
7
|
+
write_command_registers,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class DeviceController:
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
device: SimulatedDevice,
|
|
15
|
+
protocols: List,
|
|
16
|
+
register_map: RegisterMap,
|
|
17
|
+
):
|
|
18
|
+
self.device = device
|
|
19
|
+
self.protocols = protocols
|
|
20
|
+
self.register_map = register_map
|
|
21
|
+
self._last_commands: Dict[str, float] = {}
|
|
22
|
+
self._initialized = False
|
|
23
|
+
|
|
24
|
+
def step(self, dt: float):
|
|
25
|
+
commands = self.collect_commands()
|
|
26
|
+
|
|
27
|
+
# First run: initialize baseline without applying
|
|
28
|
+
if not self._initialized:
|
|
29
|
+
self._last_commands = dict(commands)
|
|
30
|
+
self.device.init_applied_commands(commands)
|
|
31
|
+
self._initialized = True
|
|
32
|
+
|
|
33
|
+
elif commands and commands != self._last_commands:
|
|
34
|
+
applied = self.device.apply_commands(commands)
|
|
35
|
+
self.write_protocol_commands(applied)
|
|
36
|
+
self._last_commands = dict(commands)
|
|
37
|
+
|
|
38
|
+
self.device.update(dt)
|
|
39
|
+
|
|
40
|
+
telemetry = self.device.get_telemetry().to_dict()
|
|
41
|
+
self.apply_telemetry(telemetry)
|
|
42
|
+
|
|
43
|
+
def collect_commands(self) -> Dict[str, float]:
|
|
44
|
+
merged: Dict[str, float] = {}
|
|
45
|
+
|
|
46
|
+
for proto in self.protocols:
|
|
47
|
+
raw_cmds = collect_write_instructions(
|
|
48
|
+
self.register_map.writes,
|
|
49
|
+
proto.context,
|
|
50
|
+
proto.unit_id,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for vendor_name, value in raw_cmds.items():
|
|
54
|
+
reg = self.register_map.get_by_name(vendor_name)
|
|
55
|
+
|
|
56
|
+
internal_name = reg.internal_name or reg.name
|
|
57
|
+
merged[internal_name] = value
|
|
58
|
+
|
|
59
|
+
return merged
|
|
60
|
+
|
|
61
|
+
def apply_telemetry(self, telemetry: Dict[str, float]) -> None:
|
|
62
|
+
for proto in self.protocols:
|
|
63
|
+
write_telemetry_registers(
|
|
64
|
+
self.register_map.reads,
|
|
65
|
+
proto.context,
|
|
66
|
+
proto.unit_id,
|
|
67
|
+
telemetry,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def write_protocol_commands(self, commands: Dict[str, float]) -> None:
|
|
71
|
+
for proto in self.protocols:
|
|
72
|
+
write_command_registers(
|
|
73
|
+
self.register_map.writes,
|
|
74
|
+
proto.context,
|
|
75
|
+
proto.unit_id,
|
|
76
|
+
commands,
|
|
77
|
+
)
|