Metadata-Version: 2.4
Name: py20305
Version: 0.3.0
Summary: An open IEEE 2030.5 / CSIP client for distributed energy resources
License-Expression: Apache-2.0
Keywords: ieee2030.5,csip,csip-aus,der,sunspec,modbus,smart-grid
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: aiohttp>=3.9
Requires-Dist: cryptography>=42.0
Requires-Dist: pydantic>=2.6
Requires-Dist: xsdata-pydantic>=24.0
Requires-Dist: lxml>=5.0
Provides-Extra: api
Requires-Dist: fastapi>=0.110; extra == "api"
Requires-Dist: uvicorn>=0.27; extra == "api"
Provides-Extra: cli
Requires-Dist: pyyaml>=6.0; extra == "cli"
Provides-Extra: sunspec
Requires-Dist: pysunspec2>=1.3.5; extra == "sunspec"
Requires-Dist: pyserial>=3.5; extra == "sunspec"
Provides-Extra: mqtt
Requires-Dist: aiomqtt>=2.0; extra == "mqtt"
Provides-Extra: all
Requires-Dist: py20305[api,cli,mqtt,sunspec]; extra == "all"
Provides-Extra: docs
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
Provides-Extra: dev
Requires-Dist: py20305[all,docs]; extra == "dev"
Requires-Dist: ruff<0.16,>=0.4; extra == "dev"
Requires-Dist: mypy>=1.9; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: pytest-aiohttp>=1.1; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: lxml-stubs>=0.5; extra == "dev"
Requires-Dist: import-linter>=2.0; extra == "dev"
Requires-Dist: pre-commit>=3.7; extra == "dev"
Dynamic: license-file

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/satori-dark.png">
    <img src="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/satori.png" alt="Satori" height="88">
  </picture>
</p>

<p align="center">
  <a href="https://lfenergy.org/"><picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/lf-energy-white.png">
    <img src="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/lf-energy-color.png" alt="LF Energy" height="32">
  </picture></a>
  &nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://sunspec.org/"><img src="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/sunspec.png" alt="SunSpec Alliance" height="44"></a>
  &nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://dersec.io/"><picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/dersec-white.png">
    <img src="https://raw.githubusercontent.com/DERSecurity/py20305/main/docs/assets/dersec-dark.png" alt="DER Security" height="32">
  </picture></a>
</p>

# py20305

