Master¶
A master for exercising outstations. The master, The console, The master's API and Evaluating an outstation are the guides; this is the reference.
Over a socket¶
py1815.master.api ¶
A master on a socket: the interface a script uses.
:class:Master holds outstations by name. :class:Outstation is one
association over one TCP connection, with TLS when it is given a context, and
is where the requests are. Each request is a coroutine that returns an
:class:~py1815.master.association.Exchange saying what happened: a timeout
and an error indication are results, and an exception means the interface was
misused or the connection could not be made.
Each outstation also runs automatic tasks: a startup sequence on connect,
clearing the restart indication, writing the time on request, polling for
events when indicated, and reconnecting after a lost connection. Configure
them with :class:~py1815.master.tasks.Tasks, or pass manual=True to
disable every task and confirmation. :mod:py1815.master.sync has the same
interface for a script with no event loop.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
RetryReport ¶
Log failed reconnection attempts without filling the log.
The first failure is logged at INFO. Later failures are logged at DEBUG,
with one INFO record each interval seconds that counts them. An
outstation that is down for a day with a five-second retry then writes
about 25 INFO records instead of 17,000.
Source code in src/py1815/master/api.py
failed ¶
Record one failed attempt, and return the level it was logged at.
Source code in src/py1815/master/api.py
NotConnected ¶
Outstation ¶
Bases: Operations[Awaitable[Exchange], Awaitable[Operated], Awaitable[Synchronized]]
One outstation, as a master sees it: a connection, an association, a store.
Source code in src/py1815/master/api.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 | |
tasks
property
writable
¶
The automatic task settings for this outstation.
Set it to change them while the outstation is connected. A task that is due and is now off is not done; what is done next follows the new settings.
unsolicited
property
¶
The unsolicited responses received, oldest first.
scan_intervals
property
¶
The scans being repeated, by kind, as seconds between them.
connect
async
¶
Open the connection. Raises OSError if it cannot be made.
With wait, a connection that cannot be made is tried again every
every seconds for up to wait seconds, and the last error is
raised when none could be made in that time. Without it the first
failure is raised.
Returns as soon as the connection is open. The startup tasks run
after that; use :meth:idle to wait for them.
If this is called while the master is retrying a lost connection and the attempt fails, the error is raised and the retries carry on.
Source code in src/py1815/master/api.py
idle
async
¶
Wait until no automatic task is pending or running.
Returns immediately if there is none, or if the connection has ended.
close
async
¶
Close the connection. The store and the association's counters stay.
Source code in src/py1815/master/api.py
wait_for
async
¶
Wait until condition(store) is true, and return True; or False at the timeout.
The condition is looked at now, and again each time a response or an
unsolicited response has been taken into the store and
:attr:indications, and when the connection is made or ends. It may
look at anything, this outstation included:
.. code-block:: python
await lab.wait_for(lambda store: store.analog_output(87).value == 5000, 10)
await lab.wait_for(lambda _: lab.indications.is_set(IINBit.NEED_TIME), 10)
Nothing is sent to make it come true. A timeout is a result and not an exception; an exception the condition raises is raised here.
Source code in src/py1815/master/api.py
repeat_scan ¶
Repeat a scan every interval seconds, or stop repeating it with None.
No scan is repeated unless this is called. A repeated scan waits its turn like any other request, stops when the connection ends, and starts again when it is made again.
Source code in src/py1815/master/api.py
Master ¶
Outstations, by the names their caller gave them.
Source code in src/py1815/master/api.py
add
async
¶
Add an outstation and, unless told not to, connect to it.
options are those of :class:Outstation, and wait is that of
:meth:Outstation.connect. A name is used once. An outstation whose
connection cannot be made is not added.
Source code in src/py1815/master/api.py
remove
async
¶
The operations¶
py1815.master.operations ¶
The operations a master offers, whatever carries them.
One list of operations with one implementation each, so that a master wired straight to a session and a master on a socket cannot drift apart. What differs between them is only how an exchange is carried out, which is the one method a subclass supplies; over a socket that method is a coroutine, and the operations return what it returns.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Steps ¶
Bases: Protocol
Requests decided one at a time, each from the answers to those before it.
A select and its operate, and a time synchronization, are carried out this way, so every carrier follows one rule for when the second request is sent.
Source code in src/py1815/master/operations.py
next ¶
Operations ¶
Bases: Generic[ResultT, OperatedT, SyncedT]
What a master can be asked to do.
Source code in src/py1815/master/operations.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
operate ¶
operate(
*,
binary_outputs: Mapping[Any, Any] | None = None,
analog_outputs: Mapping[Any, Any] | None = None,
mode: Mode | str = controls.Mode.DIRECT,
variation: int | None = None,
) -> OperatedT
Command outputs, named by index, in one request.
A binary output is given true or false for a latch, or an operation
by name: pulse_on, trip, close. An analog output is given
a number. Binary outputs are sent first, and each in the order given.
mode is direct for a direct operate, select for a select
followed by an operate, and direct_no_ack for a direct operate the
outstation does not answer. After a select the operate is sent only if
the outstation echoed every control unchanged and accepted each one.
The result has a status for each control, and says whether the outstation accepted them all, refused, or never said. Nothing is sent twice: a response that does not arrive is reported, and whether to try again is the caller's to decide.
A whole number is sent as an integer and anything else as a float,
unless variation says which. An outstation that scales its points
takes an integer as the transmitted value and a float as the
engineering one.
Source code in src/py1815/master/operations.py
write_time ¶
Set the outstation's clock: milliseconds since the epoch, UTC, or now.
Source code in src/py1815/master/operations.py
synchronize_time ¶
Set the outstation's clock by one of the procedures of IEEE 1815-2012 10.3.3.
lan records the current time at the outstation and then writes
the time the master sent that request. non_lan measures the delay
and writes the master's time plus the delay. The write is sent only
if the first request was answered without an error indication, and
neither request is ever sent again. See :mod:py1815.master.timesync.
Source code in src/py1815/master/operations.py
broadcast ¶
broadcast(
function: FunctionCode,
body: bytes = b"",
*,
address: Broadcast | int | str = DEFAULT_BROADCAST,
) -> ResultT
Send a request to a broadcast address: every outstation on the link.
address is no_confirm (0xFFFD), shall_confirm (0xFFFE) or
optional_confirm (0xFFFF), which say whether an outstation asks
for its next response to be confirmed. No outstation answers a
broadcast, so the exchange ends as sent and the indication that one
arrived (IIN1.0) comes in the next response to a request of this
master's own. Nothing is sent again.
Source code in src/py1815/master/operations.py
clear_restart ¶
Clear the restart indication, which a master does once it has seen it.
freeze ¶
Freeze every counter, clearing each as it is frozen if asked.
With respond false the request is one the outstation does not
answer.
Source code in src/py1815/master/operations.py
restart ¶
Ask the outstation to restart: cold or warm.
Source code in src/py1815/master/operations.py
request ¶
Send any request, as a function code and the octets after it.
The escape hatch: whatever this interface does not name yet can still be asked, and the answer comes back decoded as far as it can be.
Source code in src/py1815/master/operations.py
scan ¶
Read by name: integrity, events, class0 to class3, or outputs.
outputs reads binary and analog output status by their groups. It
is not part of an integrity poll: the IEEE 1815.2 profile leaves
output status out of class 0.
Source code in src/py1815/master/operations.py
integrity_poll ¶
enable_unsolicited ¶
Ask the outstation to report the named event classes without being polled.
Source code in src/py1815/master/operations.py
disable_unsolicited ¶
Ask the outstation to stop reporting the named event classes unasked.
Source code in src/py1815/master/operations.py
read ¶
read(
*,
binary_inputs: Indices = None,
binary_outputs: Indices = None,
counters: Indices = None,
frozen_counters: Indices = None,
analog_inputs: Indices = None,
analog_outputs: Indices = None,
) -> ResultT
Read named points, in one request.
Each argument is the indices wanted of one point type, or "all"
for every point of that type. The outstation answers in whichever
variation it sends by default.
Source code in src/py1815/master/operations.py
broadcast_address ¶
Return a broadcast address given by name, by number or as itself.
Source code in src/py1815/master/operations.py
Commands¶
py1815.master.controls ¶
Commands on outputs: what is sent, in what order, and what came of it.
A control is the one request a master makes that it must not get wrong and must not repeat. So this module does three things and nothing else. It turns what a caller asked for into the objects that travel, refusing anything that cannot be carried before a frame is built. It says which request follows which: a select is followed by an operate only when the outstation echoed every control back unchanged and accepted each one. And it reads the answer into a status for each control.
Nothing here retries. A response that never came is reported as one that never came, and whether to send the control again is the caller's to decide, because only the caller knows whether operating twice is harmless.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Mode ¶
Bases: Enum
How a control is sent.
Source code in src/py1815/master/controls.py
Command
dataclass
¶
One control, as it will travel.
Source code in src/py1815/master/controls.py
PointStatus
dataclass
¶
What an outstation said about one control.
Source code in src/py1815/master/controls.py
Operated
dataclass
¶
A control request, or the two of a select and operate, and what came of them.
Source code in src/py1815/master/controls.py
operated
property
¶
Whether a request that operates was sent.
False for a select the outstation refused, after which no operate is sent. True says only that it was sent, not that it was acted on.
accepted
property
¶
Whether the outstation accepted every control: yes, no, or not known.
None when it never said: the request takes no acknowledgment, or the response did not arrive. That is not a refusal, and the output may have been operated.
status ¶
The status of the control on one output, or None if there was none.
Source code in src/py1815/master/controls.py
Plan ¶
The requests of one operation, decided one at a time.
Whatever carries the requests asks what to send next and is told, so a master on a socket and one wired to a session follow the same rule for when a select is followed by an operate.
Source code in src/py1815/master/controls.py
next ¶
The request to make after those already made, or None when there is none.
Source code in src/py1815/master/controls.py
result ¶
What the exchanges made amount to.
Source code in src/py1815/master/controls.py
binary ¶
A command on a binary output.
value is true or false for a latch on or off, one of the names in
:data:OPERATIONS, a mapping with an operation and optionally a
count, on_time_ms and off_time_ms, or a control relay output
block made elsewhere.
Source code in src/py1815/master/controls.py
analog ¶
A command on an analog output.
value is a number, a numeric string, or an analog output made
elsewhere. variation is 1 or 2 for a 32- or 16-bit integer and 3 or 4
for a single or double float; left out, the narrowest that carries the
value is used.
Source code in src/py1815/master/controls.py
commands ¶
commands(
binary_outputs: Mapping[Any, Any] | None = None,
analog_outputs: Mapping[Any, Any] | None = None,
*,
variation: int | None = None,
) -> tuple[Command, ...]
The commands for outputs named by index: binary first, each in the order given.
Source code in src/py1815/master/controls.py
encode ¶
The object headers and objects of a control request, in the order given.
Controls of one group and variation that stand together share a header. Each object is prefixed by its index, in one octet where every index of the header fits and in two where one does not.
Source code in src/py1815/master/controls.py
read_statuses ¶
Each control's status, from the objects a response echoed.
Source code in src/py1815/master/controls.py
An IEEE 1815.2 DER¶
py1815.master.profile ¶
The IEEE 1815.2 DER profile, from the master's side.
py1815.profile resolves the profile's tables into a point map: each point's
name, units, multiplier, range and section, which output enables a function and
which input says it is supported. :class:DerProfile reads that map the other
way round, for a master: it finds a point by its name, groups points by what
they are for, and converts between engineering units and the numbers that
travel. :class:Der puts it in front of an outstation, and
:class:LoopbackDer in front of a :class:~py1815.master.loopback.Loopback.
Every operation is made of the master's own requests: a read is read, and
a write is operate. A write is a control, so it is sent once and never
repeated (D80): a write whose answer did not arrive is reported as not known,
and nothing follows it that depends on it.
Each operation is written once, as a plan: a generator that yields one request at a time and is sent what came of it. A master on a socket awaits each request and one wired to a session does not, so the two cannot come to differ.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Function
dataclass
¶
One DER function, as the tables group its points.
Source code in src/py1815/master/profile.py
Reading
dataclass
¶
What an outstation reported for one point of the profile.
Source code in src/py1815/master/profile.py
value
property
¶
The value in engineering units: the transmitted number through the multiplier.
A whole transmitted number is given to as many decimal places as the multiplier and offset have, so 4224 tenths of a volt reads 422.4 and not the 422.40000000000003 that binary floating point makes of it.
Readings
dataclass
¶
Points read in one request, in the order they were asked for.
Source code in src/py1815/master/profile.py
Setpoint
dataclass
¶
One output written, and what came of it.
Source code in src/py1815/master/profile.py
Written
dataclass
¶
A write of outputs by name, and, when asked, what their mirrors read afterwards.
Source code in src/py1815/master/profile.py
Switched
dataclass
¶
A function enabled or disabled, and what its status input then said.
Source code in src/py1815/master/profile.py
enabled
property
¶
Whether the outstation says the function is enabled. None when it did not say.
Curve
dataclass
¶
The curve the curve block shows, as the outstation reported it.
The points are the numbers that travelled: their scaling depends on the units the curve declares, and the tables give the block no multiplier.
Source code in src/py1815/master/profile.py
CurveWritten
dataclass
¶
A curve written as the profile requires, and read back.
Source code in src/py1815/master/profile.py
FunctionState
dataclass
¶
One function, and what the outstation says of it.
Source code in src/py1815/master/profile.py
Functions
dataclass
¶
Declared
dataclass
¶
One point as a Device Profile document declares it.
Source code in src/py1815/master/profile.py
Comparison
dataclass
¶
What an outstation serves, against what its Device Profile document declares.
Source code in src/py1815/master/profile.py
DerProfile ¶
The point map, read for a master: names, groups, functions and scaling. No I/O.
Source code in src/py1815/master/profile.py
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 | |
point ¶
Return a point by its address, as in AO87, or by its name.
A name is matched whole or by its first sentence, ignoring case and
spacing. The tables give some names to more than one point, an output
and the input that reads it back among them, so a name is looked for
among the inputs first and then among the outputs, or among the
outputs only when outputs is set. A name that still matches more
than one point is refused, with the addresses it could mean.
Source code in src/py1815/master/profile.py
function ¶
Return a function by its key, name, purpose, or the address of its enable output.
Source code in src/py1815/master/profile.py
group ¶
Return the points of a named group: a function, or everything of one purpose.
A purpose is what the tables say a point is for: Nameplate,
Monitoring, Return to Service. A function's group is its own
points, which may be fewer than its purpose names elsewhere.
Source code in src/py1815/master/profile.py
mirror ¶
Return the input that reads an output back, or None when the tables pair none.
Source code in src/py1815/master/profile.py
transmitted ¶
Return an output's value as it travels: a state, or a whole transmitted number.
A binary output takes true or false, 1 or 0, or the name the tables give one of its states. An analog output takes a number in engineering units, divided by its multiplier and rounded to the nearest whole number. A value outside the point's range is refused here, before anything is sent.
Source code in src/py1815/master/profile.py
limits
staticmethod
¶
Return an analog point's range in engineering units, lowest first; None where open.
Source code in src/py1815/master/profile.py
span
staticmethod
¶
Return an analog point's range in engineering units, as text.
Source code in src/py1815/master/profile.py
curve_block ¶
Return the curve block's outputs and the inputs that read them back, in order.
The selector, the four fields, then X and Y of each point. Raises ValueError when the map does not hold the block or its readback.
Source code in src/py1815/master/profile.py
referenced ¶
Return the input that says whether a function names the selected curve, if any.
Survey
dataclass
¶
Which points an outstation reported when asked for everything it serves.
Source code in src/py1815/master/profile.py
Der ¶
Bases: _Operations
The DER profile's operations on an outstation over a socket.
Each operation is a coroutine. One profile operation is under way on an outstation at a time: a curve written by one caller cannot have its selector moved by another between the selector and the points.
Source code in src/py1815/master/profile.py
LoopbackDer ¶
Bases: _Operations
The DER profile's operations on a :class:~py1815.master.loopback.Loopback, at once.
Source code in src/py1815/master/profile.py
address ¶
label ¶
quality ¶
Return what a flag octet says of a value: good, offline, comm_lost, restart.
not_reported for a point the outstation did not report, and
no_flags for a variation that carries no flag octet, which says
nothing either way.
Source code in src/py1815/master/profile.py
read_plan ¶
Read points of the profile, and report each in engineering units.
One request, unless the outstation refuses it whole for a range that holds no point it serves: then each range is read again on its own (D93).
Source code in src/py1815/master/profile.py
carry_out ¶
Return the step that carries out a control plan in one turn.
The carrier's own method is used, so nothing is sent between a select and its operate.
Source code in src/py1815/master/profile.py
write_plan ¶
write_plan(
profile: DerProfile,
values: Mapping[str, Any],
*,
verify: bool = False,
mode: Mode | str = Mode.DIRECT,
variation: int | None = None,
) -> Plan[Written]
Write outputs by name in engineering units, in one request, in the order given.
Everything that can be wrong with the write is refused here, before any
request is made. variation 3 or 4 sends an analog value as a float in
engineering units, which the profile has an outstation take unscaled;
otherwise it travels as the whole transmitted number.
Source code in src/py1815/master/profile.py
switch_plan ¶
switch_plan(
profile: DerProfile,
name: str,
enable: bool,
*,
mode: Mode | str = Mode.DIRECT,
) -> Plan[Switched]
Enable or disable a function, then read the input that reports whether it is enabled.
Source code in src/py1815/master/profile.py
functions_plan ¶
Read which functions the outstation supports, and which are enabled, in one request.
Source code in src/py1815/master/profile.py
curve_plan ¶
curve_plan(
profile: DerProfile,
number: int | None = None,
*,
mode: Mode | str = Mode.DIRECT,
) -> Plan[tuple[Operated | None, Curve]]
Read the curve the curve block shows, selecting curve number first when given.
Selecting a curve writes the selector, so it is a control.
Source code in src/py1815/master/profile.py
write_curve_plan ¶
write_curve_plan(
profile: DerProfile,
number: Any,
*,
type: Any,
x_units: Any,
y_units: Any,
points: Sequence[Sequence[Any]],
mode: Mode | str = Mode.DIRECT,
) -> Plan[CurveWritten]
Write a curve in the order IEEE 1815.2 clause 6.1.3 lays the block out, then read it back.
Three writes: the selector, then the type, the number of points and the units of X and Y, then X and Y of each point. Each is made only when the one before it was accepted, since a field or a point written after a selector the outstation refused would change whichever curve it was already showing. The curve is read back whatever happened.
Source code in src/py1815/master/profile.py
969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 | |
read_device_profile ¶
Return the points a DNP3 Device Profile document declares.
Reads the point lists of the form :mod:py1815.profile.device_profile
writes, schema version 2.12.00. A counter that declares a frozen counter
beside it is two points. A document that declares a document type is
refused, since nothing a Device Profile holds needs one and an entity
declared there is how an XML document is made to expand without bound.
Source code in src/py1815/master/profile.py
1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 | |
survey_plan ¶
Read which points an outstation serves.
A class 0 read, then a read of output status, then a read by range of
every point in wanted that neither returned. An outstation that does
not answer the class 0 read is asked nothing more.
Source code in src/py1815/master/profile.py
compare_plan ¶
Compare what an outstation serves with what its Device Profile document declares.
A class 0 read, then a read of output status, then a read of every declared point neither returned, by range. A point no read returned is absent. Whether each served point is in class 0 is checked against the class 0 read. A point's event class and deadband cannot be read from an outstation, so they are reported as declared.
Source code in src/py1815/master/profile.py
run ¶
Carry out a plan on a master whose requests return at once, as a Loopback's do.
Source code in src/py1815/master/profile.py
run_async
async
¶
Carry out a plan on a master whose requests are awaited, as an Outstation's are.
Source code in src/py1815/master/profile.py
Evaluating an outstation¶
Evaluating an outstation is the guide.
py1815.master.evaluate ¶
Run checks against an outstation, and report how each ended.
What py1815-master evaluate does: connect to an outstation, carry out the
checks of :mod:py1815.master.checks, and print a report with a verdict for
each. A check that writes to the outstation is run only when the run is
allowed to command, and a check that has the master break a protocol rule
only when the run is allowed to deviate. The report can also be written as
JSON, and the traffic as a pcap file in which each check's frames can be
found by number.
From Python:
.. code-block:: python
report = await evaluate(outstation, point_map, commanding=True)
for line in report.lines():
print(line)
:func:evaluate takes a connected :class:~py1815.master.api.Outstation.
:func:evaluate_loopback runs the same checks against a session in this
process, with no socket and no waiting.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Report
dataclass
¶
How every check of a run ended.
Source code in src/py1815/master/evaluate.py
count ¶
describe ¶
Return the report as a JSON-compatible dict.
Source code in src/py1815/master/evaluate.py
render ¶
lines ¶
Return the report as lines of text: a line for each check, then the totals.
Source code in src/py1815/master/evaluate.py
evaluate
async
¶
evaluate(
outstation: Any,
point_map: PointMap | DerProfile,
*,
checks: Sequence[Check] | None = None,
commanding: bool = False,
deviating: bool = False,
settle: float = DEFAULT_SETTLE,
curves: int | None = None,
device_profile: str | None = None,
capture: Any = None,
pause: Callable[
[float], Awaitable[None]
] = asyncio.sleep,
on_result: Callable[[Result], None] | None = None,
) -> Report
Run checks against a connected outstation, and return the report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
outstation
|
Any
|
A connected :class: |
required |
point_map
|
PointMap | DerProfile
|
The profile's points, as the outstation is expected to serve them. |
required |
checks
|
Sequence[Check] | None
|
The checks to run, in order. Defaults to the whole catalog. |
None
|
commanding
|
bool
|
Run the checks that write to the outstation. They change its settings, enable and disable its functions, and stop and start it. |
False
|
deviating
|
bool
|
Run the checks that have the master break a protocol rule on purpose. They send the outstation frames it has to discard and withhold confirmations it asked for. |
False
|
settle
|
float
|
Seconds to wait for the outstation to reach a state it was commanded to. |
DEFAULT_SETTLE
|
curves
|
int | None
|
How many curves the outstation stores. Found by selecting each in turn when not given. |
None
|
device_profile
|
str | None
|
The text of the outstation's Device Profile document. |
None
|
capture
|
Any
|
The :class: |
None
|
pause
|
Callable[[float], Awaitable[None]]
|
Awaited with the seconds of each wait. |
sleep
|
on_result
|
Callable[[Result], None] | None
|
Called with each result as its check ends. |
None
|
Raises :class:~py1815.master.bench.NoAnswer when the outstation does
not answer the first read.
Source code in src/py1815/master/evaluate.py
evaluate_loopback ¶
evaluate_loopback(
loopback: Any,
point_map: PointMap | DerProfile,
*,
checks: Sequence[Check] | None = None,
commanding: bool = False,
deviating: bool = False,
settle: float = DEFAULT_SETTLE,
curves: int | None = None,
device_profile: str | None = None,
advance: Callable[[float], None] | None = None,
) -> Report
Run checks against a :class:~py1815.master.loopback.Loopback, and return the report.
advance is called with the seconds of each wait, so the caller can
move a simulated device forward. Nothing waits in real time.
Source code in src/py1815/master/evaluate.py
catalog_lines ¶
Return the catalog as lines of text: each check's identifier, set and title.
Source code in src/py1815/master/evaluate.py
run ¶
Evaluate an outstation as args and config say. Return the exit status.
0 when no check failed, 1 when one did, and 2 when the run could not be made: a setting that cannot be used, no tables, or no answer.
Source code in src/py1815/master/evaluate.py
py1815.master.bench ¶
Checks of an outstation: what one is, how it is run, and what it reports.
A check is a short named procedure with a verdict. It is written once, as a
plan that yields one request at a time, so the same check runs against an
outstation on a socket and against a session in the same process. A check
fails by raising :class:Failed with what the outstation did, and is not
applicable when the outstation does not serve what it needs.
:class:Bench is what a check works with: the profile's point map, the
points the outstation serves, and the requests a check is made of. Each
request that is not answered fails the check. Nothing here knows any
particular check; the catalog is :mod:py1815.master.checks.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Verdict ¶
Bases: Enum
How a check ended.
Source code in src/py1815/master/bench.py
Failed ¶
Unanswered ¶
NotApplicable ¶
NoAnswer ¶
Pause
dataclass
¶
A step of a plan that waits and sends nothing.
Source code in src/py1815/master/bench.py
Check
dataclass
¶
One named procedure.
Source code in src/py1815/master/bench.py
Result
dataclass
¶
How one check ended.
Source code in src/py1815/master/bench.py
describe ¶
Return the result as a JSON-compatible dict.
Source code in src/py1815/master/bench.py
Bench ¶
What a check works with.
Every method that sends a request is a plan: call it with yield from.
A request the outstation does not answer raises :class:Unanswered.
Source code in src/py1815/master/bench.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 | |
begin ¶
Forget what the last check noted, counted and left to be put back.
note ¶
require
staticmethod
¶
afterwards ¶
Have plan carried out when the check ends, however it ends.
A check uses this to put back what it changed. The plans are carried out last one first.
Source code in src/py1815/master/bench.py
put_back ¶
Carry out what the check left to be done afterwards.
Source code in src/py1815/master/bench.py
supported ¶
supported_in ¶
Return the points of one kind the outstation serves among indices.
readback ¶
Return the input that reads an output back, or None when none is served.
Source code in src/py1815/master/bench.py
read ¶
Read points in one request.
Source code in src/py1815/master/bench.py
obtain ¶
Read one point and require the outstation to report it.
Source code in src/py1815/master/bench.py
online ¶
Read a point and require it to be ONLINE with no other quality flag.
A variation that carries no flags says the same thing.
Source code in src/py1815/master/bench.py
offline ¶
Read a point and require it to carry a value and no quality flag at all.
Source code in src/py1815/master/bench.py
state ¶
value ¶
Return an analog point's value in engineering units, required to be ONLINE.
raw ¶
Return an analog point's value in engineering units, whatever its flags say.
set ¶
Direct operate one analog output with a transmitted value. Return the status.
Source code in src/py1815/master/bench.py
latch ¶
Select and then operate a latch of one binary output. Return the status.
The status is the select's when the outstation refused the select.
Source code in src/py1815/master/bench.py
accepted ¶
Fail the check unless the outstation accepted a control.
refused ¶
Fail the check if the outstation accepted a control it is to refuse.
A refusal with a status other than usual is noted and not failed:
which status refuses a control is the outstation's to choose.
Source code in src/py1815/master/bench.py
write_curve ¶
write_curve(
number: int,
*,
type: int,
x_units: int,
y_units: int,
points: Sequence[Sequence[int]],
) -> Plan[CurveWritten]
Write a curve in the profile's three steps and read it back.
Source code in src/py1815/master/bench.py
compare ¶
Compare what the outstation serves with a Device Profile document.
Source code in src/py1815/master/bench.py
deviate ¶
Have the master break the named protocol rules until the check ends.
The master's deviations are put back as the check found them when the
check ends, or sooner by :meth:conform.
Source code in src/py1815/master/bench.py
conform ¶
isolate ¶
Begin a check that deviates: hold the master, with every deviation off.
A deviation someone left on would damage the well-formed requests the check compares against, so each is turned off until the check ends. A master that sends on its own is held as well, so that no automatic task, repeated scan or other caller sends a request while one of the check's deviations is on, or between two of its steps. Returns what the master's own hold returns, to be awaited where it is awaitable.
Source code in src/py1815/master/bench.py
restore ¶
Put a master back as the check found it. Sends nothing.
The deviations are set as they were before the check, and a master the check held is let go.
Source code in src/py1815/master/bench.py
probe ¶
Send one small read, and return whether the outstation answered it.
Source code in src/py1815/master/bench.py
unanswered ¶
Send one small read, and fail the check if the outstation answers it.
answers ¶
Send small reads until one is answered, and fail the check if none of reads is.
Source code in src/py1815/master/bench.py
events ¶
Read the events of every class, and return the exchange.
confirming ¶
Return whether the master sends a confirmation when a response asks for one.
pause ¶
eventually ¶
Carry out attempt until it does not fail, for up to the settling time.
For what an outstation takes time to do: a read that finds it not yet done is made again every half second. The last failure stands. A request that is not answered is not made again.
Source code in src/py1815/master/bench.py
becomes ¶
Wait until a binary point reports state ONLINE, or fail with said.
Source code in src/py1815/master/bench.py
name ¶
run_plan ¶
run_plan(
bench: Bench,
checks: Sequence[Check],
*,
commanding: bool,
deviating: bool = False,
on_result: Callable[[Result], None] | None = None,
) -> Plan[tuple[Result, ...]]
Carry out checks in order, and return how each ended.
The run begins by reading which points the outstation serves, with every
deviation of the master off. A check
that writes is not run unless commanding is set, and a check that has
the master break a protocol rule is not run unless deviating is set.
When the connection is lost the check under way fails and the rest are
not run. on_result is called with each result as its check ends.
Raises :class:NoAnswer when the outstation does not answer the first read.
Source code in src/py1815/master/bench.py
drive ¶
Carry out a plan on a master whose requests return at once, as a Loopback's do.
pause is called with the seconds of each wait in the plan.
Source code in src/py1815/master/bench.py
drive_async
async
¶
Carry out a plan on a master whose requests are awaited, as an Outstation's are.
pause is awaited with the seconds of each wait in the plan.
Source code in src/py1815/master/bench.py
py1815.master.checks ¶
The checks py1815-master evaluate runs against an outstation.
The DER profile set carries out EPRI's Test Procedure for Validating DNP
Application Note AN2018-001 in Distributed Energy Resources (3002016144) the
way a controlling station would: read a function's points, write its
settings and read them back, enable and disable it, and edit curves through
the block all curves share. Each check is named for the identifier the
report gives its procedure: MON-001 is the monitoring procedure. The
report is EPRI's and is not reproduced; the steps are described here in our
own words. Where the report and IEEE 1815.2 differ, these follow the standard.
The procedures carry no point list of their own. Which input reads an output back, which points make up a function and which input says it is supported come from the profile tables, and a point counts as supported when the outstation reports it. A function the outstation says it does not support is checked for that: it cannot be enabled and none of its points are served.
A check leaves the outstation as it found it where that is one command: a switch, a permission, started or stopped. Settings, curves and which functions are enabled are left as the check last wrote them, with every function it enabled disabled again.
The deviation set has the master break a protocol rule on purpose
(:mod:py1815.master.deviations) and requires what the standard requires of
the outstation: silence for a frame or a segment it has to discard, and
events kept until the right confirmation arrives. These checks write nothing
to the outstation. They are run only when the run is allowed to deviate.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Check
dataclass
¶
One named procedure.
Source code in src/py1815/master/bench.py
SampleCurve
dataclass
¶
A valid curve of one type, to write to an outstation.
Source code in src/py1815/master/checks.py
valid_values ¶
Return two transmitted values inside the range the tables give an output.
They differ unless the range holds one value.
Source code in src/py1815/master/checks.py
check_settings ¶
check_settings(
bench: Bench,
outputs: Sequence[Point],
which: int,
*,
enabled: bool = True,
) -> Plan[None]
Write one set of valid values to analog outputs, and read each back.
The value read back is compared in engineering units, since the tables
scale an output and the input that reads it back independently. With
enabled false the readbacks belong to a function that is disabled:
they are required to carry the value and no ONLINE flag.
Source code in src/py1815/master/checks.py
monitoring ¶
Check that every served point of the system meter is ONLINE and in range.
Source code in src/py1815/master/checks.py
alarms ¶
Check that every served alarm is ONLINE, and note the ones that are raised.
Source code in src/py1815/master/checks.py
operating_states ¶
Check that every served operating state is ONLINE and that the states agree.
Source code in src/py1815/master/checks.py
connect_and_disconnect ¶
Check that the connect settings read back and the switch follows its command.
Source code in src/py1815/master/checks.py
service ¶
Check that the service settings read back, then stop and start the DER.
Source code in src/py1815/master/checks.py
start_without_permission ¶
Check that a start is refused without permission and the DER stays stopped.
Source code in src/py1815/master/checks.py
stop_without_permission ¶
Check that a stop is refused without permission and the DER stays started.
Source code in src/py1815/master/checks.py
curve_reference ¶
Check that the referenced indicator is set only for a curve a function names.
Source code in src/py1815/master/checks.py
curve_that_does_not_exist ¶
Check that a curve beyond the last one stored cannot be selected.
Source code in src/py1815/master/checks.py
curve_locking ¶
Check that a curve named by an enabled function cannot be edited.
Source code in src/py1815/master/checks.py
curve_moved_under_function ¶
Check that an enabled function can move to another curve, freeing the first.
Source code in src/py1815/master/checks.py
curve_type_mismatch ¶
Check that a function cannot name a curve of a type it does not follow.
Source code in src/py1815/master/checks.py
curve_not_defined ¶
Check that a function cannot name a curve with no type, or one that does not exist.
Source code in src/py1815/master/checks.py
mode_check ¶
Return the mode procedure for the function a binary output enables.
Source code in src/py1815/master/checks.py
device_profile ¶
Check that the outstation serves the points its Device Profile declares, and no others.
Source code in src/py1815/master/checks.py
not_answered ¶
not_answered(
deviation: str,
what: str,
*,
reads: int = RECOVERY_READS,
) -> Callable[[Bench], Plan[None]]
Return the check that a request sent with one deviation on draws no answer.
A well-formed read is answered first, so that the silence that follows is
about the deviation. With the deviation on, a read has to go unanswered.
With it off again, one of the next reads reads has to be answered, so
the silence was about that one request and not a receiver that stopped.
What makes silence the required answer depends on the deviation:
- A frame that fails a checksum, is cut short, or claims a length it does not carry is discarded by the data link layer and nothing is sent for it (IEEE 1815-2012 9.2.4.3). The invalid CRC procedure of section 6.6.2 of the IED Certification Procedure tests the two checksum cases.
- A segment that is not a first segment and follows no series in progress is discarded, and a new first segment abandons a series that did not end (IEEE 1815-2012 8.2.4, Table 8-1, as TB2013-003 restates it).
Source code in src/py1815/master/checks.py
events_kept ¶
Return the check that events are kept until the right confirmation arrives.
The events are read with one confirmation deviation on, so the outstation does not receive the confirmation it asked for. Read again, it has to report every one of them again (IEEE 1815-2012 Table 6-1, and 4.6.6 rule 9). With the deviation off they are read once more and confirmed, and a read after that must not report them.
Source code in src/py1815/master/checks.py
1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 | |
select ¶
Return the checks named, in the catalog's order, or all of them for None.
A name is a check's identifier or a group, in any case. Raises ValueError for a name that is neither.
Source code in src/py1815/master/checks.py
Deviations¶
Misbehaving on purpose is the guide.
py1815.master.deviations ¶
Named ways the master breaks a protocol rule on purpose, for testing an outstation.
An evaluation tool has to be able to do the wrong thing and say exactly what it did, so a test can see whether an outstation rejects what the standard says it must. The certification procedures already send malformed frames to a session in this process; this module gives each kind of misbehavior one name and one implementation, usable over a socket against a device on a bench as well.
A deviation is applied to the octets the association produced, on their way to
the channel. The association's own code has no branch that does the wrong
thing, so the conformant path cannot be bent by a flag left on: with every
deviation off, :meth:Deviations.outbound returns what it was given,
unchanged. Each is off until it is turned on, and turning one on changes only
what the master sends, never what it expects back.
The frame-level deviations work on whole link frames, so they are built and corrupted here rather than reaching into the association or the link layer. Nothing here does I/O: a transform takes octets and returns octets, and is tested against frames written out by hand.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Deviations
dataclass
¶
The deviations a master has turned on, and the transform they make.
Off by default. :meth:outbound is called with the octets the association
produced and returns the writes to make in their place: the same octets
when nothing is on, a corrupted frame, the request twice, or nothing at
all.
Source code in src/py1815/master/deviations.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 | |
names
classmethod
¶
Every deviation's name, in the order they are listed.
any_on ¶
on ¶
set ¶
Turn a deviation on or off by name. Raises ValueError for an unknown name.
Source code in src/py1815/master/deviations.py
clear ¶
describe ¶
outbound ¶
Return the writes to make in place of octets.
kind is :data:REQUEST or :data:CONFIRM. The list is empty when
the master is to send nothing, and is [octets] with every
deviation off. A deviation that changes a frame returns each link
frame as a write of its own, so a trace can keep the changed frame
apart from the others. A request deviation changes the first frame,
except endless_series, which changes the last, and
second_master_address, which changes every frame. A confirmation
deviation acts on each frame that carries an application confirmation
of the kind it names, solicited or unsolicited, and leaves the other
kind and a link-layer reply as built. A repeated request holds its
writes twice. A confirmation held back by late_confirmation
follows the next request.
Source code in src/py1815/master/deviations.py
corrupt_header_crc ¶
Flip the frame's header checksum, so the outstation cannot trust the header.
corrupt_body_crc ¶
truncate ¶
overstate_length ¶
Make the length octet describe more user data than the frame carries.
The length octet is raised and the header checksum recomputed, so the outstation reads a length its header checksum vouches for, then finds fewer octets than it was promised. A length octet already at its highest value cannot be raised, so that frame loses its last data block and keeps its header as built.
Source code in src/py1815/master/deviations.py
break_transport_sequence ¶
Change the transport sequence number, so the segment lands out of order.
Source code in src/py1815/master/deviations.py
orphan_segment ¶
Clear the first-segment bit, so a lone segment looks like a continuation.
Source code in src/py1815/master/deviations.py
endless_series ¶
Clear the final-segment bit, so the series the segment belongs to never ends.
The outstation is left assembling a fragment whose last segment does not come. A new first segment ends that (IEEE 1815-2012 8.2.4, Table 8-1).
Source code in src/py1815/master/deviations.py
second_master_address ¶
Change the frame's source address to another master's, header checksum recomputed.
The address is the next one above the master's own that is a station address and is not the outstation's. Whether an outstation answers a master it was not told of is its own choice, which its Device Profile states, so a response to this is addressed to that other master.
Source code in src/py1815/master/deviations.py
wrong_application_sequence ¶
Change the application sequence number of the fragment the frame carries.
For a confirmation, this is confirming a sequence the outstation did not send, which must not retire the events it was waiting to have confirmed.
Source code in src/py1815/master/deviations.py
What is done unasked¶
py1815.master.tasks ¶
Automatic master tasks: the requests a master sends without being asked.
A conformant master runs a startup sequence when it connects, clears the restart indication, writes the time when the outstation needs it, and polls for events when a response indicates some are waiting. Each of these is a task that can be disabled.
:class:Housekeeper decides which requests to send. It is given the internal
indications (IIN) of each response and returns the next request. It does no
I/O, so the connection owner sends the requests and the logic can be tested
without a socket.
Loop prevention: a task is never triggered without bound by the responses to its own requests.
- Restart and need-time are edge-triggered. They fire when the bit appears and not again until it has cleared and reappeared. The integrity poll that follows a restart is made once for each appearance, including when the restart was first reported by an integrity poll.
- An overflow poll fires on any response that carries the bit, except the response to the integrity poll itself.
- An event poll fires on any response that says events are waiting. When the
response to a poll still says so, the outstation held some back, and they
are fetched at once by another event poll, up to
:attr:
Tasks.event_follow_upsin a row. The count starts again only when a poll's response says nothing is waiting.
An outstation that never clears a bit therefore costs a bounded number of extra requests per response, and the follow-up polls are spent once until it does.
No task operates an output.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Tasks
dataclass
¶
Which automatic tasks are enabled.
Source code in src/py1815/master/tasks.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
none
classmethod
¶
Return a configuration with every task disabled.
Source code in src/py1815/master/tasks.py
changed ¶
Return a copy with the settings in given applied.
Raises ValueError for an unknown task name or a value of the wrong
type.
Source code in src/py1815/master/tasks.py
reading_only ¶
describe ¶
Return the settings as a JSON-compatible dict.
Source code in src/py1815/master/tasks.py
Step ¶
Bases: NamedTuple
One request to send, and the task it belongs to.
A step with a plan is a time synchronization of two requests:
function and body are its first, and the owner carries out the
plan instead.
Source code in src/py1815/master/tasks.py
Housekeeper ¶
Decide which automatic requests to send, based on response indications.
Call :meth:connected when a connection is made, :meth:saw with the
indications of every response, and :meth:next to get each request
that is due.
Source code in src/py1815/master/tasks.py
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | |
connected ¶
Reset state for a new connection and queue the startup sequence.
Source code in src/py1815/master/tasks.py
saw ¶
Queue the tasks that a response's indications call for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indications
|
IIN | None
|
The response's IIN, or None if no response arrived. |
required |
answering
|
tuple[FunctionCode, bytes] | None
|
The request that was answered, as (function code, request body), or None for an unsolicited response. Used to avoid re-triggering a poll from its own response. |
None
|
Source code in src/py1815/master/tasks.py
next ¶
Return the next request to send, or None when nothing is due.
In one process¶
py1815.master.loopback ¶
A master wired straight to a session, in one process.
No socket, no thread and no waiting: a request is handed to the session, what the session returns is handed to the association, and so on until neither has anything to say. With one injected clock shared by the two, a test moves time itself.
Automatic tasks are off unless tasks is given. With tasks,
:meth:Loopback.start runs the startup sequence, and pending tasks run after
each request and each :meth:Loopback.listen.
This is the two halves of the library talking to each other. It is the fastest
way to exercise either, and it is not evidence that either reads the standard
correctly, since they share the layers below them. See docs/planning/MASTER.md.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Loopback ¶
Bases: Operations[Exchange, Operated, Synchronized]
A master association and a session, each handed what the other sent.
Source code in src/py1815/master/loopback.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
start ¶
Run the startup tasks, as on a new connection, and return their exchanges.
listen ¶
Ask the session for what it would send unasked, and take it.
What an owner on a socket does whenever the outstation speaks. Call it after the outstation has recorded events, or after moving the clock past a retry.
Source code in src/py1815/master/loopback.py
The association¶
One conversation with one outstation, with no I/O: octets in, octets out.
py1815.master.association ¶
One master association: octets in, octets out.
The mirror of :mod:py1815.session. It holds what is true of a conversation
with one outstation, seen from the master's end: the request that is
outstanding, the fragments of its response so far, the sequence numbers of the
two series, and what arrived that nobody asked for. It does no I/O and starts
no timer. Its owner hands it the octets that arrived and writes the octets it
returns, and time is a clock that is read when a method is called.
That is what lets a test drive it against a :class:~py1815.session.Session
in one process, through as much protocol time as it likes, with no socket and
no sleep.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Busy ¶
Bases: RuntimeError
A request was made while another was still waiting for its response.
Misuse and not a protocol outcome: one association carries one request at a time, and its owner is what makes a second one wait.
Outcome ¶
Bases: Enum
How an exchange ended.
Source code in src/py1815/master/association.py
Exchange
dataclass
¶
One request and everything that came back for it.
A timeout and an error indication are results, not exceptions: what an outstation did with a request is the thing a caller asked to know.
Source code in src/py1815/master/association.py
Unsolicited
dataclass
¶
An unsolicited response, as received.
Source code in src/py1815/master/association.py
MasterAssociation ¶
The master's side of one conversation with one outstation.
Driven by three calls. :meth:request returns the octets of a request to
send. :meth:receive takes what arrived and returns what to send back,
which is a confirmation or nothing. :meth:expire is called when the time
:meth:expires_after gave has passed, and ends an exchange that is not
going to finish, after :meth:retry has said whether a read is sent again
instead. A finished exchange is collected with :meth:take, and
unsolicited responses with :meth:take_unsolicited.
Source code in src/py1815/master/association.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 | |
request ¶
request(
function: FunctionCode,
body: bytes = b"",
*,
broadcast: Broadcast | None = None,
) -> bytes
Begin an exchange, and return the octets that carry its request.
With broadcast the request goes to that broadcast address and not
to the outstation's own. No outstation answers a broadcast (IEEE
1815-2012 4.5.1), so the exchange ends at once as sent, whatever the
function code.
Raises :class:Busy while another request is outstanding, and while
a finished exchange has not been collected with :meth:take: the
result of one request is not overwritten by the next.
Source code in src/py1815/master/association.py
confirm ¶
The octets that confirm a response fragment, for an owner that turned automatic confirmation off.
Source code in src/py1815/master/association.py
receive ¶
Take octets from the outstation, and return octets to send back.
expires_after ¶
Seconds until the outstanding request times out, or None if none is.
retry ¶
Send the outstanding request again if it may be, and return its octets.
It may be when its time has passed with nothing received, it is a
read, and it has retries left (read_retries). It is sent under the
same sequence number with the same octets (IEEE 1815-2012 4.3 rule
16), and waits a whole response timeout again. A read whose response
has begun to arrive is not sent again, since the outstation is
answering it.
Return no octets when the request is not sent again; the owner then
calls :meth:expire. With at_once the time is not waited for: for
an owner that knows nothing more is coming, as :meth:give_up says.
Source code in src/py1815/master/association.py
expire ¶
End the outstanding exchange if its time has passed. Says whether it did.
Source code in src/py1815/master/association.py
give_up ¶
End the outstanding exchange now, as a timeout. Says whether one was.
For an owner that knows nothing more is coming without waiting to find out: one wired straight to a session in the same process, where an outstation that has not answered by the time the call returns is not going to.
Source code in src/py1815/master/association.py
abandon ¶
End the pending exchange as abandoned. Return True if there was one.
Call this when the caller cancels a request that has already been sent. A late response is then dropped and not confirmed. Without this the association stays busy and rejects every later request.
Source code in src/py1815/master/association.py
take ¶
take_unsolicited ¶
The unsolicited responses received since this was last called.
connection_reset ¶
Forget what a dead connection left behind.
Half a frame and half a fragment mean nothing to the connection that replaces the one they arrived on. A request that was outstanding is ended as abandoned: its response, if one was coming, went with the connection. The sequence numbers carry on, since they belong to the association and not to the socket.
Source code in src/py1815/master/association.py
The store¶
py1815.master.store ¶
What a master has been told: the last value of every point, and the events.
A store is filled from the objects of responses, in the order they arrived, and is asked afterwards. It knows nothing of how the objects were obtained, so a poll, a read and an unsolicited response all feed it the same way.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
PointValue
dataclass
¶
The last thing an outstation said about one point.
Source code in src/py1815/master/store.py
Store ¶
The last value of every point an outstation has reported.
Source code in src/py1815/master/store.py
events
property
¶
The events received, oldest first, up to the capacity.
events_since ¶
Return the events received after events_received stood at received.
Only those still held: an event dropped for capacity or cleared is not returned.
Source code in src/py1815/master/store.py
apply ¶
Take the objects of a response, in the order they were sent.
The order matters and is the outstation's: an integrity poll asks for events before static data so that a present value is not overwritten by an older change, and applying objects in the order sent is what honors that.
Source code in src/py1815/master/store.py
get ¶
points ¶
Every point of one type that has been reported, by index, in order.
Source code in src/py1815/master/store.py
The trace¶
py1815.master.trace ¶
The record of an exchange: every frame, with what it says.
A trace is what a person reads to see what two stations said to each other. Each frame that was sent or received is kept with its time and its direction, its octets, and a reading of them one layer at a time: the data link, the transport function, and, on the frame that completes a fragment, the application layer.
The reading is for people. Nothing in the master acts on it, so a frame it describes badly is a display fault and never a protocol one.
A trace also knows which connection each frame crossed and the addresses of
its two ends, so it can be exported as a pcap file (:meth:Trace.capture) or
written to one as it is recorded (:class:Recorder), for a dissector to read.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Entry
dataclass
¶
One frame, as sent or received.
Source code in src/py1815/master/trace.py
Trace ¶
Frames, in the order they crossed the wire.
Source code in src/py1815/master/trace.py
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
record ¶
Take octets that were sent or received, and keep each frame in them.
Received octets arrive split wherever the network split them, so a frame is recorded when its last octet has arrived and not before.
Source code in src/py1815/master/trace.py
record_write ¶
Record one write this master made, whether or not it is valid frames.
A deviation can make the master write a frame that no reader accepts:
a checksum that does not verify, or fewer octets than the length octet
describes. :meth:record reads octets the way a receiver does, so it
drops such a frame, and holds a short one and reads the next write as
the rest of it. A write that is not whole valid frames is kept here as
one entry, with the octets as written and what is wrong with them, so
the trace and a capture made from it show what crossed the wire.
Source code in src/py1815/master/trace.py
since ¶
clear ¶
reset ¶
Start a new connection: forget half a frame and half a fragment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
local
|
Address | None
|
This master's address and port on the new connection. |
None
|
peer
|
Address | None
|
The outstation's address and port. |
None
|
Source code in src/py1815/master/trace.py
endpoints ¶
Return this master's and the outstation's address on a connection.
Either is None where it is not known: for frames recorded before any connection, and for a connection whose addresses were not given.
Source code in src/py1815/master/trace.py
capture ¶
Return the frames kept as a pcap file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
after
|
int
|
Only frames whose id is greater. |
0
|
port
|
int
|
The outstation's port, for a connection whose addresses are not known. |
DNP3_PORT
|
Source code in src/py1815/master/trace.py
Recorder ¶
Write a trace's frames into a capture, one TCP connection per connection.
Give :meth:record each entry in order, or add it to the trace's
listeners to write frames as they are recorded. A connection's
handshake is written before its first frame, and its FIN exchange when a
frame of the next connection arrives or :meth:close is called, at the
time of its last frame.
A frame is one TCP segment. An address that is not IPv4, or that is not
known, is written as 127.0.0.1; a port that is not known is port
for the outstation and one from 41000 up for the master.
Source code in src/py1815/master/trace.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 | |
record ¶
Write one entry, opening a new connection first when it crossed one.
Before each entry the capture may start a new file. A connection that was open in the old file then starts again with a handshake in the new one, so each file reads on its own. The old file has no FIN for it.
Source code in src/py1815/master/trace.py
close ¶
Write the FIN exchange of the open connection, if there is one.
A connection opened in a file the capture has since rotated away from is forgotten instead: its FIN packets would land in the new file with no handshake before them.
Source code in src/py1815/master/trace.py
iin_names ¶
The indications that are set, by name, first octet first.
The service¶
py1815.master.service ¶
The master as a service: its operations as JSON, for a caller in another process.
One table of operations, each taking a JSON object and returning one. Two ways in carry the same messages: a line of JSON at a time over a local TCP socket, for a test rig, and HTTP for a browser, where a request is a POST and what happens unasked arrives as server-sent events. Neither needs anything the standard library does not have.
The console speaks only this. Whatever it can do, a script can do with the same words.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
Condition ¶
A condition wait_for is given as JSON, made a test of the store.
value holds once a point has been reported, and has the value named
when one is: equals, within tolerance of it, at_least or
at_most. event holds once an event arrives after the wait began,
of the type and index named when they are. indication holds once the
indication named is set, or clear when set is false.
Source code in src/py1815/master/service.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
ControlNotAllowed ¶
BadRequest ¶
Service ¶
A master, and the operations it answers as JSON.
Source code in src/py1815/master/service.py
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 | |
operations
property
¶
The names of the operations, as a message's op may give them.
handle
async
¶
Answer one message. Never raises: what went wrong is the answer.
Source code in src/py1815/master/service.py
subscribe ¶
A queue that receives everything that happens, until unsubscribed.
Source code in src/py1815/master/service.py
publish ¶
Send an event to every subscriber.
A subscriber that has fallen SUBSCRIBER_QUEUE updates behind has its waiting updates replaced by one lost event that says how many were dropped, and stays subscribed. It can then reload what it shows, and memory stays bounded however long it lags.
Source code in src/py1815/master/service.py
set_names ¶
Give an outstation's points names, for a caller that knows its map.
set_profile ¶
Give an outstation the profile it is meant to serve.
The whole map, and not only the points the outstation reports. That is what lets a caller see the difference: a point of the profile the outstation has never reported is one it does not implement, or one nobody has read yet, and a master cannot tell which from silence.
Source code in src/py1815/master/service.py
set_device_profile ¶
Give an outstation its Device Profile document.
der.compare compares what the outstation serves with it, and
der.document lays it out for reading.
Raises ValueError for a document whose point lists cannot be read.
Source code in src/py1815/master/service.py
attach ¶
Have what happens to an outstation published to subscribers.
Source code in src/py1815/master/service.py
close
async
¶
Close every connection, then end each connection in the capture file and close it.
Source code in src/py1815/master/service.py
LineServer ¶
The service a line of JSON at a time, over a local TCP socket.
For a test rig. A request is one JSON object on one line, and so is its
response. A connection that sends {"op": "subscribe"} is also sent
what happens unasked, each as a line with an event field.
Source code in src/py1815/master/service.py
1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 | |
HttpServer ¶
The service over HTTP, and the console's files.
POST /api/<operation> takes an operation's parameters and returns its
answer, and POST /api takes a whole message that names its operation.
GET /events is a stream of server-sent events. Everything else is a
file of the console, among them /openapi.json, which describes the
routes.
It listens on this machine unless told otherwise, and told otherwise it requires a token of every request to the service. A request is refused unless it comes from a page this server served: a browser will carry a request from any page to a local port, and a tool pointed at real equipment must not take one.
Source code in src/py1815/master/service.py
1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 | |
url
property
¶
Where to open the console, with the token when there is one.
An address that means "every interface" is not one a browser can go to, so this machine's own name stands in for it.
split_enumeration ¶
A point's name without the values it lists, and the values apart.
The profile's tables write an enumerated point's values into its name:
Curve Type. Enumeration: <0> Curve is not defined <2> Volt-Var. That
is the right thing to have and the wrong thing to print in a column, so
this takes it apart: ("Curve Type", [{"value": "0", "name": "Curve is
not defined"}, ...]). A value may be a number, a range (11-255) or
a number and up (99+), as the tables have them.
A name that lists fewer than two values is returned whole, with None. A
value written in passing, as in Default is <3>, names nothing after
it and is not an entry: it is left in the name, without its brackets.
Source code in src/py1815/master/service.py
flag_names ¶
A flag octet as the names of the bits set, or None where there is none.
Source code in src/py1815/master/service.py
describe_point ¶
Describe a point of the profile: where it is, its name, its units and its range.
Source code in src/py1815/master/service.py
describe_reading ¶
Describe one point as an outstation reported it, in engineering units.
Source code in src/py1815/master/service.py
describe_check ¶
Return a check of the catalog as a JSON-compatible dict.
Source code in src/py1815/master/service.py
Configuration¶
py1815.master.config ¶
The master's configuration: every setting in one JSON document.
A configuration has its top-level settings, an evaluate object with the
settings of py1815-master evaluate, a defaults object with the
settings shared by every outstation, and a list of outstations. An
outstation entry needs a name and a host; any other setting it gives
overrides the default.
.. code-block:: json
{
"allow_control": false,
"defaults": {"reconnect": 5, "repeat": {"integrity": 30}},
"outstations": [
{"name": "lab", "host": "192.0.2.10"},
{"name": "bench", "host": "192.0.2.11", "port": 20001, "manual": true}
]
}
py1815-master config prints the complete document, with every default
filled in, for editing. console, serve and evaluate load one with
--config FILE.
Unknown settings and values of the wrong type are errors that name where they are, so a typing mistake is reported instead of ignored.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
ConfigError ¶
OutstationConfig
dataclass
¶
The settings of one outstation. name and host are required.
Source code in src/py1815/master/config.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
changed ¶
Return a copy with the settings in given applied.
tasks, repeat and tls are merged: a setting they leave out
keeps its current value. tls given as null is plain TCP.
Source code in src/py1815/master/config.py
output_interval ¶
Seconds between reads of output status, with with_integrity resolved.
Source code in src/py1815/master/config.py
add_params ¶
Return the parameters of the service's add operation for this outstation.
Source code in src/py1815/master/config.py
describe ¶
Return every setting as a JSON-compatible dict.
Source code in src/py1815/master/config.py
EvaluateConfig
dataclass
¶
The settings of py1815-master evaluate.
Source code in src/py1815/master/config.py
from_mapping
classmethod
¶
Build the settings from a decoded JSON object.
Source code in src/py1815/master/config.py
describe ¶
Return every setting as a JSON-compatible dict.
Source code in src/py1815/master/config.py
MasterConfig
dataclass
¶
The master's whole configuration.
Source code in src/py1815/master/config.py
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | |
from_mapping
classmethod
¶
Build a configuration from a decoded JSON document.
Raises :class:ConfigError for an unknown setting, a value of the
wrong type, an outstation with no name or host, or a name used twice.
Source code in src/py1815/master/config.py
load
classmethod
¶
describe ¶
Return the complete configuration as a JSON-compatible dict.
defaults lists every outstation setting. Each outstation lists its
name, its host and only the settings that differ from the defaults.
Source code in src/py1815/master/config.py
read ¶
Read a JSON configuration file and return the decoded document.
Source code in src/py1815/settings.py
The description of the API¶
py1815.master.openapi ¶
The master's HTTP API, described as an OpenAPI document.
One description of every operation: what it takes, what it returns, and an
example of asking for it. :func:build turns that into an OpenAPI 3.1
document with a route for each operation, and the document is committed beside
the console, which serves it at /openapi.json.
The description is checked against the service and not trusted: a test fails when an operation exists that is not described here, or is described here and does not exist, and when the committed document is not the one this module builds.
python -m py1815.master.openapi # print the document
python -m py1815.master.openapi --write # write it where it is served from
python -m py1815.master.openapi --check # fail if the committed one is stale
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
build ¶
The OpenAPI document, as a dictionary.
Source code in src/py1815/master/openapi.py
1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 | |
render ¶
Requests¶
py1815.master.requests ¶
The requests a master reads with, as the object headers that ask for them.
Kept apart from anything that sends, so the same request is built whether it goes over a socket or straight into a session, and so each can be pinned to its octets in a test.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
write_time ¶
The object of a time write: milliseconds since the epoch, UTC, in 48 bits.
write_recorded_time ¶
The object of the write that ends the LAN procedure (IEEE 1815-2012 10.3.3.2).
Group 50 variation 3: the time, in milliseconds since the epoch, at which the master sent the request to record the current time.
Source code in src/py1815/master/requests.py
clear_restart ¶
The object that clears the restart indication: bit 7 of the indications, written as 0.
freeze_counters ¶
class_scan ¶
The headers of a read of the named classes, in the order named.
Source code in src/py1815/master/requests.py
scan ¶
The headers of a scan by name: integrity, events, class0 to class3, or outputs.
Source code in src/py1815/master/requests.py
read_points ¶
Build the headers of a read of named points, in the default variations.
Each point type maps to the indices wanted, or to None for every point of that type. Types are asked in the order given.
Indices are sent as start-stop ranges (qualifiers 0x00 and 0x01), one header for each run of consecutive indices, in ascending order. A DNP3 Subset Level 2 outstation is only required to accept ranges in a read. The index-list qualifiers (0x17 and 0x28) are optional at that level, and opendnp3 rejects them.
Source code in src/py1815/master/requests.py
Reading objects¶
py1815.decode ¶
Reading the objects a response carries.
The rest of this library writes objects; this reads them, which a master has
to do and an outstation never does. It is written from the object layouts in
the standard and not by turning the encoders in :mod:py1815.objects around.
An encoder and a decoder derived from one another agree with each other
whatever they get wrong, and the point of reading an outstation is to learn
what it sent.
It reads as far as it can and says where it stopped. An object's width is the only way to find the object after it, so a group or variation that is not in the table ends the walk. That is reported, with the octets that were not read, and is not raised: a response that is half understood is still half of what the outstation said.
Copyright 2026 DER Security Corp. Licensed under the Apache License, Version 2.0.
PointType ¶
Bases: Enum
The kinds of point a master keeps values for.
Source code in src/py1815/decode.py
Layout
dataclass
¶
How one object of a group and variation is laid out.
Source code in src/py1815/decode.py
DecodedObject
dataclass
¶
One object out of a response.
Source code in src/py1815/decode.py
Decoded
dataclass
¶
What was read from a response body, and what was not.
Source code in src/py1815/decode.py
decode_objects ¶
Every object in a response body that can be read, in the order sent.
Reading stops at the first header or object that cannot be read, and the result says why and holds the octets from there on. Nothing read before that point is discarded: each object was complete, and the outstation sent it.
A common time of occurrence applies to the relative-time events after it in the same body, as the standard has it, and to none before it.