opcua-sim 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.
- opcua_sim-0.1.0/CHANGELOG.md +22 -0
- opcua_sim-0.1.0/LICENSE +21 -0
- opcua_sim-0.1.0/MANIFEST.in +5 -0
- opcua_sim-0.1.0/PKG-INFO +326 -0
- opcua_sim-0.1.0/README.md +292 -0
- opcua_sim-0.1.0/examples/plant.yaml +7 -0
- opcua_sim-0.1.0/examples/test_example.py +32 -0
- opcua_sim-0.1.0/pyproject.toml +56 -0
- opcua_sim-0.1.0/setup.cfg +4 -0
- opcua_sim-0.1.0/src/opcua_sim/__init__.py +17 -0
- opcua_sim-0.1.0/src/opcua_sim/__main__.py +3 -0
- opcua_sim-0.1.0/src/opcua_sim/bench.py +424 -0
- opcua_sim-0.1.0/src/opcua_sim/cli.py +390 -0
- opcua_sim-0.1.0/src/opcua_sim/control.py +150 -0
- opcua_sim-0.1.0/src/opcua_sim/device.py +313 -0
- opcua_sim-0.1.0/src/opcua_sim/faults.py +157 -0
- opcua_sim-0.1.0/src/opcua_sim/link.py +109 -0
- opcua_sim-0.1.0/src/opcua_sim/models.py +450 -0
- opcua_sim-0.1.0/src/opcua_sim/profile.py +363 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/beckhoff-twincat3.yaml +68 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/codesys-control.yaml +68 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/drive.yaml +26 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/energy-meter.yaml +24 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/generic-sensor.yaml +16 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/leuze-bcl300.yaml +48 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/omron-nx.yaml +78 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/phoenix-plcnext.yaml +65 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/pressure-transmitter.yaml +33 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/siemens-s7-1500.yaml +68 -0
- opcua_sim-0.1.0/src/opcua_sim/profiles/temperature-controller.yaml +26 -0
- opcua_sim-0.1.0/src/opcua_sim/pytest_plugin.py +93 -0
- opcua_sim-0.1.0/src/opcua_sim/server.py +481 -0
- opcua_sim-0.1.0/src/opcua_sim/signals.py +30 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/PKG-INFO +326 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/SOURCES.txt +48 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/dependency_links.txt +1 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/entry_points.txt +5 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/requires.txt +6 -0
- opcua_sim-0.1.0/src/opcua_sim.egg-info/top_level.txt +1 -0
- opcua_sim-0.1.0/tests/conftest.py +109 -0
- opcua_sim-0.1.0/tests/test_alarms.py +125 -0
- opcua_sim-0.1.0/tests/test_cli.py +133 -0
- opcua_sim-0.1.0/tests/test_faults.py +221 -0
- opcua_sim-0.1.0/tests/test_models.py +151 -0
- opcua_sim-0.1.0/tests/test_packaging.py +65 -0
- opcua_sim-0.1.0/tests/test_plugin.py +54 -0
- opcua_sim-0.1.0/tests/test_products.py +211 -0
- opcua_sim-0.1.0/tests/test_profile.py +112 -0
- opcua_sim-0.1.0/tests/test_security.py +113 -0
- opcua_sim-0.1.0/tests/test_server.py +113 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
First version.
|
|
6
|
+
|
|
7
|
+
- Five bundled profiles: pressure transmitter, temperature controller, drive, energy meter
|
|
8
|
+
and a generic sensor; profiles and benches in YAML
|
|
9
|
+
- Device behaviour: ramps and a latching trip, a PI loop, 4-20 mA with NE 43 currents, a live
|
|
10
|
+
energy counter
|
|
11
|
+
- `DeviceHealth` with the NE 107 states, analog items with `EURange` and `EngineeringUnits`,
|
|
12
|
+
methods with arguments
|
|
13
|
+
- Alarms as OPC UA events: level and discrete alarms, `Acknowledge`, `ConditionRefresh`
|
|
14
|
+
- Runtime faults on devices (status codes, frozen values, old timestamps, drift, refused
|
|
15
|
+
writes and calls, forced health, model faults) and on the server (delay, black hole, dropped
|
|
16
|
+
and refused connections, server state, service level, restart)
|
|
17
|
+
- Security modes None, Sign and SignAndEncrypt (Basic256Sha256), user names with read-only users
|
|
18
|
+
- Command line, JSON control channel, pytest plugin
|
|
19
|
+
- Six profiles that present a simulated device the way a real product does, each naming its
|
|
20
|
+
sources and limits: Siemens S7-1500, Beckhoff TwinCAT 3, Phoenix Contact PLCnext, CODESYS,
|
|
21
|
+
Omron NJ/NX, Leuze BCL 300
|
|
22
|
+
- Browser demo in `web/`: the device code in Pyodide, without the wire protocol
|
opcua_sim-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ralf Steinert
|
|
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.
|
opcua_sim-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opcua-sim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simulated OPC UA devices with behaviour, NE 107 health, real alarms and fault injection, for testing SCADA, HMI and gateway code
|
|
5
|
+
Author: Ralf Steinert
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/steitec/opcua-sim
|
|
8
|
+
Project-URL: Repository, https://github.com/steitec/opcua-sim
|
|
9
|
+
Project-URL: Changelog, https://github.com/steitec/opcua-sim/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/steitec/opcua-sim/issues
|
|
11
|
+
Keywords: opc-ua,opcua,simulator,scada,hmi,industrial automation,ne107,test automation,pytest
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Manufacturing
|
|
15
|
+
Classifier: Framework :: Pytest
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Topic :: System :: Hardware
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: asyncua<3,>=2.1
|
|
29
|
+
Requires-Dist: pyyaml>=6
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<a href="https://steitec.net"><picture>
|
|
36
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-dark.svg">
|
|
37
|
+
<img align="right" height="32" alt="steitec" src="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-light.svg">
|
|
38
|
+
</picture></a>
|
|
39
|
+
|
|
40
|
+
# opcua-sim
|
|
41
|
+
|
|
42
|
+
[](https://github.com/steitec/opcua-sim/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/opcua-sim/)
|
|
44
|
+
[](https://pypi.org/project/opcua-sim/)
|
|
45
|
+
[](https://github.com/steitec/opcua-sim/blob/main/LICENSE)
|
|
46
|
+
|
|
47
|
+
Simulated OPC UA devices that **behave and fail**: a drive that ramps up after `Start` and
|
|
48
|
+
latches a trip until `Reset`, a transmitter whose broken sensor reads `BadSensorFailure` with
|
|
49
|
+
health `FAILURE` and 3.6 mA on the loop, alarms that arrive as real OPC UA events and wait for
|
|
50
|
+
your `Acknowledge`. Develop and test SCADA links, HMIs, gateways and data loggers without the
|
|
51
|
+
plant, also in CI.
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
$ opcua-sim serve --bench examples/plant.yaml &
|
|
55
|
+
opcua-sim: 4 device(s) on opc.tcp://127.0.0.1:4840, control on :4841
|
|
56
|
+
oven (temperature-controller)
|
|
57
|
+
pump (drive)
|
|
58
|
+
tank_pressure (pressure-transmitter)
|
|
59
|
+
meter (energy-meter)
|
|
60
|
+
$ opcua-sim write pump.SpeedSetpoint 30
|
|
61
|
+
pump.SpeedSetpoint: written 30.0
|
|
62
|
+
$ opcua-sim call pump.Start
|
|
63
|
+
pump.Start: ok
|
|
64
|
+
$ opcua-sim inject pump trip code=4
|
|
65
|
+
$ opcua-sim read pump.State pump.FaultCode pump.DeviceHealth
|
|
66
|
+
pump.State: 3 [Good, 2026-11-02T18:13:22+00:00]
|
|
67
|
+
pump.FaultCode: 4 [Good, 2026-11-02T18:13:22+00:00]
|
|
68
|
+
pump.DeviceHealth: 1 [Good, 2026-11-02T18:13:22+00:00]
|
|
69
|
+
$ opcua-sim call pump.Start
|
|
70
|
+
opcua-sim: The operation cannot be completed because the object is closed, uninitialized or in some other invalid state.(BadInvalidState)
|
|
71
|
+
$ opcua-sim inject tank_pressure sensor_break
|
|
72
|
+
$ opcua-sim read tank_pressure.Pressure tank_pressure.OutputCurrent
|
|
73
|
+
tank_pressure.Pressure: null [BadSensorFailure, 2026-11-02T18:13:24+00:00]
|
|
74
|
+
tank_pressure.OutputCurrent: 3.6 [Good, 2026-11-02T18:13:24+00:00]
|
|
75
|
+
$ opcua-sim inject server delay seconds=5
|
|
76
|
+
$ opcua-sim read pump.Speed --timeout 1
|
|
77
|
+
opcua-sim: no answer in time
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Why
|
|
81
|
+
|
|
82
|
+
OPC UA demo servers are good at showing changing numbers. Client code breaks on other things:
|
|
83
|
+
|
|
84
|
+
- **quality**: a value that is `Bad` has no number at all, an `Uncertain` one has a number you
|
|
85
|
+
may not trust, a frozen one has a source timestamp that stopped moving
|
|
86
|
+
- **device state**: methods refused in the wrong state, a latched trip, a health state that
|
|
87
|
+
changes while the values still look fine
|
|
88
|
+
- **alarms**: conditions that must be acknowledged with the right `EventId` and have to be
|
|
89
|
+
fetched again with `ConditionRefresh` after a reconnect
|
|
90
|
+
- **the connection**: late answers, a link that stays open and carries nothing, a server restart
|
|
91
|
+
that takes every session and subscription with it
|
|
92
|
+
|
|
93
|
+
opcua-sim gives you all of that from a YAML profile, switchable at runtime. The OPC UA stack
|
|
94
|
+
underneath is [asyncua](https://github.com/FreeOpcUa/opcua-asyncio).
|
|
95
|
+
|
|
96
|
+
## What it is not
|
|
97
|
+
|
|
98
|
+
The generic profiles use the names of OPC UA DI (`DeviceSet`, `ParameterSet`, `MethodSet`,
|
|
99
|
+
`DeviceHealth`), but live in their own namespace and do not implement the DI companion
|
|
100
|
+
specification: a client that looks for the DI types will not find them.
|
|
101
|
+
|
|
102
|
+
The [product profiles](#product-profiles) copy how a real server presents its nodes, as far as
|
|
103
|
+
public sources describe it. None of them is a complete copy of a product, and none has been
|
|
104
|
+
compared with real hardware.
|
|
105
|
+
|
|
106
|
+
It is checked with asyncua and node-opcua clients. It has not been tested against UaExpert,
|
|
107
|
+
commercial SCADA systems or a conformance test tool. Please
|
|
108
|
+
[open an issue](https://github.com/steitec/opcua-sim/issues) if your client disagrees with it.
|
|
109
|
+
|
|
110
|
+
## Install
|
|
111
|
+
|
|
112
|
+
Requires Python 3.10 or newer.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install opcua-sim
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
From source, with the test dependencies:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/steitec/opcua-sim
|
|
122
|
+
cd opcua-sim
|
|
123
|
+
pip install -e ".[dev]"
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Profiles
|
|
128
|
+
|
|
129
|
+
| Profile | Device | Faults of its own |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `pressure-transmitter` | 4-20 mA with NE 43 currents, `ZeroTrim`, `StartSelfTest`, level alarm | `sensor_break`, `over_range`, `maintenance_due` |
|
|
132
|
+
| `temperature-controller` | PI loop on a slow plant, setpoint, manual mode, `Start`, `Stop` | `sensor_break`, `heater_failure` |
|
|
133
|
+
| `drive` | frequency converter with ramps, `Start`, `Stop`, `Reset`, latching trip | `trip`, `overload`, `fan_worn` |
|
|
134
|
+
| `energy-meter` | three phases, power, an energy counter that counts, `ResetEnergy` | `phase_loss` |
|
|
135
|
+
| `generic-sensor` | signal sources, no behaviour: a starting point for your own | |
|
|
136
|
+
|
|
137
|
+
`opcua-sim map drive` shows the address space of a profile. Nodes are addressed by their string
|
|
138
|
+
ID in the namespace `urn:steitec:opcua-sim`, for example `ns=2;s=pump.Speed`:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
Objects/DeviceSet/pump
|
|
142
|
+
Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision
|
|
143
|
+
DeviceHealth NORMAL, FAILURE, CHECK_FUNCTION, OFF_SPEC, MAINTENANCE_REQUIRED
|
|
144
|
+
ParameterSet/ the variables; with a unit or range they are AnalogItemType
|
|
145
|
+
MethodSet/ the methods
|
|
146
|
+
Alarms/ one node per alarm: the ConditionId of its events
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A bench puts several devices behind one server:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
name: demo plant
|
|
153
|
+
security: [none, encrypt] # none, sign, encrypt; self-signed certificate
|
|
154
|
+
users: # optional; without users everybody may write
|
|
155
|
+
anna: {password: s3cret} # role operator
|
|
156
|
+
vic: {password: look, role: viewer}
|
|
157
|
+
anonymous: true # anonymous clients may read
|
|
158
|
+
tick: 0.1 # simulation step in seconds
|
|
159
|
+
seed: 0 # noise is repeatable
|
|
160
|
+
devices:
|
|
161
|
+
- {name: oven, profile: temperature-controller}
|
|
162
|
+
- {name: pump, profile: drive, params: {ramp: 5.0}}
|
|
163
|
+
- {name: mine, profile: my-device.yaml, serial_number: X-42} # relative to this file
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A profile of your own needs no Python. Without `model`, values come from signal sources
|
|
167
|
+
(`constant`, `sine`, `ramp`, `square`, `noise`), from `value`, or from what a client wrote:
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
name: flow-sensor
|
|
171
|
+
identification: {manufacturer: ACME, model: F-100}
|
|
172
|
+
variables:
|
|
173
|
+
- {name: Flow, unit: l/min, range: [0, 200], source: {kind: sine, offset: 100, amplitude: 20, period: 30}}
|
|
174
|
+
- {name: Enabled, type: Boolean, value: true, writable: true}
|
|
175
|
+
- {name: Limit, unit: l/min, value: 150, writable: true, min: 0, max: 200}
|
|
176
|
+
alarms:
|
|
177
|
+
- {name: FlowLevel, kind: level, variable: Flow, high: 115, low: 85, deadband: 1,
|
|
178
|
+
message: "Flow out of limits"}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Level alarms know `high_high`, `high`, `low` and `low_low`, by default with severity 900 for the
|
|
182
|
+
outer and 700 for the inner limits; `high: {value: 115, severity: 800}` changes that. Discrete
|
|
183
|
+
alarms (`kind: discrete`, `when: <flag>`) follow a flag of a device model. A value without a
|
|
184
|
+
valid status does not change the state of its level alarm.
|
|
185
|
+
|
|
186
|
+
## Product profiles
|
|
187
|
+
|
|
188
|
+
These profiles make the simulated server look like a real product to your client: same namespace
|
|
189
|
+
URIs and indices, same form of node IDs, same data types. A PLC server publishes whatever program
|
|
190
|
+
runs on it, so the variables are one small example program (run and reset bits, a ramp, a part
|
|
191
|
+
counter, a latching fault), presented the way each server would. That is what tests the parts
|
|
192
|
+
that break when you move between brands: quotes in Siemens IDs, the `|var|` of CODESYS, a namespace
|
|
193
|
+
index that differs per server.
|
|
194
|
+
|
|
195
|
+
| Profile | Example node ID | Based on | Assumed or missing |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| `siemens-s7-1500` | `ns=3;s="DB_Line"."Speed"` | Siemens application document 109737901 for the ID form, third-party guides for the URI and folders | folders and URI not confirmed by Siemens |
|
|
198
|
+
| `beckhoff-twincat3` | `ns=4;s=MAIN.fSpeed` | Beckhoff Information System for the URI, a Cybus guide for the IDs | real defaults are stricter: encrypted only, no anonymous access |
|
|
199
|
+
| `phoenix-plcnext` | `ns=2;s=Arp.Plc.Eclr/MainInstance.rSpeed` | PLCnext documentation for the URI and ID form | namespace index, folders |
|
|
200
|
+
| `omron-nx` | `ns=2;s=new_Controller_0.GlobalVars.Speed` | Omron manual W588 for the tree, types and status nodes | manual gives no URI or ID strings: **placeholders**, so browse |
|
|
201
|
+
| `codesys-control` | `ns=4;s=\|var\|CODESYS Control for Linux SL.Application.PLC_PRG.rSpeed` | forum reports only | URI is a **placeholder**, folders; the weakest of the six |
|
|
202
|
+
| `leuze-bcl300` | `ns=4;i=6205` | Leuze technical description 50141930 | 19 of the 136 listed nodes; no `ScanStart`, `Scan` |
|
|
203
|
+
|
|
204
|
+
`leuze-bcl300` is the one real device in the list: a bar code reader that scans by itself, with
|
|
205
|
+
node IDs, browse names and types from the manufacturer's table. Rockwell, Mitsubishi and gateways
|
|
206
|
+
such as KEPServerEX are not included, because the sources found were not good enough to copy them.
|
|
207
|
+
|
|
208
|
+
`opcua-sim map <profile>` prints every node ID with the sources and limits of that profile. These
|
|
209
|
+
profiles have no alarms and no `DeviceHealth` node, and a product profile must be the only device
|
|
210
|
+
of its bench, because it is a whole server. Their own faults are `trip` and `plc_stop` on the PLCs
|
|
211
|
+
and `read_fail` and `device_error` on the reader; every other fault works as usual.
|
|
212
|
+
|
|
213
|
+
```console
|
|
214
|
+
$ opcua-sim serve --profile siemens-s7-1500 --name plc &
|
|
215
|
+
$ opcua-sim write 'ns=3;s="DB_Line"."Run"' true
|
|
216
|
+
$ opcua-sim inject plc trip code=12
|
|
217
|
+
$ opcua-sim read 'ns=3;s="DB_Line"."Fault"' 'ns=3;s="DB_Line"."FaultCode"'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Methods need their object: `opcua-sim call 'ns=4;i=7002' --object 'ns=4;i=5001'`.
|
|
221
|
+
|
|
222
|
+
## Faults
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
opcua-sim inject <device> <kind> [name=value ...]
|
|
226
|
+
opcua-sim inject server <kind> [name=value ...]
|
|
227
|
+
opcua-sim clear <device or server> [kind]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**On every device:**
|
|
231
|
+
|
|
232
|
+
| Kind | Parameters | Effect |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `bad_status` | `variable`, `status=BadCommunicationError` | the variable (or all) carries this status code; `Bad*` reads as null, `Uncertain*` keeps its value |
|
|
235
|
+
| `freeze` | `variable` | values stop changing, source timestamps stay old |
|
|
236
|
+
| `stale` | `seconds=60` | values go on, source timestamps lag behind |
|
|
237
|
+
| `drift` | `variable`, `per_second` | a numeric value runs away |
|
|
238
|
+
| `reject_write` | `variable`, `status=BadUserAccessDenied` | writes answer this status code |
|
|
239
|
+
| `method_fail` | `method`, `status=BadInvalidState` | calls answer this status code |
|
|
240
|
+
| `health` | `state` | forces `DeviceHealth` |
|
|
241
|
+
|
|
242
|
+
**On the server:**
|
|
243
|
+
|
|
244
|
+
| Kind | Parameters | Effect |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `delay` | `seconds` | every response arrives late |
|
|
247
|
+
| `blackhole` | | connections stay open, nothing gets through |
|
|
248
|
+
| `drop_connections` | | all connections are cut now |
|
|
249
|
+
| `refuse` | | new connections are closed at once |
|
|
250
|
+
| `server_state` | `state` | `ServerStatus/State`: `Suspended`, `Test`, `Failed`, ... |
|
|
251
|
+
| `service_level` | `value` | `Server/ServiceLevel`, 0-255 |
|
|
252
|
+
| `restart` | `downtime=2` | the server goes away and returns on the same port without sessions or subscriptions; device state stays; building the server adds about a second |
|
|
253
|
+
|
|
254
|
+
Network faults sit in a small TCP forwarder in front of the server. `blackhole` holds the data
|
|
255
|
+
back rather than losing it, so a client that has not given up yet receives it after `clear`.
|
|
256
|
+
|
|
257
|
+
## Alarms
|
|
258
|
+
|
|
259
|
+
Alarms arrive as `ExclusiveLevelAlarmType` and `OffNormalAlarmType` events, from the device node
|
|
260
|
+
and from the `Server` object, with the usual fields: `ConditionId`, `ConditionName`, `Severity`,
|
|
261
|
+
`ActiveState/Id`, `AckedState/Id`, `Retain`, `Message`, and for level alarms the limits and
|
|
262
|
+
`LimitState/CurrentState`.
|
|
263
|
+
|
|
264
|
+
`Acknowledge` (`i=9111`, called on the `ConditionId`) checks the `EventId` and answers
|
|
265
|
+
`BadEventIdUnknown` or `BadConditionBranchAlreadyAcked`. `ConditionRefresh` (`i=3875`) sends
|
|
266
|
+
`RefreshStartEvent`, every retained condition, then `RefreshEndEvent`.
|
|
267
|
+
|
|
268
|
+
Alarm conditions are events, not full condition instances: `Enable`, `Disable`, `AddComment`,
|
|
269
|
+
`Confirm`, shelving and branches are not implemented, and the `Alarms/` nodes have no child
|
|
270
|
+
variables to browse.
|
|
271
|
+
|
|
272
|
+
## In pytest
|
|
273
|
+
|
|
274
|
+
Installing the package adds two fixtures. Every test gets a new bench on a free port:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
def test_notices_a_device_failure(opcua_bench):
|
|
278
|
+
opcua_bench.inject("drive", "bad_status", variable="Speed", status="BadDeviceFailure")
|
|
279
|
+
with pytest.raises(ua.uaerrors.BadDeviceFailure):
|
|
280
|
+
read_speed(opcua_bench.url) # your code
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_two_servers(opcua_bench_factory):
|
|
284
|
+
plant = opcua_bench_factory("plant.yaml") # bench file, dict or profile name
|
|
285
|
+
meter = opcua_bench_factory("energy-meter")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`--opcua-bench FILE` or the ini key `opcua_bench` choose the bench of `opcua_bench`; without them
|
|
289
|
+
it is one `drive`. See [`examples/test_example.py`](examples/test_example.py). The bench API is
|
|
290
|
+
`url`, `port`, `inject`, `clear`, `set`, `get`, `status`, `wait_restarted`.
|
|
291
|
+
|
|
292
|
+
Starting a server takes about a second, because asyncua loads the whole standard address space.
|
|
293
|
+
|
|
294
|
+
## Command line
|
|
295
|
+
|
|
296
|
+
| Command | |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `serve --bench FILE` or `--profile NAME` | run; `--host`, `--port`, `--endpoint-host`, `--security` |
|
|
299
|
+
| `profiles`, `map PROFILE` | what is there |
|
|
300
|
+
| `browse`, `read`, `write`, `call`, `watch` | a small OPC UA client; `--url`, `--user`, `--password`, `--timeout` |
|
|
301
|
+
| `inject`, `clear`, `set`, `get`, `list` | the control channel of a running bench; `--control-port` |
|
|
302
|
+
|
|
303
|
+
`set` forces a value from outside (`null` hands it back to the simulation), `watch --alarms` prints
|
|
304
|
+
alarm events. The control channel is one JSON object per line on port 4841, described in
|
|
305
|
+
[`control.py`](src/opcua_sim/control.py).
|
|
306
|
+
|
|
307
|
+
## Security
|
|
308
|
+
|
|
309
|
+
Do not put this on a network you do not trust. It is a test tool:
|
|
310
|
+
|
|
311
|
+
- By default it binds to loopback. With `--host 0.0.0.0` the control channel, which has no
|
|
312
|
+
authentication, is reachable too
|
|
313
|
+
- Passwords are plain text in the bench file, and on an endpoint without encryption they can
|
|
314
|
+
cross the wire in plain text
|
|
315
|
+
- Certificates are self-signed and created at every start; client certificates are not checked
|
|
316
|
+
|
|
317
|
+
## Browser demo
|
|
318
|
+
|
|
319
|
+
[`web/`](web/) holds a static demo page. It runs the real device code in the browser with
|
|
320
|
+
[Pyodide](https://pyodide.org), but not the OPC UA wire protocol, because a browser has no TCP
|
|
321
|
+
sockets. To test a client, install opcua-sim.
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
[MIT](LICENSE). The OPC UA stack [asyncua](https://github.com/FreeOpcUa/opcua-asyncio) is LGPL-3.0
|
|
326
|
+
and is installed as a separate package.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<a href="https://steitec.net"><picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-dark.svg">
|
|
3
|
+
<img align="right" height="32" alt="steitec" src="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-light.svg">
|
|
4
|
+
</picture></a>
|
|
5
|
+
|
|
6
|
+
# opcua-sim
|
|
7
|
+
|
|
8
|
+
[](https://github.com/steitec/opcua-sim/actions/workflows/ci.yml)
|
|
9
|
+
[](https://pypi.org/project/opcua-sim/)
|
|
10
|
+
[](https://pypi.org/project/opcua-sim/)
|
|
11
|
+
[](https://github.com/steitec/opcua-sim/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
Simulated OPC UA devices that **behave and fail**: a drive that ramps up after `Start` and
|
|
14
|
+
latches a trip until `Reset`, a transmitter whose broken sensor reads `BadSensorFailure` with
|
|
15
|
+
health `FAILURE` and 3.6 mA on the loop, alarms that arrive as real OPC UA events and wait for
|
|
16
|
+
your `Acknowledge`. Develop and test SCADA links, HMIs, gateways and data loggers without the
|
|
17
|
+
plant, also in CI.
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
$ opcua-sim serve --bench examples/plant.yaml &
|
|
21
|
+
opcua-sim: 4 device(s) on opc.tcp://127.0.0.1:4840, control on :4841
|
|
22
|
+
oven (temperature-controller)
|
|
23
|
+
pump (drive)
|
|
24
|
+
tank_pressure (pressure-transmitter)
|
|
25
|
+
meter (energy-meter)
|
|
26
|
+
$ opcua-sim write pump.SpeedSetpoint 30
|
|
27
|
+
pump.SpeedSetpoint: written 30.0
|
|
28
|
+
$ opcua-sim call pump.Start
|
|
29
|
+
pump.Start: ok
|
|
30
|
+
$ opcua-sim inject pump trip code=4
|
|
31
|
+
$ opcua-sim read pump.State pump.FaultCode pump.DeviceHealth
|
|
32
|
+
pump.State: 3 [Good, 2026-11-02T18:13:22+00:00]
|
|
33
|
+
pump.FaultCode: 4 [Good, 2026-11-02T18:13:22+00:00]
|
|
34
|
+
pump.DeviceHealth: 1 [Good, 2026-11-02T18:13:22+00:00]
|
|
35
|
+
$ opcua-sim call pump.Start
|
|
36
|
+
opcua-sim: The operation cannot be completed because the object is closed, uninitialized or in some other invalid state.(BadInvalidState)
|
|
37
|
+
$ opcua-sim inject tank_pressure sensor_break
|
|
38
|
+
$ opcua-sim read tank_pressure.Pressure tank_pressure.OutputCurrent
|
|
39
|
+
tank_pressure.Pressure: null [BadSensorFailure, 2026-11-02T18:13:24+00:00]
|
|
40
|
+
tank_pressure.OutputCurrent: 3.6 [Good, 2026-11-02T18:13:24+00:00]
|
|
41
|
+
$ opcua-sim inject server delay seconds=5
|
|
42
|
+
$ opcua-sim read pump.Speed --timeout 1
|
|
43
|
+
opcua-sim: no answer in time
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Why
|
|
47
|
+
|
|
48
|
+
OPC UA demo servers are good at showing changing numbers. Client code breaks on other things:
|
|
49
|
+
|
|
50
|
+
- **quality**: a value that is `Bad` has no number at all, an `Uncertain` one has a number you
|
|
51
|
+
may not trust, a frozen one has a source timestamp that stopped moving
|
|
52
|
+
- **device state**: methods refused in the wrong state, a latched trip, a health state that
|
|
53
|
+
changes while the values still look fine
|
|
54
|
+
- **alarms**: conditions that must be acknowledged with the right `EventId` and have to be
|
|
55
|
+
fetched again with `ConditionRefresh` after a reconnect
|
|
56
|
+
- **the connection**: late answers, a link that stays open and carries nothing, a server restart
|
|
57
|
+
that takes every session and subscription with it
|
|
58
|
+
|
|
59
|
+
opcua-sim gives you all of that from a YAML profile, switchable at runtime. The OPC UA stack
|
|
60
|
+
underneath is [asyncua](https://github.com/FreeOpcUa/opcua-asyncio).
|
|
61
|
+
|
|
62
|
+
## What it is not
|
|
63
|
+
|
|
64
|
+
The generic profiles use the names of OPC UA DI (`DeviceSet`, `ParameterSet`, `MethodSet`,
|
|
65
|
+
`DeviceHealth`), but live in their own namespace and do not implement the DI companion
|
|
66
|
+
specification: a client that looks for the DI types will not find them.
|
|
67
|
+
|
|
68
|
+
The [product profiles](#product-profiles) copy how a real server presents its nodes, as far as
|
|
69
|
+
public sources describe it. None of them is a complete copy of a product, and none has been
|
|
70
|
+
compared with real hardware.
|
|
71
|
+
|
|
72
|
+
It is checked with asyncua and node-opcua clients. It has not been tested against UaExpert,
|
|
73
|
+
commercial SCADA systems or a conformance test tool. Please
|
|
74
|
+
[open an issue](https://github.com/steitec/opcua-sim/issues) if your client disagrees with it.
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
Requires Python 3.10 or newer.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install opcua-sim
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
From source, with the test dependencies:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/steitec/opcua-sim
|
|
88
|
+
cd opcua-sim
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Profiles
|
|
94
|
+
|
|
95
|
+
| Profile | Device | Faults of its own |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `pressure-transmitter` | 4-20 mA with NE 43 currents, `ZeroTrim`, `StartSelfTest`, level alarm | `sensor_break`, `over_range`, `maintenance_due` |
|
|
98
|
+
| `temperature-controller` | PI loop on a slow plant, setpoint, manual mode, `Start`, `Stop` | `sensor_break`, `heater_failure` |
|
|
99
|
+
| `drive` | frequency converter with ramps, `Start`, `Stop`, `Reset`, latching trip | `trip`, `overload`, `fan_worn` |
|
|
100
|
+
| `energy-meter` | three phases, power, an energy counter that counts, `ResetEnergy` | `phase_loss` |
|
|
101
|
+
| `generic-sensor` | signal sources, no behaviour: a starting point for your own | |
|
|
102
|
+
|
|
103
|
+
`opcua-sim map drive` shows the address space of a profile. Nodes are addressed by their string
|
|
104
|
+
ID in the namespace `urn:steitec:opcua-sim`, for example `ns=2;s=pump.Speed`:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Objects/DeviceSet/pump
|
|
108
|
+
Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision
|
|
109
|
+
DeviceHealth NORMAL, FAILURE, CHECK_FUNCTION, OFF_SPEC, MAINTENANCE_REQUIRED
|
|
110
|
+
ParameterSet/ the variables; with a unit or range they are AnalogItemType
|
|
111
|
+
MethodSet/ the methods
|
|
112
|
+
Alarms/ one node per alarm: the ConditionId of its events
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A bench puts several devices behind one server:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
name: demo plant
|
|
119
|
+
security: [none, encrypt] # none, sign, encrypt; self-signed certificate
|
|
120
|
+
users: # optional; without users everybody may write
|
|
121
|
+
anna: {password: s3cret} # role operator
|
|
122
|
+
vic: {password: look, role: viewer}
|
|
123
|
+
anonymous: true # anonymous clients may read
|
|
124
|
+
tick: 0.1 # simulation step in seconds
|
|
125
|
+
seed: 0 # noise is repeatable
|
|
126
|
+
devices:
|
|
127
|
+
- {name: oven, profile: temperature-controller}
|
|
128
|
+
- {name: pump, profile: drive, params: {ramp: 5.0}}
|
|
129
|
+
- {name: mine, profile: my-device.yaml, serial_number: X-42} # relative to this file
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A profile of your own needs no Python. Without `model`, values come from signal sources
|
|
133
|
+
(`constant`, `sine`, `ramp`, `square`, `noise`), from `value`, or from what a client wrote:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
name: flow-sensor
|
|
137
|
+
identification: {manufacturer: ACME, model: F-100}
|
|
138
|
+
variables:
|
|
139
|
+
- {name: Flow, unit: l/min, range: [0, 200], source: {kind: sine, offset: 100, amplitude: 20, period: 30}}
|
|
140
|
+
- {name: Enabled, type: Boolean, value: true, writable: true}
|
|
141
|
+
- {name: Limit, unit: l/min, value: 150, writable: true, min: 0, max: 200}
|
|
142
|
+
alarms:
|
|
143
|
+
- {name: FlowLevel, kind: level, variable: Flow, high: 115, low: 85, deadband: 1,
|
|
144
|
+
message: "Flow out of limits"}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Level alarms know `high_high`, `high`, `low` and `low_low`, by default with severity 900 for the
|
|
148
|
+
outer and 700 for the inner limits; `high: {value: 115, severity: 800}` changes that. Discrete
|
|
149
|
+
alarms (`kind: discrete`, `when: <flag>`) follow a flag of a device model. A value without a
|
|
150
|
+
valid status does not change the state of its level alarm.
|
|
151
|
+
|
|
152
|
+
## Product profiles
|
|
153
|
+
|
|
154
|
+
These profiles make the simulated server look like a real product to your client: same namespace
|
|
155
|
+
URIs and indices, same form of node IDs, same data types. A PLC server publishes whatever program
|
|
156
|
+
runs on it, so the variables are one small example program (run and reset bits, a ramp, a part
|
|
157
|
+
counter, a latching fault), presented the way each server would. That is what tests the parts
|
|
158
|
+
that break when you move between brands: quotes in Siemens IDs, the `|var|` of CODESYS, a namespace
|
|
159
|
+
index that differs per server.
|
|
160
|
+
|
|
161
|
+
| Profile | Example node ID | Based on | Assumed or missing |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| `siemens-s7-1500` | `ns=3;s="DB_Line"."Speed"` | Siemens application document 109737901 for the ID form, third-party guides for the URI and folders | folders and URI not confirmed by Siemens |
|
|
164
|
+
| `beckhoff-twincat3` | `ns=4;s=MAIN.fSpeed` | Beckhoff Information System for the URI, a Cybus guide for the IDs | real defaults are stricter: encrypted only, no anonymous access |
|
|
165
|
+
| `phoenix-plcnext` | `ns=2;s=Arp.Plc.Eclr/MainInstance.rSpeed` | PLCnext documentation for the URI and ID form | namespace index, folders |
|
|
166
|
+
| `omron-nx` | `ns=2;s=new_Controller_0.GlobalVars.Speed` | Omron manual W588 for the tree, types and status nodes | manual gives no URI or ID strings: **placeholders**, so browse |
|
|
167
|
+
| `codesys-control` | `ns=4;s=\|var\|CODESYS Control for Linux SL.Application.PLC_PRG.rSpeed` | forum reports only | URI is a **placeholder**, folders; the weakest of the six |
|
|
168
|
+
| `leuze-bcl300` | `ns=4;i=6205` | Leuze technical description 50141930 | 19 of the 136 listed nodes; no `ScanStart`, `Scan` |
|
|
169
|
+
|
|
170
|
+
`leuze-bcl300` is the one real device in the list: a bar code reader that scans by itself, with
|
|
171
|
+
node IDs, browse names and types from the manufacturer's table. Rockwell, Mitsubishi and gateways
|
|
172
|
+
such as KEPServerEX are not included, because the sources found were not good enough to copy them.
|
|
173
|
+
|
|
174
|
+
`opcua-sim map <profile>` prints every node ID with the sources and limits of that profile. These
|
|
175
|
+
profiles have no alarms and no `DeviceHealth` node, and a product profile must be the only device
|
|
176
|
+
of its bench, because it is a whole server. Their own faults are `trip` and `plc_stop` on the PLCs
|
|
177
|
+
and `read_fail` and `device_error` on the reader; every other fault works as usual.
|
|
178
|
+
|
|
179
|
+
```console
|
|
180
|
+
$ opcua-sim serve --profile siemens-s7-1500 --name plc &
|
|
181
|
+
$ opcua-sim write 'ns=3;s="DB_Line"."Run"' true
|
|
182
|
+
$ opcua-sim inject plc trip code=12
|
|
183
|
+
$ opcua-sim read 'ns=3;s="DB_Line"."Fault"' 'ns=3;s="DB_Line"."FaultCode"'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Methods need their object: `opcua-sim call 'ns=4;i=7002' --object 'ns=4;i=5001'`.
|
|
187
|
+
|
|
188
|
+
## Faults
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
opcua-sim inject <device> <kind> [name=value ...]
|
|
192
|
+
opcua-sim inject server <kind> [name=value ...]
|
|
193
|
+
opcua-sim clear <device or server> [kind]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**On every device:**
|
|
197
|
+
|
|
198
|
+
| Kind | Parameters | Effect |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `bad_status` | `variable`, `status=BadCommunicationError` | the variable (or all) carries this status code; `Bad*` reads as null, `Uncertain*` keeps its value |
|
|
201
|
+
| `freeze` | `variable` | values stop changing, source timestamps stay old |
|
|
202
|
+
| `stale` | `seconds=60` | values go on, source timestamps lag behind |
|
|
203
|
+
| `drift` | `variable`, `per_second` | a numeric value runs away |
|
|
204
|
+
| `reject_write` | `variable`, `status=BadUserAccessDenied` | writes answer this status code |
|
|
205
|
+
| `method_fail` | `method`, `status=BadInvalidState` | calls answer this status code |
|
|
206
|
+
| `health` | `state` | forces `DeviceHealth` |
|
|
207
|
+
|
|
208
|
+
**On the server:**
|
|
209
|
+
|
|
210
|
+
| Kind | Parameters | Effect |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `delay` | `seconds` | every response arrives late |
|
|
213
|
+
| `blackhole` | | connections stay open, nothing gets through |
|
|
214
|
+
| `drop_connections` | | all connections are cut now |
|
|
215
|
+
| `refuse` | | new connections are closed at once |
|
|
216
|
+
| `server_state` | `state` | `ServerStatus/State`: `Suspended`, `Test`, `Failed`, ... |
|
|
217
|
+
| `service_level` | `value` | `Server/ServiceLevel`, 0-255 |
|
|
218
|
+
| `restart` | `downtime=2` | the server goes away and returns on the same port without sessions or subscriptions; device state stays; building the server adds about a second |
|
|
219
|
+
|
|
220
|
+
Network faults sit in a small TCP forwarder in front of the server. `blackhole` holds the data
|
|
221
|
+
back rather than losing it, so a client that has not given up yet receives it after `clear`.
|
|
222
|
+
|
|
223
|
+
## Alarms
|
|
224
|
+
|
|
225
|
+
Alarms arrive as `ExclusiveLevelAlarmType` and `OffNormalAlarmType` events, from the device node
|
|
226
|
+
and from the `Server` object, with the usual fields: `ConditionId`, `ConditionName`, `Severity`,
|
|
227
|
+
`ActiveState/Id`, `AckedState/Id`, `Retain`, `Message`, and for level alarms the limits and
|
|
228
|
+
`LimitState/CurrentState`.
|
|
229
|
+
|
|
230
|
+
`Acknowledge` (`i=9111`, called on the `ConditionId`) checks the `EventId` and answers
|
|
231
|
+
`BadEventIdUnknown` or `BadConditionBranchAlreadyAcked`. `ConditionRefresh` (`i=3875`) sends
|
|
232
|
+
`RefreshStartEvent`, every retained condition, then `RefreshEndEvent`.
|
|
233
|
+
|
|
234
|
+
Alarm conditions are events, not full condition instances: `Enable`, `Disable`, `AddComment`,
|
|
235
|
+
`Confirm`, shelving and branches are not implemented, and the `Alarms/` nodes have no child
|
|
236
|
+
variables to browse.
|
|
237
|
+
|
|
238
|
+
## In pytest
|
|
239
|
+
|
|
240
|
+
Installing the package adds two fixtures. Every test gets a new bench on a free port:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
def test_notices_a_device_failure(opcua_bench):
|
|
244
|
+
opcua_bench.inject("drive", "bad_status", variable="Speed", status="BadDeviceFailure")
|
|
245
|
+
with pytest.raises(ua.uaerrors.BadDeviceFailure):
|
|
246
|
+
read_speed(opcua_bench.url) # your code
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_two_servers(opcua_bench_factory):
|
|
250
|
+
plant = opcua_bench_factory("plant.yaml") # bench file, dict or profile name
|
|
251
|
+
meter = opcua_bench_factory("energy-meter")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`--opcua-bench FILE` or the ini key `opcua_bench` choose the bench of `opcua_bench`; without them
|
|
255
|
+
it is one `drive`. See [`examples/test_example.py`](examples/test_example.py). The bench API is
|
|
256
|
+
`url`, `port`, `inject`, `clear`, `set`, `get`, `status`, `wait_restarted`.
|
|
257
|
+
|
|
258
|
+
Starting a server takes about a second, because asyncua loads the whole standard address space.
|
|
259
|
+
|
|
260
|
+
## Command line
|
|
261
|
+
|
|
262
|
+
| Command | |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `serve --bench FILE` or `--profile NAME` | run; `--host`, `--port`, `--endpoint-host`, `--security` |
|
|
265
|
+
| `profiles`, `map PROFILE` | what is there |
|
|
266
|
+
| `browse`, `read`, `write`, `call`, `watch` | a small OPC UA client; `--url`, `--user`, `--password`, `--timeout` |
|
|
267
|
+
| `inject`, `clear`, `set`, `get`, `list` | the control channel of a running bench; `--control-port` |
|
|
268
|
+
|
|
269
|
+
`set` forces a value from outside (`null` hands it back to the simulation), `watch --alarms` prints
|
|
270
|
+
alarm events. The control channel is one JSON object per line on port 4841, described in
|
|
271
|
+
[`control.py`](src/opcua_sim/control.py).
|
|
272
|
+
|
|
273
|
+
## Security
|
|
274
|
+
|
|
275
|
+
Do not put this on a network you do not trust. It is a test tool:
|
|
276
|
+
|
|
277
|
+
- By default it binds to loopback. With `--host 0.0.0.0` the control channel, which has no
|
|
278
|
+
authentication, is reachable too
|
|
279
|
+
- Passwords are plain text in the bench file, and on an endpoint without encryption they can
|
|
280
|
+
cross the wire in plain text
|
|
281
|
+
- Certificates are self-signed and created at every start; client certificates are not checked
|
|
282
|
+
|
|
283
|
+
## Browser demo
|
|
284
|
+
|
|
285
|
+
[`web/`](web/) holds a static demo page. It runs the real device code in the browser with
|
|
286
|
+
[Pyodide](https://pyodide.org), but not the OPC UA wire protocol, because a browser has no TCP
|
|
287
|
+
sockets. To test a client, install opcua-sim.
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
[MIT](LICENSE). The OPC UA stack [asyncua](https://github.com/FreeOpcUa/opcua-asyncio) is LGPL-3.0
|
|
292
|
+
and is installed as a separate package.
|