**The IEEE 2030.5 / CSIP client of [Project Satori](https://dersec.io/satori)** —
an LF Energy open-source project led by the SunSpec Alliance, DER Security,
and industry consortium members.

[![CI](https://github.com/DERSecurity/py20305/actions/workflows/ci.yml/badge.svg)](https://github.com/DERSecurity/py20305/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FDERSecurity%2Fpy20305%2Fbadges%2F.github%2Fbadges%2Ftests.json)](https://github.com/DERSecurity/py20305/actions/workflows/ci.yml)
[![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FDERSecurity%2Fpy20305%2Fbadges%2F.github%2Fbadges%2Fcoverage.json)](https://github.com/DERSecurity/py20305/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/py20305)](https://pypi.org/project/py20305/)
[![Python](https://img.shields.io/pypi/pyversions/py20305)](https://pypi.org/project/py20305/)
[![License](https://img.shields.io/github/license/DERSecurity/py20305)](https://github.com/DERSecurity/py20305/blob/main/LICENSE)
[![Project Satori](https://img.shields.io/badge/Project-Satori-b7410e)](https://dersec.io/satori)

An open IEEE 2030.5 client for distributed energy resources, with CSIP-AUS
support. It speaks the utility's protocol so your device doesn't have to.

Point it at an IEEE 2030.5 server and it registers, discovers what the server
exposes, runs the DERControl schedule it is given, applies the resulting
setpoints to a device, and posts telemetry back.

## Install

```bash
pip install py20305
```

The base install is the protocol client. Extras add what you may not need:
`sunspec` for Modbus devices, `api` for the HTTP management API, `mqtt` for
traffic forwarding, or `all`.

## Running it as a service

```bash
pip install "py20305[cli,sunspec]"
py20305 --config client.yaml --check   # validate, print your LFDI
py20305 --config client.yaml           # run
```

One YAML or JSON file describes the server, the certificate and the devices. It
retries the connection while the server is down, registers itself only if the
server does not already know it, and stops cleanly on SIGTERM. See
[Running a client](https://dersecurity.github.io/py20305/running/)
and [`examples/client.example.yaml`](https://github.com/DERSecurity/py20305/blob/main/examples/client.example.yaml).

## Running it in a container

```bash
docker build -t py20305 .
docker run -d -v "$PWD/config:/etc/py20305:ro" py20305
```

The image carries the client and nothing else -- configuration and certificates
are mounted, never baked in. It runs unprivileged and stops in order on
`SIGTERM`. See [Running in a container](https://dersecurity.github.io/py20305/docker/)
and [`examples/docker-compose.yml`](https://github.com/DERSecurity/py20305/blob/main/examples/docker-compose.yml).

## Embedding it: talking to a server in five minutes

IEEE 2030.5 is mutual TLS throughout, and the server identifies your client by
its certificate. Your LFDI — the identifier the utility registers — is derived
from it:

```python
from pathlib import Path
from py20305.security import compute_lfdi

print(compute_lfdi(Path("certs/client.pem").read_text()))
```

Give that to whoever runs the server, then:

```bash
python examples/quickstart.py \
  --url https://server.example.com:8443 \
  --cert certs/client.pem --key certs/client.key --ca certs/ca.pem
```

That drives the print connector, which needs no hardware — it logs the controls
it would have applied. So you can exercise the whole discovery, event and
telemetry path against a real server before an inverter is wired up.

Swap in `SunSpecDeviceConfig` when you have one. Full walkthrough in the
[quickstart](https://dersecurity.github.io/py20305/quickstart/).

## What's in it

| | |
|---|---|
| **Protocol client** | Discovery, registration, mTLS transport, retry with backoff, redirect probing, server timebase |
| **Event engine** | DERControl five-state machine, supersession across programs, randomized start and duration, `Response` acknowledgments |
| **Telemetry** | DERStatus, DERCapability, DERSettings, DERAvailability, metered readings as MirrorUsagePoints |
| **Connectors** | SunSpec Modbus over TCP, RTU and TLS; a no-hardware print connector; your own by subclass or factory |
| **Subscriptions** | Subscribe/notify as an alternative to polling, with a notification listener |
| **Runner** | A CLI, config file, connection retry and signal-handled shutdown |
| **Management API** | Optional HTTP API for observing and nudging a running client |
| **Forwarding** | Optional MQTT publication of every captured exchange |

## Standards

**IEEE 2030.5-2018 and 2030.5-2023.** Both XSDs ship with the package and
validation runs against them. The generated bindings track 2023; a
`server_2018_compat` flag adjusts behavior for servers still on 2018.

**CSIP-AUS**, including DOE (dynamic operating envelope) limits.

The client maps one certificate identity to one end device. It registers and
drives that device, and does not fan a server-side EndDevice out across several
local ones.

## Certification

py20305 is not SunSpec CSIP certified, and using it does not make a product
certified. Certification is a property of the *product* -- the assembled
client device or system, tested as a whole -- not of a library inside it.

If you are building a client product that needs SunSpec CSIP certification,
follow the SunSpec Alliance's certification process:
[IEEE 2030.5 / CSIP certification](https://sunspec.org/ieee-2030-5-csip-certification/).

## Project Satori

*Any certified DER. Any utility program.* Satori — “awakening” — is an
open-source initiative to make any certified DER a compliant participant in
utility DER programs: point it at the Modbus port of a UL 1741 SB device —
inverter, battery, or EV — and it joins a program without a firmware rewrite.
The bundle builds on SunSpec-certified implementations contributed by
DER Security, and this repository is its IEEE 2030.5 half:

| Project | Role |
|---|---|
| [PySunSpec2](https://github.com/sunspec/pysunspec2) | SunSpec Modbus reference library, used in more than 80% of inverter-based products shipped globally |
| **py20305** (this repository) | IEEE 2030.5 client stack with CSIP and CSIP-AUS support, and a SunSpec Modbus bridge |
| SunSpec DevKit CE | Discover, read and identify SunSpec devices on the wire |

## Documentation

<https://dersecurity.github.io/py20305/>

## Development

```bash
pip install -e ".[dev]"
pytest tests -q                 # unit tests, no network
ruff check src tests examples scripts
mypy
mkdocs serve                    # docs at http://127.0.0.1:8000
```

There is also an end-to-end suite that runs this client against
[envoy](https://github.com/bsgip/envoy), the open-source CSIP-AUS utility
server from the Australian National University — the implementation ANU's
certification program runs on. It needs Docker:

```bash
python scripts/e2e_server.py up
pytest tests/e2e -v
python scripts/e2e_server.py down
```

The unit suite proves this client does what we believe the standard requires.
That one proves the belief survives contact with an implementation nobody here
wrote. See [Testing](https://dersecurity.github.io/py20305/testing/).

See [CONTRIBUTING.md](https://github.com/DERSecurity/py20305/blob/main/CONTRIBUTING.md).

## License

Apache-2.0. See [LICENSE](https://github.com/DERSecurity/py20305/blob/main/LICENSE) and [NOTICE](https://github.com/DERSecurity/py20305/blob/main/NOTICE).
