Skip to main content

dnp3py

CI codecov PyPI version Python versions License: MIT Ruff Checked with mypy

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)

Source distribution for dnp3py 0.6.0
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)

Table of built distributions (wheels) for dnp3py 0.6.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page