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.
Files changed (52) hide show
  1. dertwin-0.1.0/LICENSE +21 -0
  2. dertwin-0.1.0/PKG-INFO +274 -0
  3. dertwin-0.1.0/README.md +224 -0
  4. dertwin-0.1.0/dertwin/__init__.py +0 -0
  5. dertwin-0.1.0/dertwin/controllers/__init__.py +0 -0
  6. dertwin-0.1.0/dertwin/controllers/device_controller.py +77 -0
  7. dertwin-0.1.0/dertwin/controllers/site_controller.py +206 -0
  8. dertwin-0.1.0/dertwin/core/__init__.py +0 -0
  9. dertwin-0.1.0/dertwin/core/clock.py +24 -0
  10. dertwin-0.1.0/dertwin/core/device.py +23 -0
  11. dertwin-0.1.0/dertwin/core/engine.py +100 -0
  12. dertwin-0.1.0/dertwin/core/registers.py +156 -0
  13. dertwin-0.1.0/dertwin/devices/__init__.py +0 -0
  14. dertwin-0.1.0/dertwin/devices/bess/__init__.py +0 -0
  15. dertwin-0.1.0/dertwin/devices/bess/battery.py +238 -0
  16. dertwin-0.1.0/dertwin/devices/bess/bess.py +58 -0
  17. dertwin-0.1.0/dertwin/devices/bess/controller.py +155 -0
  18. dertwin-0.1.0/dertwin/devices/bess/inverter.py +64 -0
  19. dertwin-0.1.0/dertwin/devices/bess/simulator.py +206 -0
  20. dertwin-0.1.0/dertwin/devices/energy_meter/__init__.py +0 -0
  21. dertwin-0.1.0/dertwin/devices/energy_meter/model.py +65 -0
  22. dertwin-0.1.0/dertwin/devices/energy_meter/simulator.py +66 -0
  23. dertwin-0.1.0/dertwin/devices/external/__init__.py +0 -0
  24. dertwin-0.1.0/dertwin/devices/external/ambient_temperature.py +57 -0
  25. dertwin-0.1.0/dertwin/devices/external/external_models.py +138 -0
  26. dertwin-0.1.0/dertwin/devices/external/grid_frequency.py +131 -0
  27. dertwin-0.1.0/dertwin/devices/external/grid_voltage.py +132 -0
  28. dertwin-0.1.0/dertwin/devices/external/irradiance.py +60 -0
  29. dertwin-0.1.0/dertwin/devices/external/power_flow.py +63 -0
  30. dertwin-0.1.0/dertwin/devices/pv/__init__.py +0 -0
  31. dertwin-0.1.0/dertwin/devices/pv/controller.py +44 -0
  32. dertwin-0.1.0/dertwin/devices/pv/inverter.py +106 -0
  33. dertwin-0.1.0/dertwin/devices/pv/panel.py +69 -0
  34. dertwin-0.1.0/dertwin/devices/pv/pv.py +47 -0
  35. dertwin-0.1.0/dertwin/devices/pv/simulator.py +143 -0
  36. dertwin-0.1.0/dertwin/logging_config.py +32 -0
  37. dertwin-0.1.0/dertwin/main.py +56 -0
  38. dertwin-0.1.0/dertwin/protocol/__init__.py +0 -0
  39. dertwin-0.1.0/dertwin/protocol/modbus.py +207 -0
  40. dertwin-0.1.0/dertwin/telemetry/__init__.py +0 -0
  41. dertwin-0.1.0/dertwin/telemetry/base.py +14 -0
  42. dertwin-0.1.0/dertwin/telemetry/bess.py +90 -0
  43. dertwin-0.1.0/dertwin/telemetry/energy_meter.py +46 -0
  44. dertwin-0.1.0/dertwin/telemetry/pv.py +36 -0
  45. dertwin-0.1.0/dertwin.egg-info/PKG-INFO +274 -0
  46. dertwin-0.1.0/dertwin.egg-info/SOURCES.txt +50 -0
  47. dertwin-0.1.0/dertwin.egg-info/dependency_links.txt +1 -0
  48. dertwin-0.1.0/dertwin.egg-info/entry_points.txt +2 -0
  49. dertwin-0.1.0/dertwin.egg-info/requires.txt +6 -0
  50. dertwin-0.1.0/dertwin.egg-info/top_level.txt +3 -0
  51. dertwin-0.1.0/pyproject.toml +47 -0
  52. 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
@@ -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
+ )