py61850 0.3.0.dev2__tar.gz → 0.5.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.
- py61850-0.5.0/.gitignore +60 -0
- py61850-0.5.0/CHANGELOG.md +224 -0
- py61850-0.5.0/NOTICE-IEC.txt +144 -0
- py61850-0.3.0.dev2/README.md → py61850-0.5.0/PKG-INFO +116 -6
- py61850-0.3.0.dev2/PKG-INFO → py61850-0.5.0/README.md +91 -33
- {py61850-0.3.0.dev2 → py61850-0.5.0}/ROADMAP.md +85 -2
- py61850-0.5.0/pyproject.toml +73 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/__init__.py +1 -1
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/__init__.py +7 -2
- py61850-0.5.0/src/py61850/scl/__init__.py +311 -0
- py61850-0.5.0/src/py61850/scl/_content_models.py +361 -0
- py61850-0.5.0/src/py61850/scl/_nsd_types.py +2483 -0
- py61850-0.5.0/src/py61850/scl/address.py +689 -0
- py61850-0.5.0/src/py61850/scl/control_block.py +625 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/controls.py +19 -6
- py61850-0.5.0/src/py61850/scl/data_set.py +794 -0
- py61850-0.5.0/src/py61850/scl/data_types.py +882 -0
- py61850-0.5.0/src/py61850/scl/document.py +920 -0
- py61850-0.5.0/src/py61850/scl/edit.py +602 -0
- py61850-0.5.0/src/py61850/scl/extref.py +736 -0
- py61850-0.5.0/src/py61850/scl/generator.py +517 -0
- py61850-0.5.0/src/py61850/scl/ied.py +939 -0
- py61850-0.5.0/src/py61850/scl/ordering.py +177 -0
- py61850-0.5.0/src/py61850/scl/report_control.py +531 -0
- py61850-0.5.0/src/py61850/scl/sampled_value_control.py +354 -0
- py61850-0.5.0/src/py61850/scl/substation.py +1056 -0
- py61850-0.5.0/src/py61850/scl/supervision.py +1433 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/BENCH.md +4 -4
- py61850-0.5.0/tests/fixtures/scl/README.md +82 -0
- py61850-0.5.0/tests/unit/editgen.py +791 -0
- py61850-0.5.0/tests/unit/roundtrip.py +382 -0
- py61850-0.5.0/tests/unit/scl_fixtures.py +717 -0
- py61850-0.5.0/tests/unit/test_fixture_hygiene.py +108 -0
- py61850-0.5.0/tests/unit/test_scl_address.py +1019 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_api.py +44 -0
- py61850-0.5.0/tests/unit/test_scl_comments.py +332 -0
- py61850-0.5.0/tests/unit/test_scl_control_block.py +847 -0
- py61850-0.5.0/tests/unit/test_scl_data_set.py +1100 -0
- py61850-0.5.0/tests/unit/test_scl_data_types.py +774 -0
- py61850-0.5.0/tests/unit/test_scl_edit.py +601 -0
- py61850-0.5.0/tests/unit/test_scl_edit_invertibility.py +358 -0
- py61850-0.5.0/tests/unit/test_scl_extref.py +817 -0
- py61850-0.5.0/tests/unit/test_scl_generator.py +651 -0
- py61850-0.5.0/tests/unit/test_scl_ied.py +1712 -0
- py61850-0.5.0/tests/unit/test_scl_namespaces.py +346 -0
- py61850-0.5.0/tests/unit/test_scl_ordering.py +374 -0
- py61850-0.5.0/tests/unit/test_scl_report_control.py +659 -0
- py61850-0.5.0/tests/unit/test_scl_roundtrip.py +200 -0
- py61850-0.5.0/tests/unit/test_scl_sampled_value_control.py +488 -0
- py61850-0.5.0/tests/unit/test_scl_substation.py +1434 -0
- py61850-0.5.0/tests/unit/test_scl_supervision.py +1758 -0
- py61850-0.5.0/tests/unit/test_scl_write.py +303 -0
- py61850-0.3.0.dev2/.gitignore +0 -24
- py61850-0.3.0.dev2/pyproject.toml +0 -54
- py61850-0.3.0.dev2/src/py61850/scl/__init__.py +0 -98
- py61850-0.3.0.dev2/src/py61850/scl/document.py +0 -326
- py61850-0.3.0.dev2/tests/unit/scl_fixtures.py +0 -221
- {py61850-0.3.0.dev2 → py61850-0.5.0}/CLA.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/COMMERCIAL.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/CONTRIBUTING.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/LICENSE +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/01_connect_and_scan.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/02_read_values.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/03_file_transfer.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/04_error_handling.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/05_fleet_inventory.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/06_find_files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/07_read_object_references.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/__main__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/__main__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/main.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/scan.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/ber.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/data.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/fc.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/quality.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/refs.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/time.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/errors.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/goose/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/link/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/client.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/pdu.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/service_error.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/directory.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/read.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/types.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/acse.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/cotp.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/oids.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/presentation.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/session.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/stack.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/tpkt.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/py.typed +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/_xmlsafe.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/communication.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/model.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/templates.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/sv/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/fixtures/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_ber.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_client.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_data.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_fc.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_file_filter.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_logical_nodes.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_osi.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_pdu.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_public_api.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_refs.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_communication.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_controls.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_document.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_model.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_templates.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_types.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_xmlsafe.py +0 -0
py61850-0.5.0/.gitignore
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.egg-info/
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
downloads/
|
|
10
|
+
relay_files/
|
|
11
|
+
inventory.csv
|
|
12
|
+
# Written by release.yml, from CHANGELOG.md, to be the GitHub release body.
|
|
13
|
+
RELEASE_NOTES.md
|
|
14
|
+
|
|
15
|
+
# SCL / IED capability files (vendor-supplied, large, not redistributable)
|
|
16
|
+
*.icd
|
|
17
|
+
*.ICD
|
|
18
|
+
*.cid
|
|
19
|
+
*.CID
|
|
20
|
+
*.scd
|
|
21
|
+
*.SCD
|
|
22
|
+
*.iid
|
|
23
|
+
*.IID
|
|
24
|
+
*.sed
|
|
25
|
+
*.SED
|
|
26
|
+
fixtures/ICD/
|
|
27
|
+
|
|
28
|
+
# The round-trip corpus is tracked, by name and never by wildcard.
|
|
29
|
+
#
|
|
30
|
+
# The `*.scd` rule above is right for every SCD except these four: a vendor
|
|
31
|
+
# file dropped in this tree is somebody's substation and must not be
|
|
32
|
+
# committed by accident. The three real ones below were anonymised by
|
|
33
|
+
# `tools/anonymise_scd.py` before they were added -- substation, utility,
|
|
34
|
+
# Windows accounts, machine names and addressing all substituted, IED names
|
|
35
|
+
# deliberately kept. The fourth is hand-written and carries no identity.
|
|
36
|
+
#
|
|
37
|
+
# Named one by one on purpose. A negation like `!tests/fixtures/scl/*.scd`
|
|
38
|
+
# would re-include ANY .scd dropped in that directory, which is exactly how
|
|
39
|
+
# an un-anonymised file reached a sibling repository once.
|
|
40
|
+
!tests/fixtures/scl/sel.scd
|
|
41
|
+
!tests/fixtures/scl/mixed.scd
|
|
42
|
+
!tests/fixtures/scl/siemens.scd
|
|
43
|
+
!tests/fixtures/scl/namespaces.scd
|
|
44
|
+
|
|
45
|
+
# The substitution mapping names what was substituted -- the substation, the
|
|
46
|
+
# utility, the engineers' accounts. Tracking it would undo the anonymisation.
|
|
47
|
+
tools/*.local.json
|
|
48
|
+
|
|
49
|
+
# IEC reference material -- the schema and namespace packages themselves.
|
|
50
|
+
#
|
|
51
|
+
# `docs/IEC61850 files/` holds IEC's own code-component distributions: the SCL
|
|
52
|
+
# XSD sets (2003, 2007B, 2007B4, 2007C5), the 7-2/7-3/7-4 NSD packages and
|
|
53
|
+
# IEC's note on handling code components. They are IEC copyright, the terms
|
|
54
|
+
# are at www.iec.ch/CCv1, and this repository is public and ships to PyPI.
|
|
55
|
+
#
|
|
56
|
+
# Excluded as a directory rather than by extension, because the useful ones
|
|
57
|
+
# arrive as .zip and a future .xsd dropped beside them must not be committed
|
|
58
|
+
# either. What may be DERIVED from them and shipped -- the content-model
|
|
59
|
+
# ordering table -- is a separate question, recorded as Q17 in the plan.
|
|
60
|
+
docs/IEC61850 files/
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Changelog — py61850
|
|
2
|
+
|
|
3
|
+
What changed in each released version. `ROADMAP.md` is the other half and is
|
|
4
|
+
forward-looking: it says what the library can do and what is planned, this says
|
|
5
|
+
what moved and when.
|
|
6
|
+
|
|
7
|
+
**This file starts at 0.5.0.** Releases up to 0.4.0 were written up in their
|
|
8
|
+
GitHub release bodies and are still there; they are not reconstructed here,
|
|
9
|
+
because a record invented after the fact is worth less than a link to the one
|
|
10
|
+
that was written at the time.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0.5.0 — 2026-09-15
|
|
15
|
+
|
|
16
|
+
**`py61850.scl` can now CHANGE an SCL document, not only read one and write it
|
|
17
|
+
back.** 0.3.0 gave the object model, 0.4.0 gave the byte-faithful round trip,
|
|
18
|
+
and this release puts an edit layer between them: four invertible primitives, a
|
|
19
|
+
schema-ordering table that decides where a new element goes, and the IEC 61850-6
|
|
20
|
+
rules that make an edit valid rather than merely applied.
|
|
21
|
+
|
|
22
|
+
The surface grows from **31 names to 132**. Nothing was removed and nothing
|
|
23
|
+
changed meaning — see *Compatibility* at the end, where that is measured rather
|
|
24
|
+
than asserted.
|
|
25
|
+
|
|
26
|
+
### The shape of it
|
|
27
|
+
|
|
28
|
+
Every function returns **a list of edits the caller applies in one go**, and
|
|
29
|
+
`apply_edit` returns the edit that undoes it. That is the whole design: an
|
|
30
|
+
operation that touches six elements in four sections of the file is one call,
|
|
31
|
+
one application and one undo step.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from py61850.scl import SclDocument, Connection, subscribe
|
|
35
|
+
|
|
36
|
+
doc = SclDocument.parse("station.scd")
|
|
37
|
+
|
|
38
|
+
sink = doc.ied("QMA1_MU1").ext_refs()[2] # an input awaiting a source
|
|
39
|
+
block = doc.ied("TR01_2414").control_blocks()[3] # the GOOSE that publishes it
|
|
40
|
+
fcda = block.logical_node.data_sets[block.dat_set].fcdas[12]
|
|
41
|
+
|
|
42
|
+
edits = subscribe(doc, [Connection(sink=sink.element,
|
|
43
|
+
fcda=fcda.element,
|
|
44
|
+
control_block=block.element)])
|
|
45
|
+
|
|
46
|
+
undo = doc.apply_edit(edits) # ONE history entry, however many edits it is
|
|
47
|
+
doc.write("station.scd") # byte-faithful everywhere it did not touch
|
|
48
|
+
doc.apply_edit(undo) # and exactly reversible
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**The edit layer works on `xml.etree` elements, not on the model objects.** The
|
|
52
|
+
model is how you find things; `.element` is what you hand to an edit function.
|
|
53
|
+
That is deliberate — an edit has to be expressible for a part of the file the
|
|
54
|
+
read model does not cover, and the `Substation` section below is exactly that
|
|
55
|
+
case.
|
|
56
|
+
|
|
57
|
+
Depending on what the document already holds, that one `subscribe` call writes
|
|
58
|
+
the ExtRef's binding attributes, creates the `Inputs` element if the logical
|
|
59
|
+
node has none, and — **when asked for, with `ignore_supervision=False`, which is
|
|
60
|
+
not the default** — instantiates the `LGOS`/`LSVS` supervision logical node that
|
|
61
|
+
watches the subscribed control block, allocating its `inst` against what the IED
|
|
62
|
+
already uses. Whether that is one edit or six, it is one call, one application
|
|
63
|
+
and one undo, and undoing it serialises byte-identically to what was there
|
|
64
|
+
before. A property test generates edit sequences and asserts exactly that.
|
|
65
|
+
|
|
66
|
+
It also refuses: the pair above was chosen by the type-restriction guard, and
|
|
67
|
+
the first candidate tried was rejected with
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
EditRejected: ExtRef expects SPS.t (Timestamp); SPS.stVal is BOOLEAN
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**A rejected edit changes nothing.** Every guard raises `EditRejected` before
|
|
74
|
+
touching the tree; there is no partially applied state to clean up.
|
|
75
|
+
|
|
76
|
+
### What was added
|
|
77
|
+
|
|
78
|
+
101 names, in fourteen modules. By area:
|
|
79
|
+
|
|
80
|
+
| Area | New names | What it covers |
|
|
81
|
+
|---|---:|---|
|
|
82
|
+
| `edit` | 5 | `Insert`, `Remove`, `SetAttributes`, `SetTextContent`, `EditRejected` — the primitives, each computing its inverse before it applies |
|
|
83
|
+
| `ordering` | 3 | `reference_for`, `may_contain`, `content_model` — schema-ordered insertion, table-driven from the SCL content models |
|
|
84
|
+
| `extref` | 12 | `subscribe` / `unsubscribe` / `is_subscribed`, the type-restriction matching behind them, and `fcda_type` |
|
|
85
|
+
| `control_block` | 9 | `control_blocks`, `update_dat_set`, `remove_control_block`, `updated_conf_rev`, `path_id`, `control_block_obj_ref` |
|
|
86
|
+
| `data_set` | 10 | `can_add_data_set`, create / update / remove, `can_add_fcda`, `remove_fcda`, `max_attributes` |
|
|
87
|
+
| `address` | 6 | `create_gse`, `create_smv`, `change_gse_content`, `change_smv_content`, `change_gse_or_smv_address` |
|
|
88
|
+
| `report_control` | 7 | `can_add_report_control`, create / update, `max_report_control`, `number_report_control_instances` |
|
|
89
|
+
| `sampled_value_control` | 3 | `can_add_sampled_value_control`, create, update |
|
|
90
|
+
| `ied` | 5 | `insert_ied`, `remove_ied`, `update_ied` — and the rename fan-out, which is the substance |
|
|
91
|
+
| `supervision` | 11 | `can_instantiate_supervision`, `instantiate_supervision`, `remove_supervision`, `max_supervision` |
|
|
92
|
+
| `data_types` | 8 | `import_lnode_types`, `update_lnode_type`, `remove_data_type`, `same_data_type`, `lnode_type_conflicts` |
|
|
93
|
+
| `substation` | 10 | `update_substation`, `update_voltage_level`, `update_bay`, `remove_process_element`, `prune_lnode_specification` |
|
|
94
|
+
| `generator` | 11 | `next_mac_address`, `next_app_id`, `next_ln_inst`, `unique_element_name` and the ranges they allocate from |
|
|
95
|
+
| `controls` | 1 | `CONTROL_BLOCK_TAGS` |
|
|
96
|
+
|
|
97
|
+
Three of those are worth naming individually, because they are the ones that do
|
|
98
|
+
more than their name suggests:
|
|
99
|
+
|
|
100
|
+
- **`update_ied` renames across the whole document, not just the `IED`.**
|
|
101
|
+
Renaming an IED has to follow every `ExtRef@iedName`, every concatenated
|
|
102
|
+
object reference that embeds it, and the control-block and supervision
|
|
103
|
+
references that name it — six element names in all, which is what the schema
|
|
104
|
+
requires rather than what a search-and-replace would find. `remove_ied`
|
|
105
|
+
cleans up the subscriptions and supervisions pointing at what it removes.
|
|
106
|
+
- **`import_lnode_types` merges type templates between documents**, with a
|
|
107
|
+
three-way conflict policy, because 165 type ids are shared between a pair of
|
|
108
|
+
the three reference exports and **141 of them carry different content**. Reuse
|
|
109
|
+
is decided on the type's whole closure, not on the element that names it.
|
|
110
|
+
- **`remove_control_block` sweeps by the block**, taking the `GSE` or `SMV`
|
|
111
|
+
address with it and the supervisions that watch it.
|
|
112
|
+
|
|
113
|
+
### Where this differs from OpenSCD
|
|
114
|
+
|
|
115
|
+
`open-scd-core` and `scl-lib` are this layer's reference, and the comparison is
|
|
116
|
+
behavioural: what theirs does against what ours does. Most divergences are
|
|
117
|
+
Python idiom or the fact that this document is an `xml.etree` tree on a server
|
|
118
|
+
rather than a DOM in a browser. These are the ones a caller actually meets.
|
|
119
|
+
|
|
120
|
+
- **Every function returns everything you should apply, input edit first.** The
|
|
121
|
+
reference is not uniform here — one function returns the array including the
|
|
122
|
+
edit you passed it, another returns only the extra edit, a third returns a
|
|
123
|
+
plural array again — so a caller has to remember, per function, whether to
|
|
124
|
+
also apply its own input. Ours has one rule. The cost is a return type that
|
|
125
|
+
disagrees with the reference's for some functions, and the benefit is that
|
|
126
|
+
applying a fragment on its own and leaving the document half-corrected is not
|
|
127
|
+
a mistake you can make.
|
|
128
|
+
- **Refusals where the reference guesses.** Subscribing refuses a connection it
|
|
129
|
+
cannot supervise; importing a type refuses an id collision rather than picking
|
|
130
|
+
a winner; inserting an IED refuses a name the target already holds. Each of
|
|
131
|
+
those is a place the reference proceeds. The reasoning is the same every
|
|
132
|
+
time: this library produces files that go back into DIGSI and SEL Architect,
|
|
133
|
+
and a wrong answer that loads is worse than a refusal that does not.
|
|
134
|
+
- **Three named outcomes replace a boolean** where an import can reuse, rename
|
|
135
|
+
or conflict, because two of the three are success and a boolean cannot say
|
|
136
|
+
which.
|
|
137
|
+
- **`fcda_type` keeps its name**, where the reference calls the same function
|
|
138
|
+
`fcdaBaseTypes`. It was already in 0.4.0's public surface and this is a MINOR
|
|
139
|
+
release; renaming a published name is not available, and the existing name is
|
|
140
|
+
the better one anyway — it returns the same `TypeRestriction` that
|
|
141
|
+
`ext_ref_type_restrictions` returns, so the two read as the pair they are.
|
|
142
|
+
- **Allocated names are `new<Tag>_01`**, matching the reference's documented
|
|
143
|
+
behaviour. Ours briefly produced `DataSet`/`DataSet_1` and that was corrected
|
|
144
|
+
before this tag.
|
|
145
|
+
- **`prune_lnode_specification` is not called `updateLnType`**, because the
|
|
146
|
+
reference's name for it disagrees with its own doc comment, its return type
|
|
147
|
+
and its directory: it updates no `lnType`, it removes an `LNode`'s
|
|
148
|
+
specification children, and those children live in IEC TR 61850-6-100's
|
|
149
|
+
namespace inside a `Private` rather than in 61850-6 at all.
|
|
150
|
+
- **`unique_element_name` is public**, where the reference does not export it.
|
|
151
|
+
|
|
152
|
+
### What this release does not implement
|
|
153
|
+
|
|
154
|
+
Stated because overstating it is the one thing a release note can get wrong that
|
|
155
|
+
nobody notices for a year.
|
|
156
|
+
|
|
157
|
+
- **Nothing renames a `GSEControl`.** The reference exports `createGSEControl`
|
|
158
|
+
and `updateGSEControl`; there is no Python equivalent. `ReportControl` and
|
|
159
|
+
`SampledValueControl` have their full create/update pair, `GSEControl` does
|
|
160
|
+
not. This is a gap the work found in itself and did not close.
|
|
161
|
+
- **No IEC namespace (NSD) data ships**, so neither `nsdToJson` nor anything
|
|
162
|
+
built on it has an equivalent. This is a licensing posture, not an oversight.
|
|
163
|
+
It was measured rather than assumed: `pDO` resolves from the document for
|
|
164
|
+
**0 of 1,244** ExtRefs in one reference export, so the data genuinely is not
|
|
165
|
+
in the file.
|
|
166
|
+
- **No plugin adapters.** `lNodeTypeToSelection` shapes a type for an
|
|
167
|
+
`oscd-tree-grid` widget; the structure it walks is already reachable through
|
|
168
|
+
`TemplatePool`.
|
|
169
|
+
- **No `checkPermission`.** Authorisation belongs to whatever is driving the
|
|
170
|
+
library.
|
|
171
|
+
- **`Substation` and `Log` still have no READ MODEL**, and the distinction from
|
|
172
|
+
the edit layer above is real. `update_substation`, `update_voltage_level`,
|
|
173
|
+
`update_bay` and `remove_process_element` work on the tree directly and are
|
|
174
|
+
tested against fixtures built by hand. There is no `doc.substation()` object
|
|
175
|
+
tree, because **all twenty-three Substation-section element names appear zero
|
|
176
|
+
times in all three reference exports**, in every namespace — a test asserts
|
|
177
|
+
that emptiness by scanning the bytes. Building a read model with nothing to
|
|
178
|
+
check it against is how a reader acquires confident wrong answers.
|
|
179
|
+
|
|
180
|
+
### Python 3.13 is the new floor
|
|
181
|
+
|
|
182
|
+
`requires-python` moves from `>=3.9` to `>=3.13`. **Two reasons, and they are
|
|
183
|
+
not the same kind of reason:**
|
|
184
|
+
|
|
185
|
+
- **3.9 and 3.10 are dead weight.** 3.9 reached end of life in October 2025 and
|
|
186
|
+
3.10 does so in October 2026. That half is a date.
|
|
187
|
+
- **3.11 and 3.12 are dropped for breadth, not for age.** Both are still
|
|
188
|
+
supported upstream. Six interpreters is more compatibility surface than a
|
|
189
|
+
library whose consumers all move together has any reason to carry, and one
|
|
190
|
+
fewer axis on the matrix is one fewer place a difference can hide. That half
|
|
191
|
+
is a judgement, and it is recorded as one.
|
|
192
|
+
|
|
193
|
+
**This cannot break code that already runs.** A floor is enforced by the
|
|
194
|
+
resolver, not by the interpreter: `pip install py61850` on 3.12 is served 0.4.0
|
|
195
|
+
and never a failure. That is why it travels in a MINOR release alongside a
|
|
196
|
+
strictly additive API, and the distinction is the one that matters — removing a
|
|
197
|
+
name breaks a program, raising a floor declines to hand one a newer library.
|
|
198
|
+
|
|
199
|
+
Nothing in the source branched on the interpreter (`sys.version_info` appears
|
|
200
|
+
nowhere in `src/py61850`), so the bump deletes no logic. The CI matrix narrows
|
|
201
|
+
from six versions to two; Windows still covers both ends of the supported range,
|
|
202
|
+
which is now 3.13 and 3.14.
|
|
203
|
+
|
|
204
|
+
### Compatibility
|
|
205
|
+
|
|
206
|
+
**Strictly additive, measured one name at a time.** All 31 names of 0.4.0 were
|
|
207
|
+
compared against this tree at the level of kind, module, MRO and the signature of
|
|
208
|
+
every public member. The entire difference is four additions: `ExtRef` and
|
|
209
|
+
`FCDA` each gained an `element` attribute, and `SclDocument` gained `apply_edit`
|
|
210
|
+
and `parent_of`. No name was removed, no signature changed, no class lost a
|
|
211
|
+
member and nothing moved module.
|
|
212
|
+
|
|
213
|
+
One consequence worth stating for anyone holding model objects across an edit:
|
|
214
|
+
**`apply_edit` invalidates the lazy caches by ancestry, and a model object you
|
|
215
|
+
already hold is not updated.** Editing inside one IED forgets that IED and leaves
|
|
216
|
+
the other 57 of a station warm; re-reading the edited one costs about 41 ms
|
|
217
|
+
against 3 ms warm on a 22 MB export.
|
|
218
|
+
|
|
219
|
+
### Verification
|
|
220
|
+
|
|
221
|
+
1,411 tests, the round trip held against 44 MB of real station exports from
|
|
222
|
+
three vendors, and the edit-invertibility property test run over generated
|
|
223
|
+
sequences. `py61850` still has **zero runtime dependencies**, which CI asserts
|
|
224
|
+
on every run.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Third-party notice: IEC code components
|
|
2
|
+
=======================================
|
|
3
|
+
|
|
4
|
+
Two files in this distribution are DERIVED from IEC code components:
|
|
5
|
+
|
|
6
|
+
src/py61850/scl/_content_models.py
|
|
7
|
+
src/py61850/scl/_nsd_types.py
|
|
8
|
+
|
|
9
|
+
Neither code component is itself part of this distribution, and each derived
|
|
10
|
+
file takes one narrow dimension of the component it came from.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
1. src/py61850/scl/_content_models.py
|
|
14
|
+
-------------------------------------
|
|
15
|
+
|
|
16
|
+
It carries the SCL content models -- which child elements each SCL element may
|
|
17
|
+
hold, and the order they go in -- extracted from the SCL XML schema published
|
|
18
|
+
by IEC as a code component of IEC 61850-6. The schema itself is NOT part of
|
|
19
|
+
this distribution. What is derived from it is one structural dimension: element
|
|
20
|
+
names and their order, and none of the schema's attributes, types,
|
|
21
|
+
enumerations, cardinalities, uniqueness constraints or documentation.
|
|
22
|
+
|
|
23
|
+
Attribution, as the licence below requires:
|
|
24
|
+
|
|
25
|
+
This code was derived from IEC 61850-6:2009/AMD1:2018 within modifications
|
|
26
|
+
permitted in the relevant IEC standard. Please reproduce this note if
|
|
27
|
+
possible.
|
|
28
|
+
|
|
29
|
+
The code component it was derived from:
|
|
30
|
+
|
|
31
|
+
IEC 61850-6 SCL schema V2007B4
|
|
32
|
+
Code component id: IEC_61850-6.2007B4.SCL.XSD
|
|
33
|
+
Publication: IEC 61850-6:2009/AMD1:2018 (Ed. 2.1), dated 2018-01-22
|
|
34
|
+
|
|
35
|
+
The copyright notice carried by that code component:
|
|
36
|
+
|
|
37
|
+
COPYRIGHT (c) IEC, 2018. This version of this XSD is part of
|
|
38
|
+
IEC 61850-6:2009/AMD1:2018; see the IEC 61850-6:2009/AMD1:2018 for full
|
|
39
|
+
legal notices. In case of any differences between the here-below code and
|
|
40
|
+
the IEC published content, the here-below definition supersedes the IEC
|
|
41
|
+
publication; it may contain updates. See history files. The whole document
|
|
42
|
+
has to be taken into account to have a full description of this code
|
|
43
|
+
component.
|
|
44
|
+
See www.iec.ch/CCv1 for copyright details.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
2. src/py61850/scl/_nsd_types.py
|
|
48
|
+
--------------------------------
|
|
49
|
+
|
|
50
|
+
It carries two facts about the IEC 61850 data model -- which common data class
|
|
51
|
+
each data object name has, and which basic type each of a class's data
|
|
52
|
+
attributes has -- extracted from the namespace definitions published by IEC as
|
|
53
|
+
code components of IEC 61850-7-4 and IEC 61850-7-3. The namespace definitions
|
|
54
|
+
themselves are NOT part of this distribution. What is derived from them is the
|
|
55
|
+
type dimension alone, and none of their functional constraints, presence
|
|
56
|
+
conditions, enumerations and their literals, abbreviations, service parameters,
|
|
57
|
+
UML identifiers or documentation.
|
|
58
|
+
|
|
59
|
+
Attribution, as the licence below requires:
|
|
60
|
+
|
|
61
|
+
This code was derived from IEC 61850-7-3:2010 and IEC 61850-7-4:2020 within
|
|
62
|
+
modifications permitted in the relevant IEC standard. Please reproduce this
|
|
63
|
+
note if possible.
|
|
64
|
+
|
|
65
|
+
The code components it was derived from:
|
|
66
|
+
|
|
67
|
+
IEC 61850-7-3 namespace definition V2007B5
|
|
68
|
+
Code component id: IEC 61850-7-3:2007B5
|
|
69
|
+
Publication: IEC 61850-7-3:2010 (Ed. 2.1), dated 2024-02-12
|
|
70
|
+
|
|
71
|
+
IEC 61850-7-4 namespace definition V2007B5
|
|
72
|
+
Code component id: IEC 61850-7-4:2007B5
|
|
73
|
+
Publication: IEC 61850-7-4:2020 (Ed. 2.1), dated 2024-02-14
|
|
74
|
+
|
|
75
|
+
The copyright notice carried by those code components:
|
|
76
|
+
|
|
77
|
+
COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this
|
|
78
|
+
NSD is part of IEC_61850-7-3:2010 Edition 2.1 [respectively
|
|
79
|
+
IEC_61850-7-4:2020 Edition 2.1]; see that publication for full legal
|
|
80
|
+
notices. In case of any differences between the here-below code and the IEC
|
|
81
|
+
published content, the here-below definition supersedes the IEC
|
|
82
|
+
publication; it may contain updates. See history files. The whole document
|
|
83
|
+
has to be taken into account to have a full description of this code
|
|
84
|
+
component.
|
|
85
|
+
See www.iec.ch/CCv1 for copyright details.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
py61850 is not endorsed by, affiliated with, or approved by IEC.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
Code Components -- End-user licence agreement
|
|
92
|
+
---------------------------------------------
|
|
93
|
+
|
|
94
|
+
Reproduced from www.iec.ch/CCv1, which the code component's manifest names as
|
|
95
|
+
its licence.
|
|
96
|
+
|
|
97
|
+
Code Components in IEC standards (International Standards, Technical
|
|
98
|
+
Specifications or Technical Reports) which have been identified and approved
|
|
99
|
+
for licensing, are licensed subject to the following conditions:
|
|
100
|
+
|
|
101
|
+
- Redistributions of software must retain the Copyright Notice, this list of
|
|
102
|
+
conditions and the disclaimer below ("Disclaimer").
|
|
103
|
+
- The software license extends to modifications permitted under the relevant
|
|
104
|
+
IEC standard.
|
|
105
|
+
- The software license extends to clarifications and corrections approved by
|
|
106
|
+
IEC.
|
|
107
|
+
- Neither the name of IEC, nor the names of specific contributors, may be used
|
|
108
|
+
to endorse or promote products derived from this software without specific
|
|
109
|
+
prior written permission. The relevant IEC standard may be referenced when
|
|
110
|
+
claiming compliance with the relevant IEC standard.
|
|
111
|
+
- The user of Code Components shall attribute each such Code Component to IEC
|
|
112
|
+
and identify the IEC standard from which it is taken. Such attribution (e.g.,
|
|
113
|
+
"This code was derived from IEC [insert standard reference number:publication
|
|
114
|
+
year] within modifications permitted in the relevant IEC standard. Please
|
|
115
|
+
reproduce this note if possible."), may be placed in the code itself or any
|
|
116
|
+
other reasonable location.
|
|
117
|
+
|
|
118
|
+
Code Components means components included in IEC standards that are intended to
|
|
119
|
+
be directly processed by a computer and also includes any text found between
|
|
120
|
+
the markers <CODE BEGINS> and <CODE ENDS>, or otherwise clearly labeled in this
|
|
121
|
+
standard as a Code Component.
|
|
122
|
+
|
|
123
|
+
The Disclaimer is:
|
|
124
|
+
|
|
125
|
+
EACH OF THE CODE COMPONENTS IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
126
|
+
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
127
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
128
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
129
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
130
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
131
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
132
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
133
|
+
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
134
|
+
ARISING IN ANY WAY OUT OF THE USE OF THE CODE COMPONENTS, EVEN IF ADVISED OF
|
|
135
|
+
THE POSSIBILITY OF SUCH DAMAGE.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Everything else in this distribution
|
|
139
|
+
------------------------------------
|
|
140
|
+
|
|
141
|
+
Licensed under AGPL-3.0-or-later; see LICENSE. A commercial licence is
|
|
142
|
+
available from the copyright holder -- see COMMERCIAL.md. The terms above
|
|
143
|
+
apply to the derived files named at the top of this notice, and travel with
|
|
144
|
+
them.
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: py61850
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Pure-Python IEC 61850 toolkit — an MMS (ISO 9506) client, and an SCL reader/writer/editor with a byte-faithful round trip. Standard library only.
|
|
5
|
+
Project-URL: Homepage, https://github.com/GuilhermeMarini/py61850
|
|
6
|
+
Project-URL: Repository, https://github.com/GuilhermeMarini/py61850
|
|
7
|
+
Project-URL: Issues, https://github.com/GuilhermeMarini/py61850/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/GuilhermeMarini/py61850/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Roadmap, https://github.com/GuilhermeMarini/py61850/blob/main/ROADMAP.md
|
|
10
|
+
Author: Guilherme
|
|
11
|
+
License-Expression: AGPL-3.0-or-later
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
License-File: NOTICE-IEC.txt
|
|
14
|
+
Keywords: goose,iec61850,ied,iso9506,mms,relay,sampled-values,scada,substation
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
1
26
|
# py61850 — IEC 61850 toolkit (pure Python)
|
|
2
27
|
|
|
3
28
|
An importable library (and its CLIs) for talking to an IEC 61850 IED over MMS
|
|
@@ -10,8 +35,14 @@ TCP → TPKT (RFC1006) → COTP (ISO8073) → ISO Session → ISO Presentation
|
|
|
10
35
|
→ ACSE (AARQ/AARE) → MMS (Initiate + confirmed services)
|
|
11
36
|
```
|
|
12
37
|
|
|
38
|
+
It is also a **61850-6 (SCL) toolkit**: `py61850.scl` reads an `.scd`/`.cid`/
|
|
39
|
+
`.icd` into a general object model, **changes** it through an edit layer whose
|
|
40
|
+
every operation is invertible, and writes it back **byte for byte** where it did
|
|
41
|
+
not touch. That half needs no relay and no network at all.
|
|
42
|
+
|
|
13
43
|
See [ROADMAP.md](ROADMAP.md) for what's next (an MMS sniffer, MMS simulation
|
|
14
|
-
from SCL, and a GOOSE sniff/publish interface)
|
|
44
|
+
from SCL, and a GOOSE sniff/publish interface), and
|
|
45
|
+
[CHANGELOG.md](CHANGELOG.md) for what changed in each release.
|
|
15
46
|
|
|
16
47
|
## What this library is for
|
|
17
48
|
|
|
@@ -40,11 +71,12 @@ Concretely, that means three things for any change here:
|
|
|
40
71
|
pip install -e . # from a checkout (editable)
|
|
41
72
|
# or, once published/tagged:
|
|
42
73
|
# pip install py61850
|
|
43
|
-
# pip install "git+https://github.com/OWNER/py61850@v0.
|
|
74
|
+
# pip install "git+https://github.com/OWNER/py61850@v0.5.0"
|
|
44
75
|
```
|
|
45
76
|
|
|
46
|
-
Requires Python ≥ 3.
|
|
47
|
-
|
|
77
|
+
Requires **Python ≥ 3.13** (raised from 3.9 in 0.5.0 — see
|
|
78
|
+
[CHANGELOG.md](CHANGELOG.md) for both reasons). Installs `py61850`
|
|
79
|
+
(subcommands) plus `mms-scan` and `mms-files` for the two MMS drivers directly.
|
|
48
80
|
|
|
49
81
|
## Use as a library
|
|
50
82
|
|
|
@@ -108,7 +140,7 @@ with FileTransfer("192.0.2.22") as ft:
|
|
|
108
140
|
progress=lambda got, total: print(f"{got}/{total}"))
|
|
109
141
|
```
|
|
110
142
|
|
|
111
|
-
### Reading an SCL file
|
|
143
|
+
### Reading an SCL file — and writing it back unchanged
|
|
112
144
|
|
|
113
145
|
```python
|
|
114
146
|
from py61850.scl import SclDocument
|
|
@@ -126,6 +158,74 @@ for ln in ied.logical_nodes():
|
|
|
126
158
|
print(attr.reference(), attr.mms_item(), attr.fc, attr.btype)
|
|
127
159
|
```
|
|
128
160
|
|
|
161
|
+
A parsed document goes back out as the file it came from:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
doc.write("station.scd") # atomic: temp file beside it, then os.replace
|
|
165
|
+
raw = doc.to_bytes() # or the bytes, to hand somewhere else
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Byte for byte**, with no edit applied — comments, indentation, attribute
|
|
169
|
+
order, namespace prefixes, `xmlns` declarations nothing uses, the line ending
|
|
170
|
+
the file was written with and the XML declaration as it was spelled all
|
|
171
|
+
survive. Five differences are permitted, none of them observable through an
|
|
172
|
+
XML parser and every one a limit of the standard library's serialiser:
|
|
173
|
+
attribute quote style, empty-element spacing and form, CDATA boundaries, and
|
|
174
|
+
the spelling of a numeric character reference. `SclDocument.to_bytes` states
|
|
175
|
+
all five; the round-trip test holds them against 44 MB of real station
|
|
176
|
+
exports from three vendors.
|
|
177
|
+
|
|
178
|
+
That guarantee is the point of the write side. The file goes back into DIGSI
|
|
179
|
+
and SEL Architect, and a library that reformats the 99 % of an SCD it did not
|
|
180
|
+
touch turns every save into a whole-file diff.
|
|
181
|
+
|
|
182
|
+
### Changing it — the edit layer
|
|
183
|
+
|
|
184
|
+
Since 0.5.0 a document can also be **edited**, and every edit is invertible.
|
|
185
|
+
Four primitives (`Insert`, `Remove`, `SetAttributes`, `SetTextContent`) each
|
|
186
|
+
compute their inverse *before* they apply; a list of them is itself an edit,
|
|
187
|
+
applied in order and inverted in reverse. On top of those sit the IEC 61850-6
|
|
188
|
+
rules that decide what may be created where, what a rename has to drag along,
|
|
189
|
+
and where in a parent a new element belongs.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from py61850.scl import SclDocument, Connection, subscribe
|
|
193
|
+
|
|
194
|
+
doc = SclDocument.parse("station.scd")
|
|
195
|
+
|
|
196
|
+
sink = doc.ied("QMA1_MU1").ext_refs()[2] # an input awaiting a source
|
|
197
|
+
block = doc.ied("TR01_2414").control_blocks()[3] # the GOOSE that publishes it
|
|
198
|
+
fcda = block.logical_node.data_sets[block.dat_set].fcdas[12]
|
|
199
|
+
|
|
200
|
+
edits = subscribe(doc, [Connection(sink=sink.element,
|
|
201
|
+
fcda=fcda.element,
|
|
202
|
+
control_block=block.element)])
|
|
203
|
+
|
|
204
|
+
undo = doc.apply_edit(edits) # ONE history entry, however many edits it is
|
|
205
|
+
doc.write("station.scd")
|
|
206
|
+
doc.apply_edit(undo) # restores the document exactly
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Three things hold the layer together:
|
|
210
|
+
|
|
211
|
+
- **Every function returns everything you should apply, input edit first.** One
|
|
212
|
+
rule, one `apply_edit` call, one undo entry — so "subscribe this ExtRef",
|
|
213
|
+
which may touch an `ExtRef`, an `Inputs` and a supervision logical node in
|
|
214
|
+
three places, is one step in a caller's history.
|
|
215
|
+
- **The edit functions take `xml.etree` elements, not the model objects.** The
|
|
216
|
+
model is how you find things and `.element` is what you pass; that is what
|
|
217
|
+
lets an edit reach a part of the file the read model does not cover.
|
|
218
|
+
- **A rejected edit changes nothing.** Guards raise `EditRejected` before the
|
|
219
|
+
tree is touched, so there is no half-applied state — the same rule the round
|
|
220
|
+
trip depends on.
|
|
221
|
+
|
|
222
|
+
`py61850.scl.__all__` is 132 names across the areas it can edit: control blocks
|
|
223
|
+
and datasets, GSE/SMV addresses, report and sampled-value control, IEDs and the
|
|
224
|
+
rename fan-out, subscription supervision, `DataTypeTemplates` import and merge,
|
|
225
|
+
the `Substation` section, and the allocators for MAC addresses, APPIDs and
|
|
226
|
+
instance numbers. `CHANGELOG.md` lists them by area, says where this diverges
|
|
227
|
+
from OpenSCD and — more usefully — what it does **not** implement.
|
|
228
|
+
|
|
129
229
|
`py61850.scl` is imported explicitly and is never pulled in by `import
|
|
130
230
|
py61850`, so the MMS client keeps installing and running unprivileged.
|
|
131
231
|
|
|
@@ -157,6 +257,9 @@ use one client per thread.
|
|
|
157
257
|
| `Iec61850Error` | base of `TransportError`, `MmsError`, and the reserved `LinkError` / `GooseError` / `SvError` / `SclError` |
|
|
158
258
|
| `decode_read_response`, `decode_data_definition`, `decode_service_error` | TLV decoders |
|
|
159
259
|
|
|
260
|
+
`py61850.scl` has a public surface of its own — 132 names, imported from
|
|
261
|
+
`py61850.scl` rather than from `py61850` — and the two do not overlap.
|
|
262
|
+
|
|
160
263
|
Everything else — `core.ber`, `mms.pdu`, `osi.*`, `CotpTransport` — is internal
|
|
161
264
|
and may change between releases.
|
|
162
265
|
|
|
@@ -248,7 +351,14 @@ src/py61850/
|
|
|
248
351
|
├── link/ # packet-capture sources — planned (ROADMAP 0.2, 2.0)
|
|
249
352
|
├── goose/ # GOOSE pub/sub — planned (ROADMAP 2.0)
|
|
250
353
|
├── sv/ # Sampled Values — planned (after GOOSE)
|
|
251
|
-
├── scl/ #
|
|
354
|
+
├── scl/ # IEC 61850-6: read, edit, write. The largest package
|
|
355
|
+
│ │ # here, and never imported by `py61850` itself
|
|
356
|
+
│ ├── document.py # SclDocument: parse, the byte-faithful write, edits
|
|
357
|
+
│ ├── model.py # the instance tree, built per IED on demand
|
|
358
|
+
│ ├── templates.py # DataTypeTemplates resolved once per document
|
|
359
|
+
│ ├── edit.py # the four primitives, each inverted before it applies
|
|
360
|
+
│ ├── ordering.py # where a new element goes, from the content models
|
|
361
|
+
│ └── … # one module per 61850-6 area it can edit
|
|
252
362
|
└── cli/
|
|
253
363
|
├── main.py # `py61850` — subcommand dispatcher
|
|
254
364
|
├── scan.py # `mms-scan` — data-model scan driver
|