dnp3py
A pure Python implementation of the DNP3 (IEEE 1815-2012) protocol, including a MESA IEEE 1815.2 DER outstation simulator introduced in v0.2.0.
Features
- Pure Python - No C/C++ dependencies, works anywhere Python runs
- Level 2 object model - targets the IEEE 1815-2012 clause 14.4 (Table 14-3) subset for RTU-class SCADA use; see Protocol Conformance for the one known gap
- Async I/O - Built on asyncio for efficient network communication
- Type Safe - Full type annotations with strict mypy compliance
- Well Tested - see the CI and codecov badges above for current numbers
- MESA IEEE 1815.2 Outstation - Profile-driven DER outstation simulator for meters, DERs, inverters, and batteries
Installation
pip install dnp3py
Or with pixi:
pixi add dnp3py
Quick Start
Run the outstation script in one terminal, then the master script in a
second; both use 127.0.0.1:20000, so the master's integrity poll returns
the two points the outstation set.
Outstation (Server)
import asyncio
import logging
from dnp3.database import AnalogInputConfig, BinaryInputConfig, Database
from dnp3.outstation import Outstation, OutstationConfig, OutstationTcpRunner
logging.basicConfig(level=logging.INFO)
async def main():
# Create database with points
database = Database()
database.add_binary_input(0, BinaryInputConfig())
database.add_analog_input(0, AnalogInputConfig())
# Update values
database.update_binary_input(0, value=True)
# Static Analog Input transmits as a 32-bit integer by default (g30v1);
# a fractional value here is silently truncated on the wire.
database.update_analog_input(0, value=42)
# Create outstation and run it over TCP
outstation = Outstation(database=database, config=OutstationConfig(address=1))
runner = OutstationTcpRunner(outstation=outstation, host="127.0.0.1", port=20000)
await runner.run() # serves connections until runner.stop() is called
asyncio.run(main())
Master (Client)
import asyncio
from dnp3.master import DefaultSOEHandler, Master, MasterConfig, MasterTcpRunner
async def main():
# Create master with event handler
handler = DefaultSOEHandler()
master = Master(handler=handler, config=MasterConfig(address=2, outstation_address=1))
# Connect and perform an integrity poll
async with MasterTcpRunner(master=master, host="127.0.0.1", port=20000) as runner:
await runner.integrity_poll()
print(handler.binary_inputs[0].value) # True
print(handler.analog_inputs[0].value) # 42.0
asyncio.run(main())
MESA IEEE 1815.2 Outstation
The dnp3.mesa module is a DER-oriented outstation built on mesa-tool's
PicsProfile format (the companion Rust conformance control station); see
docs/mesa-outstation.md for the format and the
adoption rationale. It supports meters, DERs (distributed energy resources),
inverters, and batteries, plus counters, curves, and schedules. You describe
the device by loading a PicsProfile JSON file; the module builds the DNP3
database and command handler automatically, scaling analog values from
engineering units to DNP3 transmission integers on load.
Four bundled profiles ship inside the package
(full, mandatory_1815, mandatory_1547, minimal_1547); full is the
default. Profiles are authored as JSON; there is no spreadsheet ingestion
path.
Quick start (CLI)
# Run against the bundled full profile (the default)
python -m dnp3.mesa
# Run against a conformance subset
python -m dnp3.mesa --profile-name minimal_1547
# Run a custom profile, limited to the first meter and no DERs/inverters/batteries
python -m dnp3.mesa --profile my_device_profile.json --meters 1 --ders 0 --inverters 0 --batteries 0
For the full flag reference and defaults, see docs/mesa-outstation.md.
Programmatic API
import asyncio
from pathlib import Path
from dnp3.mesa.outstation import create_mesa_outstation
async def main():
outstation = create_mesa_outstation(
profile_path=Path("my_device_profile.json"),
host="0.0.0.0",
port=20000,
address=1,
master_address=0,
entity_overrides={"meters": 1, "ders": 0}, # optional
)
await outstation.run()
asyncio.run(main())
create_mesa_outstation returns a MesaOutstation dataclass. Call
await outstation.run() to start the TCP server; call await outstation.stop()
to shut it down cleanly.
For a full description of the PicsProfile format, the bundled profiles, the engineering-to-transmission scaling contract, and CTR/curve/schedule handling, see docs/mesa-outstation.md.
Protocol Conformance
Function Codes
CONFIRM, READ, WRITE, SELECT, OPERATE, DIRECT_OPERATE,
DIRECT_OPERATE_NO_ACK, COLD_RESTART, WARM_RESTART, DELAY_MEASURE,
RECORD_CURRENT_TIME, ENABLE_UNSOLICITED, DISABLE_UNSOLICITED,
IMMEDIATE_FREEZE, IMMEDIATE_FREEZE_NO_ACK, FREEZE_CLEAR,
FREEZE_CLEAR_NO_ACK (IEEE 1815-2012 Clause 4). Control commands are covered
in detail, with wire-level request/response encoding, in
docs/control-commands.md.
Object Groups (outstation)
| Group | Description |
|---|---|
| 1, 2 | Binary Input (static, event) |
| 10 | Binary Output (static) |
| 12 | Control Relay Output Block (select, operate, direct operate) |
| 20, 21, 22 | Counter (static, frozen, event) |
| 30, 32 | Analog Input (static, event) |
| 40, 41 | Analog Output (status, command: select, operate, direct operate) |
| 50 | Time and Date (WRITE g50v1 sets time; WRITE g50v3 sets time from RECORD_CURRENT_TIME, LAN sync) |
| 52 | Time Delay (response to DELAY_MEASURE) |
| 60 | Class data |
| 80 | Internal Indications (WRITE to clear DEVICE_RESTART or NEED_TIME) |
Wire layout follows IEEE 1815-2012 Annex A. The master additionally decodes
and delivers Double-Bit Binary Input (groups 3, 4) from a peer that sends it,
on its own handler callback (DoubleBitInputHandler); a few other groups the
wire layout recognizes (command events, frozen analog input, deadband, time)
are framed but not delivered to any handler.
Level 2 (clause 14.4, Table 14-3)
Time synchronization is implemented for both procedures IEEE 1815-2012
10.3.3 describes. Non-LAN (10.3.3.1): DELAY_MEASURE (function code 23)
answers the outstation's own processing delay (step c), which this
outstation reports as 0 (#146), and a WRITE of g50v1 delivers the written
time to the time_handler hook and clears NEED_TIME in its own response.
LAN (10.3.3.2, required of a TCP/IP outstation that sets NEED_TIME per
4.4.16.1 Rule 2): RECORD_CURRENT_TIME (function code 24) records the
receipt instant, and a following WRITE of g50v3 delivers the written time
plus the elapsed time since that instant, and clears NEED_TIME the same way.
A WRITE of g80v1 index 4 also clears NEED_TIME directly (4.5.5). Every other
Table 14-3 request row is implemented; g51 (Time and Date Common Time of
Occurrence) appears only in the response column of Table 14-3 and this
outstation reports no relative-time events that would need it.
Development
Setup
# Clone repository
git clone https://github.com/craigpnnl/dnp3py.git
cd dnp3py
# Install with pixi
pixi install
pixi run dev-install
# Set up pre-commit hooks (enforces quality checks before commits)
pixi run pre-commit-install
# Run tests
pixi run test
# Run with coverage
pixi run test-cov
# Lint and type check
pixi run check
# Test with a specific Python version (default, py311, py312, py313, py314)
pixi run -e py311 test
pixi run -e py312 test
# Test all Python versions (via nox)
pixi run nox
See CHANGELOG.md for release notes and upgrade notes between versions.
Project Structure
dnp3py/
+-- src/dnp3/
| +-- core/ # CRC, types, enums, flags
| +-- datalink/ # Data link layer (frames, parsing)
| +-- transport/ # Transport layer (segmentation)
| +-- application/ # Application layer (messages)
| +-- objects/ # DNP3 object definitions
| +-- database/ # Point database and events
| +-- outstation/ # Outstation implementation
| +-- master/ # Master implementation
| +-- mesa/ # MESA IEEE 1815.2 DER outstation
| | +-- data/profiles/ # Bundled PicsProfile JSON files (full.json default)
| +-- transport_io/ # TCP/simulator channels
+-- tests/
+-- unit/ # Unit tests
+-- integration/ # Integration tests
License
MIT License - see LICENSE for details.
Acknowledgments
This implementation follows the IEEE 1815-2012 standard for DNP3.
Metadata
Release files for dnp3py 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dnp3py-0.6.0.tar.gz | 652.8 kB | With provenance | Details for dnp3py-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dnp3py-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for dnp3py-0.6.0-py3-none-any.whl |
Total release size: 986.8 kB
Release files / dnp3py-0.6.0.tar.gz
| Download URL | dnp3py-0.6.0.tar.gz |
|---|---|
| Size | 652.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bfdba416d24cea659efd398da7e5e6218fcd277b3ad8bbe071071e92fb02bafb
|
|
BLAKE2b-256 checksum How to use checksums |
6c8dc01badc9bb899f62f2f49444e9f11c99ab1a9f7c29b1e2f6f2e04a05fe94
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / dnp3py-0.6.0-py3-none-any.whl
| Download URL | dnp3py-0.6.0-py3-none-any.whl |
|---|---|
| Size | 334.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6721e1167d83f99e5a5a3a13bb51b05b23cf7c83f3c6d3b6873611ef64b5d6ab
|
|
BLAKE2b-256 checksum How to use checksums |
c3b7adecfcc632fdd029dd7b14f793e7c2d3c5eb3e25c665f41a0954ebf1bf21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logSecurity for dnp3py 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dnp3py-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.