├── .github └── workflows │ ├── python-health.yml │ └── python-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── py61850 ├── __init__.py ├── communication │ ├── __init__.py │ └── publish_goose.py ├── goose │ ├── __init__.py │ ├── ethernet.py │ ├── pdu.py │ ├── publisher.py │ └── virtual_lan.py ├── types │ ├── __init__.py │ ├── base.py │ ├── boolean.py │ ├── floating_point.py │ ├── integer.py │ ├── times.py │ └── visible_string.py └── utils │ ├── __init__.py │ ├── errors.py │ ├── numbers.py │ ├── parser.py │ └── tags.py ├── setup.cfg └── tests ├── goose ├── test_goose_base.py ├── test_pdu.py ├── test_pdu_base.py └── test_vlan.py └── types ├── test_base.py ├── test_boolean.py ├── test_float.py ├── test_integer.py ├── test_times.py └── test_visible_string.py /.github/workflows/python-health.yml: -------------------------------------------------------------------------------- 1 | name: Health (Py3.8) 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.8' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install flake8 pytest coverage 20 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 21 | - name: Lint with flake8 22 | run: | 23 | flake8 24 | - name: Test with pytest 25 | run: | 26 | coverage run -m pytest 27 | - name: Check test coverage 28 | run: | 29 | coverage report 30 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Version (Py3.6, Py3.7) 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pytest 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | - name: Test with pytest 25 | run: | 26 | python -m pytest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pycharm 2 | .idea/ 3 | 4 | # venv 5 | venv/ 6 | 7 | # python 8 | __pycache__/ 9 | *.pyc 10 | 11 | # pytest 12 | .pytest_cache 13 | 14 | # coverage 15 | htmlcov/ 16 | .coverage 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Arthur Zopellaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py61850 2 | 3 | IEC 61850 for Python 3. 4 | 5 | [![Health Status](https://github.com/arthurazs/py61850/workflows/Health%20%28Py3.8%29/badge.svg)](https://github.com/arthurazs/py61850/actions?query=workflow%3A"Health+(Py3.8)") 6 | Stable Version, Linting (PEP8) and Coverage 7 | 8 | [![Health Status](https://github.com/arthurazs/py61850/workflows/Version%20%28Py3.6%2C%20Py3.7%29/badge.svg)](https://github.com/arthurazs/py61850/actions?query=workflow%3A"Version+(Py3.6,+Py3.7)") 9 | Testing for other Python Versions 10 | 11 | ## What I want it to be 12 | 13 | ### sync61850 14 | 15 | ```python 16 | from socket import AF_PACKET, socket, SOCK_RAW 17 | from sys import argv 18 | from time import sleep 19 | 20 | nic = socket(AF_PACKET, SOCK_RAW) 21 | nic.bind((argv[1], 0)) 22 | 23 | from sansio61850.goose import Publisher 24 | from sansio61850.types import Boolean, VisibleString 25 | 26 | 27 | ied = 'IED_Pub' 28 | ref = f'{ied}_CFG/LLN0' 29 | publisher = Publisher(destination='01:0c:cd:01:00:13', vlan=False, app_id=1, 30 | gcb_ref=f'{ref}$GO$GOOSE_SENDER', data_set=f'{ref}$MyDataSet', 31 | go_id=ied, all_data=(Boolean(True), VisibleString('Content'))) 32 | 33 | for sq_num, goose in enumerate(publisher): 34 | if sq_num == 10: 35 | goose.pdu.all_data[1] = 'New Content' # VisibleString 36 | nic.send(bytes(goose)) 37 | sleep(goose.next_goose_timer) 38 | ``` 39 | 40 | ## Roadmap 41 | 42 | According to IEC 61850 parts 7-2, 8-1 and 9-2, and ISO 9506 parts 1 and 2. 43 | 44 | BER encoding. 45 | 46 | - NEXT TODOs 47 | - add base.py missing test 48 | - move py61850 to sansio61850, sync61850 49 | - separate encoder from decoder 50 | 51 | - [ ] Exceptions 52 | - [ ] Add custom exceptions 53 | - [ ] TagError 54 | - [ ] LengthError 55 | - [ ] RangeError 56 | - [ ] Review every exception message 57 | - [ ] Implement EAFP-based functions? 58 | - [ ] Basic Types 59 | - [ ] Make sure data returns only the specified length, e.g.: 60 | - `data[1:5]` instead of `data[1:]` 61 | - [ ] Care for undefined [ASN.1 length](http://luca.ntop.org/Teaching/Appunti/asn1.html) 62 | - `0x0000` to denote the end of the data 63 | - [X] Boolean 64 | - [X] Unsigned Integer (ISO) 65 | - [ ] Enumerated (IEC) 66 | - [X] Signed Integer 67 | - [X] Floating Point 68 | - [X] Visible String 69 | - [ ] Octet String 70 | - [ ] MMS String (ISO) | Unicode String (IEC) 71 | - [X] Time Stamp 72 | - [ ] Time of Day (ISO) | Entry Time (IEC) 73 | - [ ] Bit String (ISO) | Coded Enum (IEC) 74 | 75 | Should py61850 support raw MMS (ISO)? 76 | 77 | Should py61850 log what is happening? This might decrease performance. 78 | 79 | Should support different modes of operation? (direct control, SBO, normal/enhanced sec) 80 | 81 | Change obj.tag to return the class name instead of the hex value of raw_tag? 82 | 83 | Should I enable changing obj value/raw_value after it being created? 84 | 85 | Check [gridsoftware](http://www.gridsoftware.com). 86 | 87 | Frame Generator or GOOSE Emulator? *e.g.*, Should I enable starting a GOOSE from stnum 10 instead of 0? 88 | 89 | ```python 90 | # TODO Test the time difference between both 91 | 92 | v_str = VisibleString.encode('arthur') 93 | assert v_str == b'\x8A\x06arthur' 94 | assert type(v_str) == bytes 95 | 96 | v_str = VisibleStringEncoder('arthur') 97 | assert bytes(v_str) == b'\x8A\x06arthur' 98 | assert type(v_str) == VisibleStringEncoder 99 | ``` 100 | 101 | ## Reference 102 | 103 | - https://www.sphinx-doc.org/en/1.8/usage/extensions/example_google.html#example-google 104 | - https://www.ossnokalva.com/asn1/resources/asn1-made-simple/introduction.html 105 | - IEC 61850-9-2 Figure A.3 (ASN.1) 106 | 107 | ## Ideas 108 | 109 | - Read MMS PCAP and recreate an IED 110 | - Read GOOSE + MMS and get INFO about GOOSE 111 | - Raspberies 112 | - Raspberies + IEDs 113 | - SDN 114 | - Communication Security 115 | - Teleprotection 116 | - Protection 117 | - IED Simm, simple, that sends a trip, and has gui (info tech ied sim) 118 | - Test bed with 3 substations? to study goose cascading, etc... 119 | - Use SCD to recreate all the IED instances 120 | - AMI 121 | - GOOSE, SV and MMS monitor 122 | 123 | ## GOOSE 124 | 125 | You can test the generic publisher by running the following: 126 | 127 | ```bash 128 | user@host:~/communication$ sudo python3 -m communication.publish_goose lo 129 | ``` 130 | 131 | **NOTE**: The `lo` parameter represents the network interface which will send the GOOSE frame. 132 | -------------------------------------------------------------------------------- /py61850/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthurazs/py61850/ba9c5f40ef21bfecd14a8d380e9ff512da9ba5bf/py61850/__init__.py -------------------------------------------------------------------------------- /py61850/communication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthurazs/py61850/ba9c5f40ef21bfecd14a8d380e9ff512da9ba5bf/py61850/communication/__init__.py -------------------------------------------------------------------------------- /py61850/communication/publish_goose.py: -------------------------------------------------------------------------------- 1 | from socket import AF_PACKET, socket, SOCK_RAW 2 | from sys import argv 3 | from time import time, time_ns 4 | 5 | from py61850.goose.publisher import Publisher 6 | from py61850.types import Boolean, VisibleString 7 | from py61850.types.floating_point import DoublePrecision, SinglePrecision 8 | from py61850.types.integer import Signed, Unsigned 9 | from py61850.types.times import Quality, Timestamp 10 | 11 | nic = socket(AF_PACKET, SOCK_RAW) 12 | nic.bind((argv[1], 0)) 13 | 14 | now = time_ns() 15 | 16 | data = { 17 | # HEADER 18 | 'destination': b'\x01\x0c\xcd\x01\x00\x13', 19 | 'source': b'\x7c\x8a\xe1\xd9\xcf\xbe', 20 | # VLAN 21 | 'virtual_lan': True, 22 | 'vlan_priority': 7, 23 | 'vlan_id': 0xFFF, 24 | # GOOSE 25 | 'app_id': 13, 26 | # PDU 27 | 'goose_control_block_reference': 'ASD_CFG/LLN0$GO$GOOSE_SENDER', 28 | 'time_allowed_to_live': 1000, 29 | 'data_set': 'ASD_CFG/LLN0$MyDataSet', 30 | 'goose_identifier': 'ASD', 31 | 'goose_timestamp': time(), 32 | 'status_number': 1, 33 | 'sequence_number': 0, 34 | 'test': True, 35 | 'configuration_revision': 13, 36 | 'needs_commissioning': True, 37 | # ALL DATA 38 | 'all_data': ( 39 | Boolean(True), 40 | VisibleString('Content'), 41 | DoublePrecision(1.2), 42 | SinglePrecision(3.4), 43 | Signed(-5), 44 | Unsigned(6), 45 | Timestamp( 46 | 705762855.123456789, 47 | Quality(False, False, True, 13) 48 | ) 49 | ) 50 | } 51 | 52 | publisher = Publisher(**data) 53 | 54 | goose = bytes(publisher) 55 | nic.send(goose) 56 | 57 | for index, goose in enumerate(publisher): 58 | nic.send(bytes(goose)) 59 | if index == 0xF: 60 | break 61 | 62 | print(f'{(time_ns() - now) / 1000}us') 63 | -------------------------------------------------------------------------------- /py61850/goose/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthurazs/py61850/ba9c5f40ef21bfecd14a8d380e9ff512da9ba5bf/py61850/goose/__init__.py -------------------------------------------------------------------------------- /py61850/goose/ethernet.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack, Struct 2 | from py61850.utils.numbers import U48 3 | from py61850.utils.errors import raise_type 4 | 5 | 6 | class Ethernet: 7 | 8 | @staticmethod 9 | def enet_itoe(integer, max_range=0xFFFF): 10 | # integer to ether type 11 | if isinstance(integer, int): 12 | if 0 <= integer <= max_range: 13 | return s_pack('!H', integer) 14 | raise ValueError('integer out of supported range') 15 | raise_type('integer', int, type(integer)) 16 | 17 | @staticmethod 18 | def enet_stoe(string): 19 | # string to ether type 20 | if isinstance(string, str): 21 | if len(string) == 4: 22 | return s_pack('!H', int(string, 16)) 23 | raise ValueError('string out of supported length') 24 | raise_type('string', str, type(string)) 25 | 26 | @staticmethod 27 | def pack_ether_type(ether_type): 28 | if isinstance(ether_type, bytes): 29 | return ether_type 30 | elif isinstance(ether_type, int): 31 | return Ethernet.enet_itoe(ether_type) 32 | elif isinstance(ether_type, str): 33 | return Ethernet.enet_stoe(ether_type) 34 | raise_type('ether_type', (bytes, int, str), type(ether_type)) 35 | 36 | @staticmethod 37 | def unpack_ether_type(byte_stream): 38 | # ether type to string 39 | if isinstance(byte_stream, bytes): 40 | if len(byte_stream) == 2: 41 | return byte_stream.hex().upper() 42 | raise ValueError('byte_stream out of supported range') 43 | raise_type('byte_stream', bytes, type(byte_stream)) 44 | 45 | @staticmethod 46 | def enet_etos(byte_stream): 47 | return Ethernet.unpack_ether_type(byte_stream) 48 | 49 | @staticmethod 50 | def assert_destination(byte_stream): 51 | # according to IEC 61850-8-1 52 | if isinstance(byte_stream, bytes): 53 | if len(byte_stream) == 6: 54 | if byte_stream[0:4] == b'\x01\x0c\xcd\x01': 55 | if 0 <= byte_stream[4] <= 1: 56 | return True 57 | raise ValueError('fifth octet out of supported range') 58 | raise ValueError('first four octets do not represent a GOOSE multicast address') 59 | raise ValueError('byte_stream out of supported range') 60 | raise_type('byte_stream', bytes, type(byte_stream)) 61 | 62 | @staticmethod 63 | def pack_mac_address(mac_address): 64 | if isinstance(mac_address, bytes): 65 | return mac_address 66 | elif isinstance(mac_address, int): 67 | return Ethernet.enet_itom(mac_address) 68 | elif isinstance(mac_address, str): 69 | return Ethernet.enet_stom(mac_address) 70 | raise_type('mac_address', (bytes, int, str), type(mac_address)) 71 | 72 | @staticmethod 73 | def unpack_mac_address(byte_stream, splitter='-'): 74 | if isinstance(byte_stream, bytes): 75 | if len(byte_stream) == 6: 76 | address = byte_stream.hex() 77 | return splitter.join(address[index:index + 2] for index in range(0, len(address), 2)).upper() 78 | raise ValueError('byte_stream out of supported range') 79 | raise_type('byte_stream', bytes, type(byte_stream)) 80 | 81 | @staticmethod 82 | def enet_mtos(byte_stream): 83 | # mac to string 84 | return Ethernet.unpack_mac_address(byte_stream) 85 | 86 | @staticmethod 87 | def enet_itom(integer): 88 | # integer to mac 89 | if isinstance(integer, int): 90 | if 0 <= integer < U48: 91 | return s_pack('!Q', integer)[2:] 92 | raise ValueError('integer out of supported range') 93 | raise_type('integer', int, type(integer)) 94 | 95 | @staticmethod 96 | def enet_stom(string): 97 | # string to mac 98 | # NOTE Should enet_aton be smarter? e.g. accept 0::0? 99 | if isinstance(string, str): 100 | if len(string) == 17: 101 | # TODO Improve this function, it should accept any type of 'splitter' 102 | for splitter in [':', '-', ' ']: 103 | if splitter in string: 104 | unsigned_char = Struct('!B') 105 | mac_address = b'' 106 | for byte in string.split(splitter): 107 | mac_address += unsigned_char.pack(int(byte, 16)) 108 | return mac_address 109 | else: 110 | # TODO Improve 'malformed' checking 111 | raise ValueError('string seems to be malformed') 112 | elif len(string) == 12: 113 | return Ethernet.enet_itom(int(string, 16)) 114 | raise ValueError('string out of supported range') 115 | raise_type('string', str, type(string)) 116 | -------------------------------------------------------------------------------- /py61850/goose/pdu.py: -------------------------------------------------------------------------------- 1 | from typing import NoReturn, Optional, Tuple 2 | 3 | from py61850.types import Boolean, VisibleString 4 | from py61850.types.base import Base 5 | from py61850.types.integer import Unsigned 6 | from py61850.types.times import Quality, Timestamp 7 | from py61850.utils.errors import raise_type 8 | 9 | 10 | class GooseControlBlockReference(VisibleString): 11 | def __init__(self, string: str = None): 12 | super().__init__(string, max_length=65, raw_tag=b'\x80') 13 | 14 | 15 | class TimeAllowedToLive(Unsigned): 16 | def __init__(self, integer: int): 17 | # TODO Follow goose sending rate (should decrease with a new event/status number) 18 | # Should it though? 19 | super().__init__(integer, min_range=1, max_range=0xFFFFFFFF, raw_tag=b'\x81') 20 | 21 | @property 22 | def tag(self) -> str: 23 | """The class name.""" 24 | return self.__class__.__name__ 25 | 26 | 27 | class DataSet(VisibleString): 28 | def __init__(self, string: str = None): 29 | super().__init__(string, max_length=65, raw_tag=b'\x82') 30 | 31 | 32 | class GooseIdentifier(VisibleString): 33 | def __init__(self, string: str = None): 34 | super().__init__(string, max_length=65, raw_tag=b'\x83') 35 | 36 | 37 | class GooseTimestamp(Timestamp): 38 | def __init__(self, epoch: float, quality: Quality = Quality()): 39 | super().__init__(epoch, quality, raw_tag=b'\x84') 40 | 41 | 42 | class StatusNumber(Unsigned): 43 | def __init__(self, integer: int): 44 | super().__init__(integer, min_range=1, max_range=0xFFFFFFFF, raw_tag=b'\x85') 45 | self._iter = False 46 | 47 | def __iter__(self): 48 | # TODO Return a different object? #asd 49 | # next(obj) should not affect the original object 50 | self._iter = True 51 | return self 52 | 53 | def __next__(self): 54 | try: 55 | if self._iter: 56 | self.value += 1 57 | return self 58 | else: 59 | raise TypeError(f"'{self.__class__.__name__}' object is not iterable") 60 | except ValueError: 61 | self._iter = False 62 | raise StopIteration 63 | 64 | @property 65 | def tag(self) -> str: 66 | """The class name.""" 67 | return self.__class__.__name__ 68 | 69 | 70 | class SequenceNumber(Unsigned): 71 | def __init__(self, integer: int): 72 | super().__init__(integer, min_range=0, max_range=0xFFFFFFFF, raw_tag=b'\x86') 73 | 74 | def __iter__(self): 75 | self._first = True 76 | return self 77 | 78 | def __next__(self): 79 | try: 80 | if self._first is False: 81 | self.value += 1 82 | self._first = False 83 | return self 84 | except ValueError: 85 | raise StopIteration 86 | except AttributeError: 87 | raise TypeError(f"'{self.__class__.__name__}' object is not iterable") 88 | 89 | @property 90 | def tag(self) -> str: 91 | """The class name.""" 92 | return self.__class__.__name__ 93 | 94 | 95 | class GooseTest(Boolean): 96 | def __init__(self, boolean: bool): 97 | super().__init__(boolean, raw_tag=b'\x87') 98 | 99 | 100 | class ConfigurationRevision(Unsigned): 101 | def __init__(self, integer: int): 102 | super().__init__(integer, min_range=0, max_range=0xFFFFFFFF, raw_tag=b'\x88') 103 | 104 | @property 105 | def tag(self) -> str: 106 | """The class name.""" 107 | return self.__class__.__name__ 108 | 109 | 110 | class NeedsCommissioning(Boolean): 111 | def __init__(self, boolean: bool): 112 | super().__init__(boolean, raw_tag=b'\x89') 113 | 114 | 115 | class NumberOfDataSetEntries(Unsigned): 116 | def __init__(self, integer: int): 117 | # TODO whats the limit? 118 | super().__init__(integer, min_range=0, max_range=0xFFFFFFFF, raw_tag=b'\x8A') 119 | 120 | @property 121 | def tag(self) -> str: 122 | """The class name.""" 123 | return self.__class__.__name__ 124 | 125 | 126 | class AllData(Base): 127 | def __init__(self, *data: Base): 128 | (raw_value, number_of_entries), value = self._parse(data) 129 | if raw_value == b'': 130 | raw_value, value = None, None 131 | self._value = value 132 | super().__init__(raw_tag=b'\xAB', raw_value=raw_value) 133 | self._number_of_entries = number_of_entries 134 | try: 135 | for value in self._value: 136 | value.set_parent(self) 137 | except TypeError: 138 | pass 139 | 140 | @staticmethod 141 | def _encode(value: Tuple[Base, ...]) -> Tuple[bytes, int]: 142 | # TODO Improve 143 | # if value is None: return None, 0 144 | 145 | all_data = b'' 146 | for base in value: 147 | # TODO validate against standard types instead of Base? 148 | # like, test if v_str, u_int, s_int, f_32, f_64, etc 149 | # like, raise error for g_t_smp, sq_num, etc 150 | if not isinstance(base, Base): 151 | raise_type('base', Base, type(base)) 152 | all_data += bytes(base) 153 | return all_data, len(value) 154 | 155 | @staticmethod 156 | def _decode(raw_value: bytes) -> NoReturn: 157 | # TODO Implement 158 | raise NotImplementedError 159 | 160 | @property 161 | def number_of_data_set_entries(self): 162 | return self._number_of_entries 163 | 164 | def __getitem__(self, item): 165 | return self._value[item] 166 | 167 | 168 | class ProtocolDataUnit(Base): 169 | _DATA_TYPES = [('control_block_reference', GooseControlBlockReference), ('time_allowed_to_live', TimeAllowedToLive), 170 | ('data_set', DataSet), ('goose_identifier', GooseIdentifier), ('goose_timestamp', GooseTimestamp), 171 | ('status_number', StatusNumber), ('sequence_number', SequenceNumber), ('test', GooseTest), 172 | ('configuration_revision', ConfigurationRevision), ('needs_commissioning', NeedsCommissioning), 173 | ('skip', None), ('all_data', AllData)] 174 | 175 | # Table 56 (61850-8-1) 176 | def __init__(self, 177 | control_block_reference: GooseControlBlockReference, time_allowed_to_live: TimeAllowedToLive, 178 | data_set: DataSet, goose_identifier: GooseIdentifier, goose_timestamp: GooseTimestamp, 179 | status_number: StatusNumber, sequence_number: SequenceNumber, test: GooseTest, 180 | configuration_revision: ConfigurationRevision, needs_commissioning: NeedsCommissioning, 181 | # TODO remove numOfDatSet from here, remove default value for AllData 182 | number_of_data_set_entries: Optional[NumberOfDataSetEntries] = None, all_data: AllData = AllData(), 183 | raw_value: bytes = None): 184 | # TODO What if the user want's to exclude some unnecessary fields 185 | # e.g. remove nds_com and test fields 186 | if raw_value: 187 | raise NotImplementedError 188 | else: 189 | raw_value, number_of_data_set_entries = self._encode((control_block_reference, time_allowed_to_live, 190 | data_set, goose_identifier, goose_timestamp, 191 | status_number, sequence_number, test, 192 | configuration_revision, needs_commissioning, 193 | number_of_data_set_entries, all_data)) 194 | super().__init__(raw_tag=b'\x61', raw_value=raw_value) 195 | self._value = (control_block_reference, time_allowed_to_live, data_set, goose_identifier, goose_timestamp, 196 | status_number, sequence_number, test, configuration_revision, needs_commissioning, 197 | number_of_data_set_entries, all_data) 198 | for value in self._value: 199 | value.set_parent(self) 200 | 201 | def __iter__(self): 202 | self._iter = True 203 | self._iter_status = iter(self.status_number) 204 | self._iter_sequence = iter(self.sequence_number) 205 | return self 206 | 207 | def __next__(self): 208 | try: 209 | # update time allowed to live + time stamp 210 | self._iter 211 | next(self._iter_sequence) 212 | except StopIteration: 213 | self.sequence_number.value = 1 214 | self._iter_sequence = iter(self.sequence_number) 215 | next(self._iter_sequence) 216 | except AttributeError: 217 | raise TypeError(f"'{self.__class__.__name__}' object is not iterable") 218 | self._generic_update() 219 | return self 220 | 221 | def _generic_update(self): 222 | byte_stream = b'' 223 | for value in self._value: 224 | byte_stream += bytes(value) 225 | self._set_raw_value(byte_stream) 226 | 227 | def _update(self, caller: Base): 228 | if isinstance(caller, AllData): 229 | # TODO what if i change a value for the same value? Should update trigger? 230 | try: 231 | # TODO only change if next was already called 232 | # e.g. pdu.all_data[0] = 1; pdu.all_data[1] = 'Okay' \ 233 | # should generate only ONE new status change, not 2 234 | self._iter 235 | next(self._iter_status) 236 | self.sequence_number.value = 0 237 | self._iter_sequence = iter(self.sequence_number) 238 | except AttributeError: 239 | pass 240 | self._generic_update() 241 | 242 | @staticmethod 243 | def _assert_type(data): 244 | for value, data_type in zip(data, ProtocolDataUnit._DATA_TYPES): 245 | name, type_class = data_type 246 | if name == 'skip': 247 | continue 248 | if not isinstance(value, type_class): 249 | # TODO Test 250 | raise_type(name, type_class, type(value)) 251 | 252 | def _encode(self, 253 | value: Tuple[GooseControlBlockReference, TimeAllowedToLive, DataSet, GooseIdentifier, GooseTimestamp, 254 | StatusNumber, SequenceNumber, GooseTest, ConfigurationRevision, NeedsCommissioning, 255 | NumberOfDataSetEntries, AllData]) -> Tuple[bytes, NumberOfDataSetEntries]: 256 | ProtocolDataUnit._assert_type(value) 257 | 258 | (control_block_reference, time_allowed_to_live, data_set, goose_identifier, goose_timestamp, 259 | status_number, sequence_number, test, configuration_revision, needs_commissioning, 260 | number_of_data_set_entries, all_data) = value 261 | 262 | if number_of_data_set_entries is None: 263 | number_of_data_set_entries = NumberOfDataSetEntries(all_data.number_of_data_set_entries) 264 | elif not isinstance(number_of_data_set_entries, NumberOfDataSetEntries): 265 | raise_type('number_of_data_set_entries', NumberOfDataSetEntries, type(number_of_data_set_entries)) 266 | 267 | if number_of_data_set_entries.value != all_data.number_of_data_set_entries: 268 | raise ValueError('number_of_data_set_entries doest not match all_data number of entries') 269 | 270 | if len(control_block_reference) + len(time_allowed_to_live) + len(data_set) + \ 271 | len(goose_identifier) + len(goose_timestamp) + len(status_number) + len(sequence_number) + \ 272 | len(test) + len(configuration_revision) + len(needs_commissioning) + \ 273 | len(number_of_data_set_entries) + len(all_data) > 1492: # PDU limit 274 | raise ValueError(f'{self.__class__.__name__} out of supported length') 275 | 276 | return \ 277 | bytes(control_block_reference) + bytes(time_allowed_to_live) + bytes(data_set) + \ 278 | bytes(goose_identifier) + bytes(goose_timestamp) + bytes(status_number) + bytes(sequence_number) + \ 279 | bytes(test) + bytes(configuration_revision) + bytes(needs_commissioning) + \ 280 | bytes(number_of_data_set_entries) + bytes(all_data), number_of_data_set_entries 281 | 282 | @staticmethod 283 | def _decode(raw_value: bytes) -> NoReturn: 284 | # TODO Implement 285 | raise NotImplementedError 286 | 287 | @property 288 | def goose_control_block_reference(self): 289 | return self._value[0] 290 | 291 | @property 292 | def time_allowed_to_live(self): 293 | return self._value[1] 294 | 295 | @property 296 | def data_set(self): 297 | return self._value[2] 298 | 299 | @property 300 | def goose_identifier(self): 301 | return self._value[3] 302 | 303 | @property 304 | def goose_timestamp(self): 305 | return self._value[4] 306 | 307 | @property 308 | def status_number(self): 309 | return self._value[5] 310 | 311 | @property 312 | def sequence_number(self): 313 | return self._value[6] 314 | 315 | @property 316 | def goose_test(self): 317 | return self._value[7] 318 | 319 | @property 320 | def configuration_revision(self): 321 | return self._value[8] 322 | 323 | @property 324 | def needs_commissioning(self): 325 | return self._value[9] 326 | 327 | @property 328 | def number_of_data_set_entries(self): 329 | return self._value[10] 330 | 331 | @property 332 | def all_data(self): 333 | return self._value[11] 334 | -------------------------------------------------------------------------------- /py61850/goose/publisher.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack 2 | from typing import Optional, Tuple, Union 3 | 4 | from py61850.goose.ethernet import Ethernet 5 | from py61850.goose.pdu import AllData, ConfigurationRevision, DataSet, GooseControlBlockReference, GooseIdentifier 6 | from py61850.goose.pdu import GooseTimestamp, NeedsCommissioning, NumberOfDataSetEntries, ProtocolDataUnit 7 | from py61850.goose.pdu import SequenceNumber, StatusNumber, GooseTest, TimeAllowedToLive 8 | from py61850.goose.virtual_lan import VirtualLAN 9 | from py61850.types.base import Base 10 | from py61850.utils.parser import int_u16, u16_str 11 | 12 | VLAN_ETHER_TYPE = b'\x81\x00' 13 | GOOSE_ETHER_TYPE = b'\x88\xb8' 14 | 15 | 16 | class Publisher: 17 | def __init__(self, destination: bytes = b'\x01\x0c\xcd\x01\x00\x00', source: bytes = b'\x00\x00\x00\x00\x00\x00', 18 | virtual_lan: bool = True, vlan_priority: int = 4, vlan_id: int = 0, app_id: int = 1, 19 | goose_control_block_reference: str = 'IED_CFG/LLN0$GO$ControlBlockReference', 20 | time_allowed_to_live: int = 1000, data_set: str = 'IED_CFG/LLN0$DataSet', 21 | goose_identifier: str = 'IED', goose_timestamp: float = 0.0, status_number: int = 1, 22 | sequence_number: int = 0, test: bool = False, configuration_revision: int = 1, 23 | needs_commissioning: bool = False, number_of_data_set_entries: Optional[int] = None, 24 | all_data: Optional[Union[AllData, Tuple[Base, ...]]] = None): 25 | self.destination = destination 26 | self.source = source 27 | if virtual_lan: 28 | self._ether_type = VLAN_ETHER_TYPE 29 | # TODO add vlan property if, and only if, vlan set to True 30 | self._virtual_lan = VirtualLAN(GOOSE_ETHER_TYPE, vlan_priority, vlan_id) 31 | else: 32 | self._ether_type = GOOSE_ETHER_TYPE 33 | self._virtual_lan = None 34 | self._set_app_id(app_id) 35 | self._length = None # 2 bytes 36 | self._reserved = b'\x00\x00\x00\x00' 37 | 38 | goose_control_block_reference = GooseControlBlockReference(goose_control_block_reference) 39 | time_allowed_to_live = TimeAllowedToLive(time_allowed_to_live) 40 | data_set = DataSet(data_set) 41 | goose_identifier = GooseIdentifier(goose_identifier) 42 | goose_timestamp = GooseTimestamp(goose_timestamp) 43 | status_number = StatusNumber(status_number) 44 | sequence_number = SequenceNumber(sequence_number) 45 | test = GooseTest(test) 46 | configuration_revision = ConfigurationRevision(configuration_revision) 47 | needs_commissioning = NeedsCommissioning(needs_commissioning) 48 | if number_of_data_set_entries is not None: 49 | number_of_data_set_entries = NumberOfDataSetEntries(number_of_data_set_entries) 50 | if all_data: 51 | if not isinstance(all_data, AllData): 52 | all_data = AllData(*all_data) 53 | else: 54 | all_data = AllData() 55 | 56 | self._pdu = ProtocolDataUnit(goose_control_block_reference, time_allowed_to_live, data_set, goose_identifier, 57 | goose_timestamp, status_number, sequence_number, test, configuration_revision, 58 | needs_commissioning, number_of_data_set_entries, all_data) 59 | 60 | def __bytes__(self): 61 | header = self._raw_destination + self._raw_source + self._ether_type 62 | if self._virtual_lan: 63 | header += bytes(self._virtual_lan) 64 | start = self._raw_app_id 65 | # TODO Implement LENGTH 66 | end = self._reserved + bytes(self._pdu) 67 | return header + start + s_pack('!H', len(start + end) + 2) + end 68 | 69 | def __iter__(self): 70 | self._iter = iter(self._pdu) 71 | return self 72 | 73 | def __next__(self): 74 | try: 75 | next(self._iter) 76 | except AttributeError: 77 | raise TypeError(f"'{self.__class__.__name__}' object is not iterable") 78 | return self 79 | 80 | @property 81 | def raw_destination(self): 82 | return self._raw_destination 83 | 84 | @property 85 | def destination(self): 86 | return self._destination 87 | 88 | @destination.setter 89 | def destination(self, mac_address): 90 | self._raw_destination = Ethernet.pack_mac_address(mac_address) 91 | Ethernet.assert_destination(self._raw_destination) 92 | if isinstance(mac_address, str): 93 | self._destination = mac_address.upper() 94 | else: 95 | self._destination = Ethernet.unpack_mac_address(mac_address) 96 | 97 | @property 98 | def raw_source(self): 99 | return self._raw_source 100 | 101 | @property 102 | def source(self): 103 | return self._source 104 | 105 | @source.setter 106 | def source(self, mac_address): 107 | self._raw_source = Ethernet.pack_mac_address(mac_address) 108 | if isinstance(mac_address, str): 109 | self._source = mac_address.upper() 110 | else: 111 | self._source = Ethernet.unpack_mac_address(mac_address) 112 | 113 | @property 114 | def raw_app_id(self): 115 | return self._raw_app_id 116 | 117 | @property 118 | def app_id(self): 119 | return self._app_id 120 | 121 | def _set_app_id(self, app_id: int): 122 | self._raw_app_id = int_u16(app_id) 123 | self._app_id = u16_str(self._raw_app_id) 124 | 125 | @property 126 | def protocol_data_unit(self): 127 | return self._pdu 128 | -------------------------------------------------------------------------------- /py61850/goose/virtual_lan.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack 2 | from py61850.utils.numbers import U12 3 | 4 | GOOSE_ETHER_TYPE = b'\x88\xb8' 5 | PRIORITIES = { # is it useful? 6 | 0: 'Background', 7 | 1: 'Best effort', 8 | 2: 'Excellent effort', 9 | 3: 'Critical applications', 10 | 4: 'Video, < 100 ms latency and jitter', 11 | 5: 'Voice, < 10 ms latency and jitter', 12 | 6: 'Internetwork control', 13 | 7: 'Network control', 14 | } 15 | 16 | 17 | class VirtualLAN: 18 | def __init__(self, ether_type=GOOSE_ETHER_TYPE, priority=4, vid=0): 19 | # NOTE add support for DPI == True? 20 | self._ether_type = ether_type 21 | self.priority = priority 22 | self.vid = vid 23 | 24 | def __bytes__(self): 25 | prio_dei = self._priority << 1 # add the DPI bit (which is always 0) 26 | vlan_data = (prio_dei << 12) + self._vid 27 | return s_pack('!H', vlan_data) + self._ether_type 28 | 29 | @property 30 | def priority(self): 31 | return self._priority 32 | 33 | @property 34 | def priority_desc(self): 35 | return PRIORITIES[self._priority] 36 | 37 | @priority.setter 38 | def priority(self, priority): 39 | if 0 <= priority <= 7: 40 | self._priority = priority 41 | else: 42 | raise ValueError('priority is out of supported range') 43 | 44 | @property 45 | def vid(self): 46 | return self._vid 47 | 48 | @vid.setter 49 | def vid(self, vid): 50 | if 0 <= vid < U12: 51 | self._vid = vid 52 | else: 53 | raise ValueError('vid is out of supported range') 54 | -------------------------------------------------------------------------------- /py61850/types/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from py61850.types.boolean import Boolean 3 | from py61850.types.visible_string import VisibleString 4 | -------------------------------------------------------------------------------- /py61850/types/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, Optional, Tuple, Union 3 | from struct import pack as s_pack 4 | 5 | from py61850.utils.errors import raise_type 6 | 7 | 8 | class Generic(ABC): 9 | 10 | def _parse(self, anything: Any) -> Tuple[Optional[bytes], Any]: 11 | 12 | unpacked = anything 13 | if not isinstance(anything, bytes): 14 | try: 15 | unpacked = anything[0] 16 | except (TypeError, IndexError): 17 | pass 18 | 19 | if isinstance(unpacked, bytes): 20 | raw_value = anything 21 | value = self._decode(raw_value) 22 | else: 23 | value = anything 24 | raw_value = self._encode(value) 25 | if raw_value is None or value is None: 26 | return None, None 27 | return raw_value, value 28 | 29 | @staticmethod 30 | def _encode(value: Any) -> bytes: 31 | raise NotImplementedError # pragma: no cover 32 | 33 | @staticmethod 34 | def _decode(raw_value: bytes) -> Any: 35 | raise NotImplementedError # pragma: no cover 36 | 37 | 38 | class Base(Generic, ABC): 39 | """This is the base for any IEC data type. 40 | 41 | This class does not care for encoding, nor decoding the value field, 42 | which should be handled by the subclass. 43 | Thus, the `Base` expects the already encoded value field, but handles 44 | the encoding/decoding of both tag and length field. 45 | 46 | Args: 47 | raw_tag: The encoded tag field. 48 | raw_value: The encoded value field. 49 | 50 | Raises: 51 | TypeError: If `raw_tag` type is different from `bytes`. 52 | TypeError: If `raw_value` type is different from `bytes` and `NoneType`. 53 | ValueError: If `raw_tag` length is different from 1. 54 | """ 55 | 56 | def __init__(self, raw_tag: bytes, raw_value: Optional[bytes] = None) -> None: 57 | self._set_tag(raw_tag) 58 | self._set_raw_value(raw_value) 59 | self._parent = None 60 | 61 | def __bytes__(self) -> bytes: 62 | """Return the encoded data, including all existing fields. 63 | 64 | If value field is `None`: return tag + length. 65 | 66 | If value field is not `None`: return tag + length + value. 67 | """ 68 | if self._raw_value is None: 69 | return self._raw_tag + self._raw_length 70 | return self._raw_tag + self._raw_length + self._raw_value 71 | 72 | def __len__(self) -> int: 73 | """Return the length of the encoded data, including all existing fields. 74 | 75 | If value field is `None`: return tag + length. 76 | 77 | If value field is not `None`: return tag + length + value. 78 | 79 | Note: 80 | For the length field, use the `length` property. 81 | """ 82 | if self.raw_value is None: 83 | return len(self.raw_tag) + len(self.raw_length) 84 | return len(self.raw_tag) + len(self.raw_length) + len(self.raw_value) 85 | 86 | def set_parent(self, parent: 'Base'): 87 | if not isinstance(parent, Base): 88 | raise_type('parent', Base, type(parent)) 89 | self._parent = parent 90 | 91 | def _update(self, caller: 'Base'): 92 | byte_stream = b'' 93 | for value in self._value: 94 | byte_stream += bytes(value) 95 | self._set_raw_value(byte_stream) 96 | 97 | def _set_tag(self, raw_tag: bytes) -> None: 98 | # assert `raw_tag` is `bytes` and has length of 1, then set `raw_tag` and `tag` 99 | if not isinstance(raw_tag, bytes): 100 | raise_type('raw_tag', bytes, type(raw_tag)) 101 | if len(raw_tag) != 1: 102 | raise ValueError('raw_tag out of supported length') 103 | self._raw_tag = raw_tag 104 | # self._tag = raw_tag.hex() 105 | self._tag = self.__class__.__name__ 106 | 107 | @staticmethod 108 | def unpack_extra_value(value_a: Union[bytes, Tuple[bytes, Any]], 109 | value_b: Union[Any, Tuple[Any, Any]]) -> Tuple[bytes, Any, Any]: 110 | try: 111 | value_a, value_c = value_a 112 | except ValueError: 113 | value_b, value_c = value_b 114 | if value_c is None: 115 | value_b, value_c = value_b 116 | return value_a, value_b, value_c 117 | 118 | @property 119 | def tag(self) -> str: 120 | """The class name.""" 121 | return self._tag 122 | 123 | @property 124 | def raw_tag(self) -> bytes: 125 | """The encoded tag field.""" 126 | return self._raw_tag 127 | 128 | def _set_length(self, length: int) -> None: 129 | """Encode length according to ASN.1 BER. 130 | 131 | `raw_length` will be of 1 byte long if < 128. 132 | If it's 2+ bytes long, the first byte indicates how many 133 | bytes follows. 134 | 135 | Example: 136 | 128 == b'\x81\x80', where 0x81 indicates 1 extra byte 137 | for the length, and 0x80 is the length itself. 138 | 139 | Args: 140 | length: The length to be encoded. 141 | 142 | Raises: 143 | ValueError: If `length` is greater than `0xFFFF`. 144 | """ 145 | # NOTE enable extra_length > 2? 146 | # NOTE indefinite length? 147 | if 0 <= length < 0x80: 148 | self._raw_length = s_pack('!B', length) 149 | elif 0x80 <= length <= 0xFF: 150 | self._raw_length = s_pack('!BB', 0x81, length) 151 | elif 0xFF < length <= 0xFFFF: 152 | self._raw_length = s_pack('!BH', 0x82, length) 153 | else: 154 | raise ValueError(f'data length greater than {0xFFFF}') 155 | self._length = length 156 | 157 | @property 158 | def raw_value(self) -> Optional[bytes]: 159 | """The encoded value field.""" 160 | return self._raw_value 161 | 162 | def _set_raw_value(self, raw_value: Optional[bytes]) -> None: 163 | """Set raw value field. 164 | 165 | Note: 166 | This method does not encode the value field. 167 | This should be done by the subclass using 168 | the `_encode_value()` method. 169 | 170 | Args: 171 | raw_value: The raw value to be set. 172 | 173 | Raises: 174 | ValueError: If the length of `raw_value` is greater than `0xFFFF`. 175 | TypeError: If `raw_value` type is different from `bytes` and `NoneType`. 176 | """ 177 | if raw_value is None: 178 | self._raw_value = raw_value 179 | self._set_length(0) 180 | else: 181 | if not isinstance(raw_value, bytes): 182 | raise_type('raw_value', bytes, type(raw_value)) 183 | self._raw_value = raw_value 184 | self._set_length(len(raw_value)) 185 | 186 | try: 187 | self._parent._update(self) 188 | except AttributeError: 189 | pass 190 | 191 | @property 192 | def raw_length(self): 193 | """The encoded length field. 194 | 195 | Note: 196 | For the full data length, including the tag and length fields, use the `len` method. 197 | """ 198 | return self._raw_length 199 | 200 | @property 201 | def length(self): 202 | """The decoded length field""" 203 | return self._length 204 | 205 | @property 206 | def value(self) -> Any: 207 | """The decoded value field""" 208 | return self._value 209 | 210 | @value.setter 211 | def value(self, value: Any) -> None: 212 | raw_value = self._encode(value) 213 | self._set_raw_value(raw_value) 214 | self._value = value 215 | -------------------------------------------------------------------------------- /py61850/types/boolean.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from py61850.types.base import Base 4 | from py61850.utils.errors import raise_type 5 | 6 | 7 | class Boolean(Base): 8 | 9 | def __init__(self, anything: Union[bool, bytes], raw_tag: bytes = b'\x83') -> None: 10 | raw_value, value = self._parse(anything) 11 | super().__init__(raw_tag=raw_tag, raw_value=raw_value) 12 | self._value = value 13 | 14 | @staticmethod 15 | def _encode(value: bool) -> bytes: 16 | if not isinstance(value, bool): 17 | raise_type('value', bool, type(value)) 18 | return b'\x0F' if value else b'\x00' 19 | 20 | @staticmethod 21 | def _decode(raw_value: bytes) -> bool: 22 | if len(raw_value) != 1: 23 | raise ValueError('value out of supported length') 24 | return raw_value != b'\x00' 25 | -------------------------------------------------------------------------------- /py61850/types/floating_point.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack, unpack as s_unpack 2 | from typing import Union 3 | 4 | from py61850.types.base import Base 5 | from py61850.utils.errors import raise_type 6 | 7 | 8 | class FloatingPoint(Base): 9 | def __init__(self, anything: Union[float, bytes], double_precision: bool) -> None: 10 | if double_precision: 11 | self._exponent = b'\x11' 12 | self._format = '!d' 13 | self._length = 9 14 | self._name = 'DoublePrecision' 15 | else: 16 | self._exponent = b'\x08' 17 | self._format = '!f' 18 | self._length = 5 19 | self._name = 'SinglePrecision' 20 | raw_value, value = self._parse(anything) 21 | super().__init__(raw_tag=b'\x87', raw_value=raw_value) 22 | self._value = value 23 | 24 | def _encode(self, value: float) -> bytes: 25 | if not isinstance(value, float): 26 | raise_type('value', float, type(value)) 27 | return self._exponent + s_pack(self._format, value) 28 | 29 | def _decode(self, raw_value: bytes) -> float: 30 | if len(raw_value) != self._length: 31 | raise ValueError(f'{self._name} floating point out of supported length') 32 | if raw_value[0:1] != self._exponent: 33 | raise ValueError(f"{self._name} floating point's exponent out of supported range") 34 | return s_unpack(self._format, raw_value[1:self._length])[0] 35 | 36 | @property 37 | def tag(self) -> str: 38 | """The class name.""" 39 | return self.__class__.__name__ + 'FloatingPoint' 40 | 41 | 42 | class SinglePrecision(FloatingPoint): 43 | def __init__(self, value: Union[float, bytes]) -> None: 44 | super().__init__(value, double_precision=False) 45 | 46 | 47 | class DoublePrecision(FloatingPoint): 48 | def __init__(self, value: Union[float, bytes]) -> None: 49 | super().__init__(value, double_precision=True) 50 | -------------------------------------------------------------------------------- /py61850/types/integer.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack, unpack as s_unpack 2 | from typing import Union 3 | 4 | from py61850.types.base import Base 5 | from py61850.utils.errors import raise_type 6 | 7 | 8 | class Unsigned(Base): 9 | def __init__(self, 10 | anything: Union[int, bytes], min_range: int = 0, max_range: int = 0xFFFFFFFF, 11 | raw_tag: bytes = b'\x86') -> None: 12 | self._min_range = min_range 13 | self._max_range = max_range 14 | raw_value, value = self._parse(anything) 15 | super().__init__(raw_tag=raw_tag, raw_value=raw_value) 16 | self._value = value 17 | 18 | def _encode(self, value: int) -> bytes: 19 | if not isinstance(value, int): 20 | raise_type('value', int, type(value)) 21 | if value < 0: 22 | raise ValueError('Unsigned integer cannot be negative') 23 | elif value <= 0xFF and self._min_range <= value <= self._max_range: 24 | return s_pack('!B', value) 25 | elif value <= 0xFFFF and self._min_range <= value <= self._max_range: 26 | return s_pack('!H', value) 27 | # elif value <= 0xFFFFFF and self._min_range <= value <= self._max_range: 28 | # # NOTE regular MMS does not have 24 bits unsigned int 29 | # # NOTE 24 bits unsigned int seems to be used only for timestamp 30 | # return s_pack('!I', value)[1:] 31 | elif value <= 0xFFFFFFFF and self._min_range <= value <= self._max_range: 32 | return s_pack('!I', value) 33 | raise ValueError('Unsigned integer out of supported range') 34 | 35 | @staticmethod 36 | def _decode(raw_value: bytes) -> int: 37 | if len(raw_value) == 1: 38 | return s_unpack('!B', raw_value)[0] 39 | elif len(raw_value) == 2: 40 | return s_unpack('!H', raw_value)[0] 41 | # elif len(raw_value) == 3: 42 | # # NOTE regular MMS does not have 24 bits unsigned int 43 | # # NOTE 24 bits unsigned int seems to be used only for timestamp 44 | # return s_unpack('!I', b'\x00' + raw_value)[0] 45 | elif len(raw_value) == 4: 46 | return s_unpack('!I', raw_value)[0] 47 | raise ValueError('Unsigned integer out of supported range') 48 | 49 | @property 50 | def tag(self) -> str: 51 | """The class name.""" 52 | return self.__class__.__name__ + 'Integer' 53 | 54 | 55 | class Signed(Base): 56 | def __init__(self, anything: Union[int, bytes]) -> None: 57 | raw_value, value = self._parse(anything) 58 | super().__init__(raw_tag=b'\x85', raw_value=raw_value) 59 | self._value = value 60 | 61 | @staticmethod 62 | def _encode(value: int) -> bytes: 63 | if not isinstance(value, int): 64 | raise_type('value', int, type(value)) 65 | if -0x80 <= value < 0x80: 66 | return s_pack('!b', value) 67 | elif -0x8000 <= value < 0x8000: 68 | return s_pack('!h', value) 69 | elif -0x80000000 <= value < 0x80000000: 70 | return s_pack('!i', value) 71 | elif -0x80 ** 0x9 <= value < 0x80 ** 0x9: # NOTE change support from 64 to 128? 72 | return s_pack('!q', value) 73 | raise ValueError('Signed integer out of supported range') 74 | 75 | @staticmethod 76 | def _decode(raw_value: bytes) -> int: 77 | if len(raw_value) == 1: 78 | return s_unpack('!b', raw_value)[0] 79 | elif len(raw_value) == 2: 80 | return s_unpack('!h', raw_value)[0] 81 | elif len(raw_value) == 4: 82 | return s_unpack('!i', raw_value)[0] 83 | elif len(raw_value) == 8: 84 | return s_unpack('!q', raw_value)[0] 85 | raise ValueError('Signed integer out of supported range') 86 | 87 | @property 88 | def tag(self) -> str: 89 | """The class name.""" 90 | return self.__class__.__name__ + 'Integer' 91 | -------------------------------------------------------------------------------- /py61850/types/times.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack, unpack as s_unpack 2 | from typing import Optional, Tuple, Union 3 | 4 | from py61850.types.base import Base, Generic 5 | from py61850.utils.errors import raise_type 6 | 7 | 8 | class Quality(Generic): 9 | def __init__(self, leap_seconds_known: bool = False, clock_failure: bool = False, 10 | clock_not_synchronized: bool = True, time_accuracy: int = 0, raw_value: Optional[bytes] = None): 11 | if raw_value is None: 12 | raw_value = self._encode((leap_seconds_known, clock_failure, clock_not_synchronized, time_accuracy)) 13 | else: 14 | leap_seconds_known, clock_failure, clock_not_synchronized, time_accuracy = self._decode(raw_value) 15 | self._raw_value = raw_value 16 | self._leap_seconds = leap_seconds_known 17 | self._clock_failure = clock_failure 18 | self._clock_not_sync = clock_not_synchronized 19 | self._accuracy = time_accuracy 20 | 21 | def __bytes__(self): 22 | return self._raw_value 23 | 24 | def _decode(self, raw_value: bytes) -> Tuple[bool, bool, bool, int]: 25 | if not isinstance(raw_value, bytes): 26 | raise_type('raw_value', bytes, type(raw_value)) 27 | if len(raw_value) != 1: 28 | raise ValueError('raw_value out of supported length') 29 | 30 | bits = s_unpack('!B', raw_value)[0] 31 | 32 | time_accuracy = bits & 0x1F 33 | if time_accuracy > 24 and time_accuracy != 0x1F: 34 | raise ValueError('bits out of supported range') 35 | 36 | leap_seconds_known = (bits & 0x80) == 0x80 37 | clock_failure = (bits & 0x40) == 0x40 38 | clock_not_synchronized = (bits & 0x20) == 0x20 39 | 40 | return leap_seconds_known, clock_failure, clock_not_synchronized, time_accuracy 41 | 42 | def _encode(self, value: Tuple[bool, bool, bool, int]) -> bytes: 43 | leap_seconds_known, clock_failure, clock_not_synchronized, time_accuracy = value 44 | if not isinstance(leap_seconds_known, bool): 45 | raise_type('leap_seconds_known', bool, type(leap_seconds_known)) 46 | if not isinstance(clock_failure, bool): 47 | raise_type('clock_failure', bool, type(clock_failure)) 48 | if not isinstance(clock_not_synchronized, bool): 49 | raise_type('clock_not_synchronized', bool, type(clock_not_synchronized)) 50 | if not isinstance(time_accuracy, int): 51 | raise_type('time_accuracy', int, type(time_accuracy)) 52 | 53 | if 0 <= time_accuracy <= 24 or time_accuracy == 0x1F: 54 | self._accuracy = time_accuracy 55 | else: 56 | raise ValueError('time_accuracy out of supported range') 57 | 58 | bits = (leap_seconds_known << 7) + (clock_failure << 6) + \ 59 | (clock_not_synchronized << 5) + time_accuracy 60 | return s_pack('!B', bits) 61 | 62 | @property 63 | def leap_seconds_known(self): 64 | return self._leap_seconds 65 | 66 | @property 67 | def clock_failure(self): 68 | return self._clock_failure 69 | 70 | @property 71 | def clock_not_synchronized(self): 72 | return self._clock_not_sync 73 | 74 | @property 75 | def time_accuracy(self) -> Union[int, str]: 76 | return 'Unspecified' if self._accuracy == 0x1F else self._accuracy 77 | 78 | 79 | class Timestamp(Base): 80 | 81 | # UTC Time 82 | def __init__(self, anything: Union[float, bytes], quality: Optional[Quality] = None, raw_tag: bytes = b'\x91'): 83 | raw_value, value = self._parse((anything, quality)) 84 | raw_value, value, quality = self.unpack_extra_value(raw_value, value) 85 | super().__init__(raw_tag=raw_tag, raw_value=raw_value) 86 | self._value = value 87 | self._quality = quality 88 | 89 | @staticmethod 90 | def _encode(value: Tuple[float, Quality]) -> bytes: 91 | value, quality = value 92 | if not isinstance(value, float): 93 | raise_type('value', float, type(value)) 94 | if not isinstance(quality, Quality): 95 | raise_type('quality', Quality, type(quality)) 96 | 97 | seconds, fraction = map(int, str(value).split('.')) 98 | byte_stream = s_pack('!I', seconds) 99 | byte_stream += s_pack('!I', fraction)[1:] 100 | return byte_stream + bytes(quality) 101 | 102 | @staticmethod 103 | def _decode(raw_value: bytes) -> Tuple[float, Quality]: 104 | raw_value, _ = raw_value 105 | if len(raw_value) != 8: 106 | raise ValueError('raw_value out of supported length') 107 | seconds = s_unpack('!I', raw_value[:4])[0] 108 | # TODO Fraction seems to be wrong 109 | fraction = s_unpack('!I', b'\x00' + raw_value[4:7])[0] 110 | quality = Quality(raw_value=raw_value[7:8]) 111 | return float(str(f'{seconds}.{fraction}')), quality 112 | 113 | @property 114 | def leap_seconds_known(self): 115 | return self._quality.leap_seconds_known 116 | 117 | @property 118 | def clock_failure(self): 119 | return self._quality.clock_failure 120 | 121 | @property 122 | def clock_not_synchronized(self): 123 | return self._quality.clock_not_synchronized 124 | 125 | @property 126 | def time_accuracy(self): 127 | return self._quality.time_accuracy 128 | -------------------------------------------------------------------------------- /py61850/types/visible_string.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from py61850.types.base import Base 4 | from py61850.utils.errors import raise_type 5 | 6 | 7 | class VisibleString(Base): 8 | 9 | def __init__(self, anything: Optional[Union[str, bytes]] = None, 10 | max_length: int = 0xFF, raw_tag: bytes = b'\x8A') -> None: 11 | self._max_length = max_length 12 | raw_value, value = self._parse(anything) 13 | super().__init__(raw_tag=raw_tag, raw_value=raw_value) 14 | self._value = value 15 | 16 | def _encode(self, value: Optional[str]) -> Optional[bytes]: 17 | if value is None: 18 | return None 19 | if not isinstance(value, str): 20 | raise_type('value', str, type(value)) 21 | if len(value) == 0: 22 | return None 23 | elif 0 < len(value) <= self._max_length: 24 | return value.encode('utf8') 25 | raise ValueError('value out of supported length') 26 | 27 | def _decode(self, raw_value: bytes) -> Optional[str]: 28 | if len(raw_value) == 0: 29 | return None 30 | if 0 < len(raw_value) <= self._max_length: 31 | return raw_value.decode('utf8') 32 | raise ValueError('raw_value out of supported length') 33 | -------------------------------------------------------------------------------- /py61850/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthurazs/py61850/ba9c5f40ef21bfecd14a8d380e9ff512da9ba5bf/py61850/utils/__init__.py -------------------------------------------------------------------------------- /py61850/utils/errors.py: -------------------------------------------------------------------------------- 1 | from typing import NoReturn, Tuple, Union 2 | 3 | 4 | def raise_type(argument_name: str, expected_type: Union[type, Tuple[type, ...]], real_type: type) -> NoReturn: 5 | raise TypeError(f'expected {argument_name} to be {expected_type}, got {real_type} instead') 6 | -------------------------------------------------------------------------------- /py61850/utils/numbers.py: -------------------------------------------------------------------------------- 1 | U7 = 128 # 2 ** 7 quantity 2 | U8 = 256 # 2 ** 8 quantity 3 | N8, P8 = -U7, U7 - 1 # min / max range 4 | 5 | U12 = 4_096 # 2 ** 12 quantity 6 | 7 | U15 = 32_768 # 2 ** 15 quantity 8 | U16 = 65_536 # 2 ** 16 quantity 9 | N16, P16 = -U15, U15 - 1 # min / max range 10 | 11 | U24 = 16_777_216 # 2 ** 24 quantity 12 | 13 | U31 = 2_147_483_648 # 2 ** 31 quantity 14 | U32 = 4_294_967_296 # 2 ** 32 quantity 15 | N32, P32 = -U31, U31 - 1 # min / max range 16 | 17 | U48 = 281_474_976_710_656 # 2 ** 48 quantity 18 | 19 | U63 = 9_223_372_036_854_775_808 # 2 ** 63 quantity 20 | U64 = 18_446_744_073_709_551_616 # 2 ** 64 quantity 21 | N64, P64 = -U63, U63 - 1 # min / max range 22 | 23 | PI32 = 3.1415927410125732 24 | PI64 = 3.141592653589793 25 | ONE_THIRD32 = 0.3333333432674408 26 | ONE_THIRD64 = 0.3333333333333333 27 | -------------------------------------------------------------------------------- /py61850/utils/parser.py: -------------------------------------------------------------------------------- 1 | from struct import pack as s_pack 2 | 3 | from py61850.utils.errors import raise_type 4 | 5 | 6 | def int_u16(integer: int, min_range: int = 0, max_range: int = 0xFFFF) -> bytes: 7 | if isinstance(integer, int): 8 | if min_range <= integer <= max_range: 9 | return s_pack('!H', integer) 10 | raise ValueError('integer out of supported range') 11 | raise_type('integer', int, type(integer)) 12 | 13 | 14 | def u16_str(byte_stream: bytes) -> str: 15 | if isinstance(byte_stream, bytes): 16 | if len(byte_stream) == 2: 17 | return byte_stream.hex().upper() 18 | raise ValueError('byte_stream out of supported length') 19 | raise_type('byte_stream', bytes, type(byte_stream)) 20 | -------------------------------------------------------------------------------- /py61850/utils/tags.py: -------------------------------------------------------------------------------- 1 | GOOSE_ETHER_TYPE = b'\x88\xb8' 2 | VIRTUAL_LAN_ETHER_TYPE = b'\x81\x00' 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | source = 3 | py61850 4 | tests 5 | 6 | [coverage:report] 7 | fail_under = 80 8 | show_missing = True 9 | skip_covered = True 10 | exclude_lines = pragma: no cover 11 | 12 | [flake8] 13 | exclude = venv/* 14 | max-line-length = 127 15 | statistics = True 16 | max-complexity = 10 17 | count = True 18 | -------------------------------------------------------------------------------- /tests/goose/test_goose_base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from py61850.goose.ethernet import Ethernet 3 | from py61850.utils.numbers import U16, U48, U64 4 | 5 | 6 | class TestEthernetPackMAC(unittest.TestCase): 7 | def test_integer_range_error(self): 8 | self.assertRaises(ValueError, Ethernet.enet_itom, U64) 9 | 10 | def test_integer_type_error(self): 11 | self.assertRaises(TypeError, Ethernet.enet_itom, '') 12 | 13 | def test_integer_min(self): 14 | self.assertEqual(Ethernet.enet_itom(0), b'\x00\x00\x00\x00\x00\x00') 15 | 16 | def test_integer_max(self): 17 | self.assertEqual(Ethernet.enet_itom(U48 - 1), b'\xff\xff\xff\xff\xff\xff') 18 | 19 | def test_ascii_range_error(self): 20 | self.assertRaises(ValueError, Ethernet.enet_stom, '00:00:00:00:00:00:') 21 | 22 | def test_ascii_malformed(self): 23 | self.assertRaises(ValueError, Ethernet.enet_stom, '00.00.00.00.00.00') 24 | 25 | def test_ascii_min(self): 26 | self.assertEqual(Ethernet.enet_stom('000000000000'), b'\x00\x00\x00\x00\x00\x00') 27 | 28 | def test_ascii_max(self): 29 | self.assertEqual(Ethernet.enet_stom('ffffffffffff'), b'\xff\xff\xff\xff\xff\xff') 30 | 31 | def test_colon_min(self): 32 | self.assertEqual(Ethernet.enet_stom('00:00:00:00:00:00'), b'\x00\x00\x00\x00\x00\x00') 33 | 34 | def test_colon_max(self): 35 | self.assertEqual(Ethernet.enet_stom('ff:ff:ff:ff:ff:ff'), b'\xff\xff\xff\xff\xff\xff') 36 | 37 | def test_hyphen_min(self): 38 | self.assertEqual(Ethernet.enet_stom('00-00-00-00-00-00'), b'\x00\x00\x00\x00\x00\x00') 39 | 40 | def test_hyphen_max(self): 41 | self.assertEqual(Ethernet.enet_stom('ff-ff-ff-ff-ff-ff'), b'\xff\xff\xff\xff\xff\xff') 42 | 43 | def test_space_min(self): 44 | self.assertEqual(Ethernet.enet_stom('00 00 00 00 00 00'), b'\x00\x00\x00\x00\x00\x00') 45 | 46 | def test_space_max(self): 47 | self.assertEqual(Ethernet.enet_stom('ff ff ff ff ff ff'), b'\xff\xff\xff\xff\xff\xff') 48 | 49 | def test_type_error(self): 50 | self.assertRaises(TypeError, Ethernet.enet_stom, 0) 51 | 52 | def test_pack_bytes(self): 53 | self.assertEqual(Ethernet.pack_mac_address(b'\x00\x00\x00\x00\x00\x00'), b'\x00\x00\x00\x00\x00\x00') 54 | 55 | def test_pack_string(self): 56 | self.assertEqual(Ethernet.pack_mac_address('00:00:00:00:00:00'), b'\x00\x00\x00\x00\x00\x00') 57 | 58 | def test_pack_integer(self): 59 | self.assertEqual(Ethernet.pack_mac_address(0), b'\x00\x00\x00\x00\x00\x00') 60 | 61 | def test_pack_type_error(self): 62 | self.assertRaises(TypeError, Ethernet.pack_mac_address, 0.0) 63 | 64 | def test_assert_dest_type_error(self): 65 | self.assertRaises(TypeError, Ethernet.assert_destination, 1) 66 | 67 | def test_assert_dest_value_error_length(self): 68 | self.assertRaises(ValueError, Ethernet.assert_destination, b'\x01\x0c\xcd\x01') 69 | 70 | def test_assert_dest_value_error_tag(self): 71 | self.assertRaises(ValueError, Ethernet.assert_destination, b'\x01\x0c\xcd\x02\x00\x00') 72 | 73 | def test_assert_dest_value_error_range(self): 74 | self.assertRaises(ValueError, Ethernet.assert_destination, b'\x01\x0c\xcd\x01\x02\x00') 75 | 76 | def test_assert_dest(self): 77 | self.assertTrue(Ethernet.assert_destination(b'\x01\x0c\xcd\x01\x01\xFF')) 78 | 79 | 80 | class TestEthernetUnpackMAC(unittest.TestCase): 81 | def test_range_error_below(self): 82 | self.assertRaises(ValueError, Ethernet.unpack_mac_address, b'\xff\xff\xff\xff\xff') 83 | 84 | def test_range_error_above(self): 85 | self.assertRaises(ValueError, Ethernet.unpack_mac_address, b'\xff\xff\xff\xff\xff\xff\xff') 86 | 87 | def test_type_error(self): 88 | self.assertRaises(TypeError, Ethernet.unpack_mac_address, '00-00-00-00-00-00') 89 | 90 | def test_mtos(self): 91 | self.assertEqual(Ethernet.enet_mtos(b'\x00\x00\x00\x00\x00\x00'), '00-00-00-00-00-00') 92 | 93 | def test_unpack_splitter(self): 94 | self.assertEqual(Ethernet.unpack_mac_address(b'\xff\xff\xff\xff\xff\xff', ':'), 'FF:FF:FF:FF:FF:FF') 95 | 96 | 97 | class TestEthernetPackEtherType(unittest.TestCase): 98 | def test_itoe_type_error(self): 99 | self.assertRaises(TypeError, Ethernet.enet_itoe, '0000') 100 | 101 | def test_itoe_range_error(self): 102 | self.assertRaises(ValueError, Ethernet.enet_itoe, U16) 103 | 104 | def test_itoe_min(self): 105 | self.assertEqual(Ethernet.enet_itoe(0), b'\x00\x00') 106 | 107 | def test_itoe_max(self): 108 | self.assertEqual(Ethernet.enet_itoe(U16 - 1), b'\xff\xff') 109 | 110 | def test_stoe_type_error(self): 111 | self.assertRaises(TypeError, Ethernet.enet_stoe, 0) 112 | 113 | def test_stoe_length_error_below(self): 114 | self.assertRaises(ValueError, Ethernet.enet_stoe, '123') 115 | 116 | def test_stoe_length_error_above(self): 117 | self.assertRaises(ValueError, Ethernet.enet_stoe, '12345') 118 | 119 | def test_stoe_min(self): 120 | self.assertEqual(Ethernet.enet_stoe('0000'), b'\x00\x00') 121 | 122 | def test_stoe_max(self): 123 | self.assertEqual(Ethernet.enet_stoe('ffff'), b'\xff\xff') 124 | 125 | def test_pack_type_error(self): 126 | self.assertRaises(TypeError, Ethernet.pack_ether_type, 0.0) 127 | 128 | def test_pack_bytes(self): 129 | self.assertEqual(Ethernet.pack_ether_type(b'\x00\x00'), b'\x00\x00') 130 | 131 | def test_pack_integer(self): 132 | self.assertEqual(Ethernet.pack_ether_type(0), b'\x00\x00') 133 | 134 | def test_pack_string(self): 135 | self.assertEqual(Ethernet.pack_ether_type('0000'), b'\x00\x00') 136 | 137 | 138 | class TestEthernetUnpackEtherType(unittest.TestCase): 139 | def test_etos_type_error(self): 140 | self.assertRaises(TypeError, Ethernet.enet_etos, '0000') 141 | 142 | def test_etos_length_error_below(self): 143 | self.assertRaises(ValueError, Ethernet.enet_etos, b'\x00') 144 | 145 | def test_etos_length_error_above(self): 146 | self.assertRaises(ValueError, Ethernet.enet_etos, b'\x00\x00\x00') 147 | 148 | def test_etos(self): 149 | self.assertEqual(Ethernet.enet_etos(b'\x12\xab'), '12AB') 150 | -------------------------------------------------------------------------------- /tests/goose/test_pdu.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | 3 | from py61850.goose.pdu import AllData, ConfigurationRevision, DataSet, GooseControlBlockReference, GooseIdentifier 4 | from py61850.goose.pdu import GooseTimestamp, NeedsCommissioning, NumberOfDataSetEntries, ProtocolDataUnit 5 | from py61850.goose.pdu import SequenceNumber, StatusNumber, GooseTest, TimeAllowedToLive 6 | from py61850.types import Boolean, VisibleString 7 | 8 | 9 | class TestProtocolDataUnit: 10 | 11 | @fixture 12 | def cb_ref(self): 13 | return GooseControlBlockReference('IED_CFG/LLN0$GO$ControlBlockReference') 14 | 15 | @fixture 16 | def ttl(self): 17 | return TimeAllowedToLive(1000) 18 | 19 | @fixture 20 | def dat_set(self): 21 | return DataSet('IED_CFG/LLN0$DataSet') 22 | 23 | @fixture 24 | def go_id(self): 25 | return GooseIdentifier('IED') 26 | 27 | @fixture 28 | def t(self): 29 | return GooseTimestamp(0.0) 30 | 31 | @fixture 32 | def st_num(self): 33 | return StatusNumber(1) 34 | 35 | @fixture 36 | def sq_num(self): 37 | return SequenceNumber(0) 38 | 39 | @fixture 40 | def go_test(self): 41 | return GooseTest(False) 42 | 43 | @fixture 44 | def conf_rev(self): 45 | return ConfigurationRevision(1) 46 | 47 | @fixture 48 | def nds_com(self): 49 | return NeedsCommissioning(False) 50 | 51 | @fixture 52 | def num_entries(self): 53 | return NumberOfDataSetEntries(1) 54 | 55 | @fixture 56 | def all_data(self): 57 | return AllData(Boolean(True)) 58 | 59 | @fixture 60 | def pdu(self, cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com): 61 | return ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com) 62 | 63 | @fixture 64 | def iter_pdu(self, pdu): 65 | return iter(pdu) 66 | 67 | @staticmethod 68 | def test_bytes(pdu): 69 | assert bytes(pdu) == b'\x61\x64' \ 70 | b'\x80\x25IED_CFG/LLN0$GO$ControlBlockReference' \ 71 | b'\x81\x02\x03\xe8' \ 72 | b'\x82\x14IED_CFG/LLN0$DataSet' \ 73 | b'\x83\x03IED' \ 74 | b'\x84\x08\x00\x00\x00\x00\x00\x00\x00\x20' \ 75 | b'\x85\x01\x01' \ 76 | b'\x86\x01\x00' \ 77 | b'\x87\x01\x00' \ 78 | b'\x88\x01\x01' \ 79 | b'\x89\x01\x00' \ 80 | b'\x8a\x01\x00' \ 81 | b'\xab\x00' 82 | 83 | @staticmethod 84 | def test_property_cb_ref(pdu, cb_ref): 85 | assert bytes(pdu.goose_control_block_reference) == bytes(cb_ref) 86 | 87 | @staticmethod 88 | def test_property_ttl(pdu, ttl): 89 | assert bytes(pdu.time_allowed_to_live) == bytes(ttl) 90 | 91 | @staticmethod 92 | def test_property_dat_set(pdu, dat_set): 93 | assert bytes(pdu.data_set) == bytes(dat_set) 94 | 95 | @staticmethod 96 | def test_property_go_id(pdu, go_id): 97 | assert bytes(pdu.goose_identifier) == bytes(go_id) 98 | 99 | @staticmethod 100 | def test_property_t(pdu, t): 101 | assert bytes(pdu.goose_timestamp) == bytes(t) 102 | 103 | @staticmethod 104 | def test_property_st_num(pdu, st_num): 105 | assert bytes(pdu.status_number) == bytes(st_num) 106 | 107 | @staticmethod 108 | def test_property_sq_num(pdu, sq_num): 109 | assert bytes(pdu.sequence_number) == bytes(sq_num) 110 | 111 | @staticmethod 112 | def test_property_test(pdu, go_test): 113 | assert bytes(pdu.goose_test) == bytes(go_test) 114 | 115 | @staticmethod 116 | def test_property_conf_rev(pdu, conf_rev): 117 | assert bytes(pdu.configuration_revision) == bytes(conf_rev) 118 | 119 | @staticmethod 120 | def test_property_nds_com(pdu, nds_com): 121 | assert bytes(pdu.needs_commissioning) == bytes(nds_com) 122 | 123 | @staticmethod 124 | def test_wrong_num_of_entries(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 125 | go_test, conf_rev, nds_com, all_data): 126 | with raises(ValueError): 127 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, 128 | conf_rev, nds_com, NumberOfDataSetEntries(2), all_data) 129 | 130 | @staticmethod 131 | def test_correct_num_of_entries(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 132 | go_test, conf_rev, nds_com, num_entries, all_data): 133 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 134 | go_test, conf_rev, nds_com, num_entries, all_data) 135 | assert pdu.number_of_data_set_entries.value == pdu.all_data.number_of_data_set_entries 136 | 137 | @staticmethod 138 | def test_error_cb_ref(ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 139 | with raises(TypeError): 140 | ProtocolDataUnit(1, dat_set, go_id, t, st_num, sq_num, 141 | go_test, conf_rev, nds_com, num_entries, all_data) 142 | 143 | @staticmethod 144 | def test_error_ttl(cb_ref, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 145 | with raises(TypeError): 146 | ProtocolDataUnit(cb_ref, 1, dat_set, go_id, t, st_num, sq_num, 147 | go_test, conf_rev, nds_com, num_entries, all_data) 148 | 149 | @staticmethod 150 | def test_error_dat_set(cb_ref, ttl, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 151 | with raises(TypeError): 152 | ProtocolDataUnit(cb_ref, ttl, 1, go_id, t, st_num, sq_num, 153 | go_test, conf_rev, nds_com, num_entries, all_data) 154 | 155 | @staticmethod 156 | def test_error_go_id(cb_ref, ttl, dat_set, t, st_num, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 157 | with raises(TypeError): 158 | ProtocolDataUnit(cb_ref, ttl, dat_set, 1, t, st_num, sq_num, 159 | go_test, conf_rev, nds_com, num_entries, all_data) 160 | 161 | @staticmethod 162 | def test_error_t(cb_ref, ttl, dat_set, go_id, st_num, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 163 | with raises(TypeError): 164 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, 1, st_num, sq_num, 165 | go_test, conf_rev, nds_com, num_entries, all_data) 166 | 167 | @staticmethod 168 | def test_error_st_num(cb_ref, ttl, dat_set, go_id, t, sq_num, go_test, conf_rev, nds_com, num_entries, all_data): 169 | with raises(TypeError): 170 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, 1, sq_num, 171 | go_test, conf_rev, nds_com, num_entries, all_data) 172 | 173 | @staticmethod 174 | def test_error_sq_num(cb_ref, ttl, dat_set, go_id, t, st_num, go_test, conf_rev, nds_com, num_entries, all_data): 175 | with raises(TypeError): 176 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, 1, 177 | go_test, conf_rev, nds_com, num_entries, all_data) 178 | 179 | @staticmethod 180 | def test_error_go_test(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, conf_rev, nds_com, num_entries, all_data): 181 | with raises(TypeError): 182 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 183 | 1, conf_rev, nds_com, num_entries, all_data) 184 | 185 | @staticmethod 186 | def test_error_conf_rev(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, nds_com, num_entries, all_data): 187 | with raises(TypeError): 188 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 189 | go_test, 1, nds_com, num_entries, all_data) 190 | 191 | @staticmethod 192 | def test_error_nds_com(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, num_entries, all_data): 193 | with raises(TypeError): 194 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 195 | go_test, conf_rev, 1, num_entries, all_data) 196 | 197 | @staticmethod 198 | def test_error_num_entries(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com, all_data): 199 | with raises(TypeError): 200 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 201 | go_test, conf_rev, nds_com, 1, all_data) 202 | 203 | @staticmethod 204 | def test_error_all_data(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com, num_entries): 205 | with raises(TypeError): 206 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 207 | go_test, conf_rev, nds_com, num_entries, 1) 208 | 209 | @staticmethod 210 | def test_no_iter(pdu): 211 | with raises(TypeError) as info: 212 | next(pdu) 213 | assert str(info.value) == "'ProtocolDataUnit' object is not iterable" 214 | 215 | @staticmethod 216 | def test_iter_first_st(iter_pdu): 217 | assert next(iter_pdu).status_number.value == 1 218 | 219 | @staticmethod 220 | def test_iter_first_sq(iter_pdu): 221 | assert next(iter_pdu).sequence_number.value == 0 222 | 223 | @staticmethod 224 | def test_iter_second_st(iter_pdu): 225 | next(iter_pdu) 226 | assert next(iter_pdu).status_number.value == 1 227 | 228 | @staticmethod 229 | def test_iter_second_sq(iter_pdu): 230 | next(iter_pdu) 231 | assert next(iter_pdu).sequence_number.value == 1 232 | 233 | @staticmethod 234 | def test_iter_last_st(pdu): 235 | pdu.sequence_number.value = 0xFFFFFFFF 236 | iter_pdu = iter(pdu) 237 | next(iter_pdu) 238 | assert next(iter_pdu).status_number.value == 1 239 | 240 | @staticmethod 241 | def test_iter_last_sq(pdu): 242 | pdu.sequence_number.value = 0xFFFFFFFF 243 | iter_pdu = iter(pdu) 244 | next(iter_pdu) 245 | assert next(iter_pdu).sequence_number.value == 1 246 | 247 | @staticmethod 248 | def test_all_data_change_no_iter_st(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 249 | go_test, conf_rev, nds_com, num_entries, all_data): 250 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 251 | go_test, conf_rev, nds_com, num_entries, all_data) 252 | pdu.all_data[0].value = False 253 | assert pdu.status_number.value == 1 254 | 255 | @staticmethod 256 | def test_all_data_change_no_iter_sq(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 257 | go_test, conf_rev, nds_com, num_entries, all_data): 258 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 259 | go_test, conf_rev, nds_com, num_entries, all_data) 260 | pdu.all_data[0].value = False 261 | assert pdu.sequence_number.value == 0 262 | 263 | @staticmethod 264 | def test_all_data_change_iter_st(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 265 | go_test, conf_rev, nds_com, num_entries, all_data): 266 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 267 | go_test, conf_rev, nds_com, num_entries, all_data) 268 | iter_pdu = iter(pdu) 269 | iter_pdu.all_data[0].value = False 270 | assert pdu.status_number.value == 2 271 | 272 | @staticmethod 273 | def test_all_data_change_iter_sq(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 274 | go_test, conf_rev, nds_com, num_entries, all_data): 275 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 276 | go_test, conf_rev, nds_com, num_entries, all_data) 277 | iter_pdu = iter(pdu) 278 | iter_pdu.all_data[0].value = False 279 | assert pdu.sequence_number.value == 0 280 | 281 | @staticmethod 282 | def test_all_data_change_no_iter_error(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 283 | go_test, conf_rev, nds_com, num_entries, all_data): 284 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 285 | go_test, conf_rev, nds_com, num_entries, all_data) 286 | with raises(TypeError) as info: 287 | next(pdu) 288 | assert str(info.value) == "'ProtocolDataUnit' object is not iterable" 289 | 290 | @staticmethod 291 | def test_all_data_change_next_st(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 292 | go_test, conf_rev, nds_com, num_entries, all_data): 293 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 294 | go_test, conf_rev, nds_com, num_entries, all_data) 295 | iter_pdu = iter(pdu) 296 | iter_pdu.all_data[0].value = False 297 | assert next(pdu).status_number.value == 2 298 | 299 | @staticmethod 300 | def test_all_data_change_next_sq(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 301 | go_test, conf_rev, nds_com, num_entries, all_data): 302 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 303 | go_test, conf_rev, nds_com, num_entries, all_data) 304 | iter_pdu = iter(pdu) 305 | iter_pdu.all_data[0].value = False 306 | assert next(pdu).sequence_number.value == 0 307 | 308 | @staticmethod 309 | def test_all_data_change_next_2_st(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 310 | go_test, conf_rev, nds_com, num_entries, all_data): 311 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 312 | go_test, conf_rev, nds_com, num_entries, all_data) 313 | iter_pdu = iter(pdu) 314 | iter_pdu.all_data[0].value = False 315 | next(iter_pdu) 316 | assert next(pdu).status_number.value == 2 317 | 318 | @staticmethod 319 | def test_all_data_change_next_2_sq(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 320 | go_test, conf_rev, nds_com, num_entries, all_data): 321 | pdu = ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, 322 | go_test, conf_rev, nds_com, num_entries, all_data) 323 | iter_pdu = iter(pdu) 324 | iter_pdu.all_data[0].value = False 325 | next(iter_pdu) 326 | assert next(pdu).sequence_number.value == 1 327 | 328 | @staticmethod 329 | def test_pdu_above(cb_ref, ttl, dat_set, go_id, t, st_num, sq_num, go_test, conf_rev, nds_com): 330 | with raises(ValueError) as info: 331 | all_data = (VisibleString('a' * 255), VisibleString('a' * 255), 332 | VisibleString('a' * 255), VisibleString('a' * 255), 333 | VisibleString('a' * 255), VisibleString('a' * 99)) 334 | ProtocolDataUnit(cb_ref, ttl, dat_set, go_id, t, 335 | st_num, sq_num, go_test, conf_rev, nds_com, 336 | None, AllData(*all_data)) 337 | assert str(info.value) == "ProtocolDataUnit out of supported length" 338 | -------------------------------------------------------------------------------- /tests/goose/test_pdu_base.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, mark, raises 2 | 3 | from py61850.goose.pdu import AllData, ConfigurationRevision, DataSet, GooseControlBlockReference, GooseIdentifier 4 | from py61850.goose.pdu import GooseTimestamp, NeedsCommissioning, NumberOfDataSetEntries, SequenceNumber, StatusNumber 5 | from py61850.goose.pdu import GooseTest, TimeAllowedToLive 6 | from py61850.types import Boolean 7 | from py61850.types.times import Quality 8 | 9 | 10 | # === BOOLEAN === 11 | class TestNeedsCommissioning: 12 | @fixture 13 | def true(self): 14 | return NeedsCommissioning(True) 15 | 16 | @staticmethod 17 | def test_bytes(true): 18 | assert bytes(true) == b'\x89\x01\x0F' 19 | 20 | @staticmethod 21 | def test_tag(true): 22 | assert true.tag == 'NeedsCommissioning' 23 | 24 | 25 | class TestGooseTest: 26 | @fixture 27 | def true(self): 28 | return GooseTest(True) 29 | 30 | @staticmethod 31 | def test_bytes(true): 32 | assert bytes(true) == b'\x87\x01\x0F' 33 | 34 | @staticmethod 35 | def test_tag(true): 36 | assert true.tag == 'GooseTest' 37 | 38 | 39 | # === TIMESTAMP === 40 | class TestGooseTimestamp: 41 | @fixture 42 | def regular_date(self): 43 | return GooseTimestamp(1.1, Quality(raw_value=b'\x00')) 44 | 45 | @staticmethod 46 | def test_bytes(regular_date): 47 | assert bytes(regular_date) == b'\x84\x08\x00\x00\x00\x01\x00\x00\x01\x00' 48 | 49 | @staticmethod 50 | def test_tag(regular_date): 51 | assert regular_date.tag == 'GooseTimestamp' 52 | 53 | 54 | # === UNSIGNED INTEGER === 55 | class TestConfigurationRevision: 56 | @fixture 57 | def zero(self): 58 | return ConfigurationRevision(0) 59 | 60 | @staticmethod 61 | def test_min_bytes(zero): 62 | assert bytes(zero) == b'\x88\x01\x00' 63 | 64 | @staticmethod 65 | def test_max_bytes(): 66 | assert bytes(ConfigurationRevision(0xFFFFFFFF)) == b'\x88\x04\xFF\xFF\xFF\xFF' 67 | 68 | @staticmethod 69 | def test_bellow(): 70 | assert raises(ValueError, ConfigurationRevision, -1) 71 | 72 | @staticmethod 73 | def test_above(): 74 | assert raises(ValueError, ConfigurationRevision, 0x1FFFFFFFF) 75 | 76 | @staticmethod 77 | def test_tag(zero): 78 | assert zero.tag == 'ConfigurationRevision' 79 | 80 | 81 | class TestNumberOfEntries: 82 | @fixture 83 | def zero(self): 84 | return NumberOfDataSetEntries(0) 85 | 86 | @staticmethod 87 | def test_min_bytes(zero): 88 | assert bytes(zero) == b'\x8A\x01\x00' 89 | 90 | @staticmethod 91 | def test_max_bytes(): 92 | assert bytes(NumberOfDataSetEntries(0xFFFFFFFF)) == b'\x8A\x04\xFF\xFF\xFF\xFF' 93 | 94 | @staticmethod 95 | def test_bellow(): 96 | assert raises(ValueError, NumberOfDataSetEntries, -1) 97 | 98 | @staticmethod 99 | def test_above(): 100 | assert raises(ValueError, NumberOfDataSetEntries, 0x1FFFFFFFF) 101 | 102 | @staticmethod 103 | def test_tag(zero): 104 | assert zero.tag == 'NumberOfDataSetEntries' 105 | 106 | 107 | class TestSequenceNumber: 108 | @fixture 109 | def zero(self): 110 | return SequenceNumber(0) 111 | 112 | @fixture 113 | def iter_sq(self): 114 | return iter(SequenceNumber(0)) 115 | 116 | @staticmethod 117 | def test_min_bytes(zero): 118 | assert bytes(zero) == b'\x86\x01\x00' 119 | 120 | @staticmethod 121 | def test_max_bytes(): 122 | assert bytes(SequenceNumber(0xFFFFFFFF)) == b'\x86\x04\xFF\xFF\xFF\xFF' 123 | 124 | @staticmethod 125 | def test_bellow(): 126 | with raises(ValueError) as info: 127 | SequenceNumber(-1) 128 | assert str(info.value) == 'Unsigned integer cannot be negative' 129 | 130 | @staticmethod 131 | def test_above(): 132 | with raises(ValueError) as info: 133 | SequenceNumber(0x1FFFFFFFF) 134 | assert str(info.value) == 'Unsigned integer out of supported range' 135 | 136 | @staticmethod 137 | def test_tag(zero): 138 | assert zero.tag == 'SequenceNumber' 139 | 140 | @staticmethod 141 | def test_no_iter(zero): 142 | with raises(TypeError) as info: 143 | next(zero) 144 | assert str(info.value) == "'SequenceNumber' object is not iterable" 145 | 146 | @staticmethod 147 | def test_iter_first(iter_sq): 148 | assert next(iter_sq).value == 0 149 | 150 | @staticmethod 151 | def test_iter_second(iter_sq): 152 | next(iter_sq) 153 | assert next(iter_sq).value == 1 154 | 155 | @staticmethod 156 | def test_iter_last(): 157 | sq_num = iter(SequenceNumber(0xFFFFFFFF)) 158 | next(sq_num) 159 | assert raises(StopIteration, next, sq_num) 160 | 161 | 162 | class TestStatusNumber: 163 | @fixture 164 | def one(self): 165 | return StatusNumber(1) 166 | 167 | @fixture 168 | def iter_st(self): 169 | return iter(StatusNumber(1)) 170 | 171 | @staticmethod 172 | def test_min_bytes(one): 173 | assert bytes(one) == b'\x85\x01\x01' 174 | 175 | @staticmethod 176 | def test_max_bytes(): 177 | assert bytes(StatusNumber(0xFFFFFFFF)) == b'\x85\x04\xFF\xFF\xFF\xFF' 178 | 179 | @staticmethod 180 | def test_bellow(): 181 | with raises(ValueError) as info: 182 | StatusNumber(-1) 183 | # TODO change message from Unsigned integer to StatusNumber 184 | assert str(info.value) == 'Unsigned integer cannot be negative' 185 | 186 | @staticmethod 187 | def test_above(): 188 | with raises(ValueError) as info: 189 | StatusNumber(0x1FFFFFFFF) 190 | assert str(info.value) == 'Unsigned integer out of supported range' 191 | 192 | @staticmethod 193 | def test_tag(one): 194 | assert one.tag == 'StatusNumber' 195 | 196 | @staticmethod 197 | def test_no_iter(one): 198 | with raises(TypeError) as info: 199 | next(one) 200 | assert str(info.value) == "'StatusNumber' object is not iterable" 201 | 202 | @staticmethod 203 | def test_iter_first(iter_st): 204 | assert next(iter_st).value == 2 205 | 206 | @staticmethod 207 | def test_iter_second(iter_st): 208 | next(iter_st) 209 | assert next(iter_st).value == 3 210 | 211 | @staticmethod 212 | def test_iter_last(): 213 | st_num = iter(StatusNumber(0xFFFFFFFF)) 214 | assert raises(StopIteration, next, st_num) 215 | 216 | 217 | class TestTimeAllowedToLive: 218 | @fixture 219 | def one(self): 220 | return TimeAllowedToLive(1) 221 | 222 | @staticmethod 223 | def test_min_bytes(one): 224 | assert bytes(one) == b'\x81\x01\x01' 225 | 226 | @staticmethod 227 | def test_max_bytes(): 228 | assert bytes(TimeAllowedToLive(0xFFFFFFFF)) == b'\x81\x04\xFF\xFF\xFF\xFF' 229 | 230 | @staticmethod 231 | def test_bellow(): 232 | assert raises(ValueError, TimeAllowedToLive, 0) 233 | 234 | @staticmethod 235 | def test_above(): 236 | assert raises(ValueError, TimeAllowedToLive, 0x1FFFFFFFF) 237 | 238 | @staticmethod 239 | def test_tag(one): 240 | assert one.tag == 'TimeAllowedToLive' 241 | 242 | 243 | # === VISIBLE STRING === 244 | class TestDataSet: 245 | 246 | @staticmethod 247 | def test_bytes(): 248 | assert bytes(DataSet('datSet')) == b'\x82\x06datSet' 249 | 250 | @staticmethod 251 | def test_bytes_none(): 252 | assert bytes(DataSet()) == b'\x82\x00' 253 | 254 | @staticmethod 255 | def test_raw_value_none(): 256 | assert DataSet().raw_value is None 257 | 258 | @staticmethod 259 | def test_value_none(): 260 | assert DataSet().value is None 261 | 262 | @staticmethod 263 | def test_above(): 264 | assert raises(ValueError, DataSet, 'a' * 66) 265 | 266 | @staticmethod 267 | def test_tag(): 268 | assert DataSet('').tag == 'DataSet' 269 | 270 | 271 | class TestGooseControlBlockReference: 272 | 273 | @staticmethod 274 | def test_bytes(): 275 | assert bytes(GooseControlBlockReference('gocbRef')) == b'\x80\x07gocbRef' 276 | 277 | @staticmethod 278 | def test_bytes_none(): 279 | assert bytes(GooseControlBlockReference()) == b'\x80\x00' 280 | 281 | @staticmethod 282 | def test_raw_value_none(): 283 | assert GooseControlBlockReference().raw_value is None 284 | 285 | @staticmethod 286 | def test_value_none(): 287 | assert GooseControlBlockReference().value is None 288 | 289 | @staticmethod 290 | def test_above(): 291 | assert raises(ValueError, GooseControlBlockReference, 'a' * 66) 292 | 293 | @staticmethod 294 | def test_tag(): 295 | assert GooseControlBlockReference('').tag == 'GooseControlBlockReference' 296 | 297 | 298 | class TestGooseIdentifier: 299 | 300 | @staticmethod 301 | def test_bytes(): 302 | assert bytes(GooseIdentifier('goID')) == b'\x83\x04goID' 303 | 304 | @staticmethod 305 | def test_bytes_none(): 306 | assert bytes(GooseIdentifier()) == b'\x83\x00' 307 | 308 | @staticmethod 309 | def test_raw_value_none(): 310 | assert GooseIdentifier().raw_value is None 311 | 312 | @staticmethod 313 | def test_value_none(): 314 | assert GooseIdentifier().value is None 315 | 316 | @staticmethod 317 | def test_above(): 318 | assert raises(ValueError, GooseIdentifier, 'a' * 66) 319 | 320 | @staticmethod 321 | def test_tag(): 322 | assert GooseIdentifier('').tag == 'GooseIdentifier' 323 | 324 | 325 | # === OTHERS === 326 | 327 | class TestAllData: 328 | 329 | DATA = { 330 | id: ['zero', 'one', 'two'], 331 | int: [0, 1, 2], 332 | bool: [AllData(), AllData(Boolean(False)), AllData(Boolean(False), Boolean(True))], 333 | bytes: [b'\xAB\x00', b'\xAB\x03\x83\x01\x00', b'\xAB\x06\x83\x01\x00\x83\x01\x0F'], 334 | } 335 | 336 | @fixture 337 | def none(self): 338 | return AllData() 339 | 340 | @mark.parametrize("data, byte_stream", zip(DATA[bool], DATA[bytes]), ids=DATA[id]) 341 | def test_bytes(self, data, byte_stream): 342 | assert bytes(data) == byte_stream 343 | 344 | @mark.parametrize("data, number", zip(DATA[bool], DATA[int]), ids=DATA[id]) 345 | def test_number_of_entries(self, data, number): 346 | assert data.number_of_data_set_entries == number 347 | 348 | @staticmethod 349 | def test_raw_value(none): 350 | assert none.raw_value is None 351 | 352 | @staticmethod 353 | def test_value(none): 354 | assert none.value is None 355 | 356 | @staticmethod 357 | def test_tag(): 358 | assert AllData(Boolean(False)).tag == 'AllData' 359 | 360 | @staticmethod 361 | def test_error_type(): 362 | assert raises(TypeError, AllData, 1) 363 | 364 | @staticmethod 365 | def test_get_item(): 366 | true = Boolean(True) 367 | all_data = AllData(Boolean(False), true) 368 | assert all_data[1] == true 369 | -------------------------------------------------------------------------------- /tests/goose/test_vlan.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from py61850.goose.virtual_lan import VirtualLAN 3 | from py61850.utils.numbers import U12 4 | 5 | GOOSE_ETHER_TYPE = b'\x88\xb8' 6 | DEFAULT_PRIO = 4 7 | DEFAULT_DESC = 'Video, < 100 ms latency and jitter' 8 | DEFAULT_VID = 0 9 | 10 | 11 | class TestVLAN(TestCase): 12 | def setUp(self): 13 | self.vlan = VirtualLAN(ether_type=GOOSE_ETHER_TYPE, priority=DEFAULT_PRIO, vid=DEFAULT_VID) 14 | 15 | def test_init_min(self): 16 | vlan = VirtualLAN(ether_type=GOOSE_ETHER_TYPE, priority=0, vid=0) 17 | self.assertEqual(bytes(vlan), b'\x00\x00' + GOOSE_ETHER_TYPE) 18 | 19 | def test_init_max(self): 20 | vlan = VirtualLAN(ether_type=GOOSE_ETHER_TYPE, priority=7, vid=U12 - 1) 21 | self.assertEqual(bytes(vlan), b'\xEF\xFF' + GOOSE_ETHER_TYPE) 22 | 23 | def test_prio_range_error(self): 24 | with self.assertRaises(ValueError): 25 | self.vlan.priority = 10 26 | 27 | def test_prio(self): 28 | self.assertEqual(self.vlan.priority, DEFAULT_PRIO) 29 | 30 | def test_prio_desc(self): 31 | self.assertEqual(self.vlan.priority_desc, DEFAULT_DESC) 32 | 33 | def test_vid_range_error(self): 34 | with self.assertRaises(ValueError): 35 | self.vlan.vid = U12 36 | 37 | def test_vid(self): 38 | self.assertEqual(self.vlan.vid, DEFAULT_VID) 39 | -------------------------------------------------------------------------------- /tests/types/test_base.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | 3 | from py61850.types.base import Base 4 | 5 | 6 | @fixture 7 | def empty_value(): 8 | return Base(b'\x80') 9 | 10 | 11 | @fixture 12 | def generic(): 13 | return Base(b'\x81', b'string') 14 | 15 | 16 | # === EMPTY VALUE === 17 | 18 | def test_empty_value_raw_tag(empty_value): 19 | assert empty_value.raw_tag == b'\x80' 20 | 21 | 22 | def test_empty_value_tag(empty_value): 23 | assert empty_value.tag == 'Base' 24 | 25 | 26 | def test_empty_value_raw_value(empty_value): 27 | assert empty_value.raw_value is None 28 | 29 | 30 | def test_empty_value_raw_length(empty_value): 31 | assert empty_value.raw_length == b'\x00' 32 | 33 | 34 | def test_empty_value_length(empty_value): 35 | assert empty_value.length == 0 36 | 37 | 38 | def test_empty_value_len(empty_value): 39 | assert len(empty_value) == 2 40 | 41 | 42 | def test_empty_value_bytes(empty_value): 43 | assert bytes(empty_value) == b'\x80\x00' 44 | 45 | 46 | # === GENERIC === 47 | 48 | def test_generic_raw_tag(generic): 49 | assert generic.raw_tag == b'\x81' 50 | 51 | 52 | def test_generic_tag(generic): 53 | assert generic.tag == 'Base' 54 | 55 | 56 | def test_generic_raw_value(generic): 57 | assert generic.raw_value == b'string' 58 | 59 | 60 | def test_generic_raw_length(generic): 61 | assert generic.raw_length == b'\x06' 62 | 63 | 64 | def test_generic_length(generic): 65 | assert generic.length == 6 66 | 67 | 68 | def test_generic_len(generic): 69 | assert len(generic) == 8 70 | 71 | 72 | def test_generic_bytes(generic): 73 | assert bytes(generic) == b'\x81\x06string' 74 | 75 | 76 | # === SET LENGTH === 77 | 78 | def test_set_length_0_max_raw(): 79 | base = Base(b'\x82', b'a' * 0x79) 80 | assert base.raw_length == b'\x79' 81 | 82 | 83 | def test_set_length_0_max(): 84 | base = Base(b'\x82', b'a' * 0x79) 85 | assert base.length == 0x79 86 | 87 | 88 | def test_set_length_1_min_raw(): 89 | base = Base(b'\x82', b'a' * 0x80) 90 | assert base.raw_length == b'\x81\x80' 91 | 92 | 93 | def test_set_length_1_min(): 94 | base = Base(b'\x82', b'a' * 0x80) 95 | assert base.length == 0x80 96 | 97 | 98 | def test_set_length_1_max_raw(): 99 | base = Base(b'\x82', b'a' * 0xFF) 100 | assert base.raw_length == b'\x81\xFF' 101 | 102 | 103 | def test_set_length_1_max(): 104 | base = Base(b'\x82', b'a' * 0xFF) 105 | assert base.length == 0xFF 106 | 107 | 108 | def test_set_length_2_min_raw(): 109 | base = Base(b'\x82', b'a' * 0x1FF) 110 | assert base.raw_length == b'\x82\x01\xFF' 111 | 112 | 113 | def test_set_length_2_min(): 114 | base = Base(b'\x82', b'a' * 0x1FF) 115 | assert base.length == 0x1FF 116 | 117 | 118 | def test_set_length_2_max_raw(): 119 | base = Base(b'\x82', b'a' * 0xFFFF) 120 | assert base.raw_length == b'\x82\xFF\xFF' 121 | 122 | 123 | def test_set_length_2_max(): 124 | base = Base(b'\x82', b'a' * 0xFFFF) 125 | assert base.length == 0xFFFF 126 | 127 | 128 | # === EXCEPTIONS === 129 | 130 | def test_set_tag_value_error_below(): 131 | with raises(ValueError): 132 | Base(b'') 133 | 134 | 135 | def test_set_tag_value_error_above(): 136 | with raises(ValueError): 137 | Base(b'\x00\x00') 138 | 139 | 140 | def test_set_tag_type_error(): 141 | with raises(TypeError): 142 | Base('\x83') 143 | 144 | 145 | def test_set_raw_value_type_error(): 146 | with raises(TypeError): 147 | Base(b'\x84', 'string') 148 | 149 | 150 | def test_set_length_value_error(): 151 | with raises(ValueError): 152 | Base(b'\x85', b'a' * 0x1FFFF) 153 | 154 | 155 | # === EXTRA VALUE === 156 | def test_a_c_b(): 157 | a, b, c = Base.unpack_extra_value((b'1', 3), 2) 158 | assert a == b'1' and b == 2 and c == 3 159 | 160 | 161 | def test_a_b_c(): 162 | a, b, c = Base.unpack_extra_value(b'1', (2, 3)) 163 | assert a == b'1' and b == 2 and c == 3 164 | 165 | 166 | def test_a_none_b_c(): 167 | a, b, c = Base.unpack_extra_value((b'1', None), (2, 3)) 168 | assert a == b'1' and b == 2 and c == 3 169 | -------------------------------------------------------------------------------- /tests/types/test_boolean.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | 3 | from py61850.types import Boolean 4 | 5 | 6 | @fixture 7 | def true(): 8 | return Boolean(True) 9 | 10 | 11 | # === DECODE === 12 | 13 | def test_byte_true_min_raw_value(): 14 | assert Boolean(b'\x01').raw_value == b'\x01' 15 | 16 | 17 | def test_byte_true_min_value(): 18 | assert Boolean(b'\x01').value is True 19 | 20 | 21 | def test_byte_true_max_raw_value(): 22 | assert Boolean(b'\xFF').raw_value == b'\xFF' 23 | 24 | 25 | def test_byte_true_max_value(): 26 | assert Boolean(b'\xFF').value is True 27 | 28 | 29 | def test_byte_false_raw_value(): 30 | assert Boolean(b'\x00').raw_value == b'\x00' 31 | 32 | 33 | def test_byte_false_value(): 34 | assert Boolean(b'\x00').value is False 35 | 36 | 37 | # === TRUE === 38 | 39 | def test_true_value(true): 40 | assert true.value is True 41 | 42 | 43 | def test_true_raw_value(true): 44 | assert true.raw_value != b'\x00' 45 | 46 | 47 | # === FALSE === 48 | 49 | def test_false_value(): 50 | assert Boolean(False).value is False 51 | 52 | 53 | def test_false_raw_value(true): 54 | assert Boolean(False).raw_value == b'\x00' 55 | 56 | 57 | # === UNCHANGED VALUES === 58 | 59 | def test_raw_tag(true): 60 | assert true.raw_tag == b'\x83' 61 | 62 | 63 | def test_tag(true): 64 | assert true.tag == 'Boolean' 65 | 66 | 67 | def test_raw_length(true): 68 | assert true.raw_length == b'\x01' 69 | 70 | 71 | def test_length(true): 72 | assert true.length == 1 73 | 74 | 75 | def test_bytes(): 76 | assert bytes(Boolean(False)) == b'\x83\x01\x00' 77 | 78 | 79 | def test_len(true): 80 | assert len(true) == 3 81 | 82 | 83 | # === EXCEPTIONS === 84 | 85 | def test_encode_decode(): 86 | with raises(TypeError): 87 | Boolean(1) 88 | 89 | 90 | def test_decode_below(): 91 | with raises(ValueError): 92 | Boolean(b'') 93 | 94 | 95 | def test_decode_above(): 96 | with raises(ValueError): 97 | Boolean(b'\x00\x00') 98 | 99 | 100 | def test_none(): 101 | with raises(TypeError): 102 | Boolean(None) 103 | 104 | 105 | def test_none_empty(): 106 | with raises(TypeError): 107 | Boolean() 108 | -------------------------------------------------------------------------------- /tests/types/test_float.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | from pytest import mark 3 | 4 | from py61850.types.floating_point import SinglePrecision, DoublePrecision 5 | from math import isnan 6 | 7 | test_data_single = { 8 | id: [ 9 | 'zero_negative', 'zero_positive', 10 | 'min_negative', 'min_positive', 11 | 'max_negative', 'max_positive', 12 | 'pi_negative', 'pi_positive', 13 | 'one_third_negative', 'one_third_positive', 14 | 'inf_negative', 'inf_positive', 15 | ], 16 | float: [ 17 | -0.0, 0.0, # Zero 18 | -1.401298464324817e-45, 1.401298464324817e-45, # Smallest 19 | -3.4028234663852886e+38, 3.4028234663852886e+38, # Largest 20 | -3.1415927410125732, 3.1415927410125732, # PI 21 | -0.3333333432674408, 0.3333333432674408, # 1/3 22 | float('-inf'), float('inf'), # Infinity 23 | ], 24 | bytes: [ 25 | b'\x08\x80\x00\x00\x00', b'\x08\x00\x00\x00\x00', # Zero 26 | b'\x08\x80\x00\x00\x01', b'\x08\x00\x00\x00\x01', # Smallest 27 | b'\x08\xFF\x7F\xFF\xFF', b'\x08\x7F\x7F\xFF\xFF', # Largest 28 | b'\x08\xC0\x49\x0F\xDB', b'\x08\x40\x49\x0F\xDB', # PI 29 | b'\x08\xBE\xAA\xAA\xAB', b'\x08\x3E\xAA\xAA\xAB', # 1/3 30 | b'\x08\xFF\x80\x00\x00', b'\x08\x7F\x80\x00\x00', # Infinity 31 | ], 32 | } 33 | 34 | test_data_double = { 35 | id: [ 36 | 'zero_negative', 'zero_positive', 37 | 'min_negative', 'min_positive', 38 | 'max_negative', 'max_positive', 39 | 'pi_negative', 'pi_positive', 40 | 'one_third_negative', 'one_third_positive', 41 | 'inf_negative', 'inf_positive', 42 | ], 43 | float: [ 44 | -0.0, 0.0, # Zero 45 | -5e-324, 5e-324, # Smallest 46 | -1.7976931348623157e+308, 1.7976931348623157e+308, # Largest 47 | -3.141592653589793, 3.141592653589793, # PI 48 | -0.3333333333333333, 0.3333333333333333, # 1/3 49 | float('-inf'), float('inf'), # Infinity 50 | ], 51 | bytes: [ 52 | b'\x11\x80\x00\x00\x00\x00\x00\x00\x00', b'\x11\x00\x00\x00\x00\x00\x00\x00\x00', # Zero 53 | b'\x11\x80\x00\x00\x00\x00\x00\x00\x01', b'\x11\x00\x00\x00\x00\x00\x00\x00\x01', # Smallest 54 | b'\x11\xFF\xEF\xFF\xFF\xFF\xFF\xFF\xFF', b'\x11\x7F\xEF\xFF\xFF\xFF\xFF\xFF\xFF', # Largest 55 | b'\x11\xc0\x09\x21\xfb\x54\x44\x2d\x18', b'\x11\x40\x09\x21\xfb\x54\x44\x2d\x18', # PI 56 | b'\x11\xbf\xd5\x55\x55\x55\x55\x55\x55', b'\x11\x3f\xd5\x55\x55\x55\x55\x55\x55', # 1/3 57 | b'\x11\xff\xf0\x00\x00\x00\x00\x00\x00', b'\x11\x7f\xf0\x00\x00\x00\x00\x00\x00', # Infinity 58 | ], 59 | } 60 | 61 | test_error_single = { 62 | id: ['value_length_error', 'value_none', 'value_type', 'value_empty', 'exponent_error'], 63 | float: [b'\x08\x00\x00\x00\x00\x00', None, 0, b'', b'\x09\x00\x00\x00\x00'], 64 | 'error': [ValueError, TypeError, TypeError, ValueError, ValueError], 65 | } 66 | 67 | test_error_double = { 68 | id: ['value_length_error', 'value_none', 'value_type', 'value_empty', 'exponent_error'], 69 | float: [b'\x11\x00\x00\x00\x00\x00\x00\x00', None, 0, b'', b'\x10\x00\x00\x00\x00\x00\x00\x00\x00'], 70 | 'error': [ValueError, TypeError, TypeError, ValueError, ValueError], 71 | } 72 | 73 | 74 | @fixture 75 | def negative_nan(): 76 | return SinglePrecision(float('-nan')) 77 | 78 | 79 | @fixture 80 | def positive_nan(): 81 | return SinglePrecision(float('nan')) 82 | 83 | 84 | @fixture 85 | def single(): 86 | return SinglePrecision(123.456) 87 | 88 | 89 | @fixture 90 | def double(): 91 | return DoublePrecision(123.456) 92 | 93 | 94 | class TestSinglePrecision: 95 | # === ENCODE === 96 | 97 | @mark.parametrize("value, raw_value", zip(test_data_single[float], test_data_single[bytes]), ids=test_data_single[id]) 98 | def test_encode_raw_value(self, value, raw_value): 99 | assert SinglePrecision(value).raw_value == raw_value 100 | 101 | @mark.parametrize("value", test_data_single[float], ids=test_data_single[id]) 102 | def test_encode_value(self, value): 103 | assert SinglePrecision(value).value == value 104 | 105 | def test_encode_negative_nan_raw_value(self, negative_nan): 106 | assert negative_nan.raw_value == b'\x08\xFF\xC0\x00\x00' 107 | 108 | def test_encode_negative_nan_value(self, negative_nan): 109 | assert isnan(negative_nan.value) 110 | 111 | def test_encode_positive_nan_raw_value(self, positive_nan): 112 | assert positive_nan.raw_value == b'\x08\x7F\xC0\x00\x00' 113 | 114 | def test_encode_positive_nan_value(self, positive_nan): 115 | assert isnan(positive_nan.value) 116 | 117 | # === DECODE === 118 | 119 | @mark.parametrize("raw_value", test_data_single[bytes], ids=test_data_single[id]) 120 | def test_decode_raw_value(self, raw_value): 121 | assert SinglePrecision(raw_value).raw_value == raw_value 122 | 123 | @mark.parametrize("value, raw_value", zip(test_data_single[float], test_data_single[bytes]), ids=test_data_single[id]) 124 | def test_decode_value(self, value, raw_value): 125 | assert SinglePrecision(raw_value).value == value 126 | 127 | def test_decode_negative_nan_raw_value(self): 128 | assert SinglePrecision(b'\x08\xFF\xC0\x00\x00').raw_value == b'\x08\xFF\xC0\x00\x00' 129 | 130 | def test_decode_negative_nan_value(self): 131 | assert isnan(SinglePrecision(b'\x08\xFF\xC0\x00\x00').value) 132 | 133 | def test_decode_positive_nan_raw_value(self): 134 | assert SinglePrecision(b'\x08\x7F\xC0\x00\x00').raw_value == b'\x08\x7F\xC0\x00\x00' 135 | 136 | def test_decode_positive_nan_value(self, positive_nan): 137 | assert isnan(SinglePrecision(b'\x08\x7F\xC0\x00\x00').value) 138 | 139 | # === OTHER FIELDS === 140 | 141 | def test_raw_tag(self, single): 142 | assert single.raw_tag == b'\x87' 143 | 144 | def test_tag(self, single): 145 | assert single.tag == 'SinglePrecisionFloatingPoint' 146 | 147 | def test_raw_length(self, single): 148 | assert single.raw_length == b'\x05' 149 | 150 | def test_length(self, single): 151 | assert single.length == 5 152 | 153 | def test_bytes(self, single): 154 | assert bytes(single) == b'\x87\x05\x08\x42\xF6\xE9\x79' 155 | 156 | def test_len(self, single): 157 | assert len(single) == 7 158 | 159 | # === EXCEPTIONS === 160 | 161 | @mark.parametrize("value, error", zip(test_error_single[float], test_error_single['error']), ids=test_error_single[id]) 162 | def test_error(self, value, error): 163 | with raises(error): 164 | SinglePrecision(value) 165 | 166 | 167 | class TestDoublePrecision: 168 | # === ENCODE === 169 | 170 | @mark.parametrize("value, raw_value", zip(test_data_double[float], test_data_double[bytes]), ids=test_data_double[id]) 171 | def test_encode_raw_value(self, value, raw_value): 172 | assert DoublePrecision(value).raw_value == raw_value 173 | 174 | @mark.parametrize("value", test_data_double[float], ids=test_data_double[id]) 175 | def test_encode_value(self, value): 176 | assert DoublePrecision(value).value == value 177 | 178 | def test_encode_negative_nan_raw_value(self, negative_nan): 179 | assert negative_nan.raw_value == b'\x08\xFF\xC0\x00\x00' 180 | 181 | def test_encode_negative_nan_value(self, negative_nan): 182 | assert isnan(negative_nan.value) 183 | 184 | def test_encode_positive_nan_raw_value(self, positive_nan): 185 | assert positive_nan.raw_value == b'\x08\x7F\xC0\x00\x00' 186 | 187 | def test_encode_positive_nan_value(self, positive_nan): 188 | assert isnan(positive_nan.value) 189 | 190 | # === DECODE === 191 | 192 | @mark.parametrize("raw_value", test_data_double[bytes], ids=test_data_double[id]) 193 | def test_decode_raw_value(self, raw_value): 194 | assert DoublePrecision(raw_value).raw_value == raw_value 195 | 196 | @mark.parametrize("value, raw_value", zip(test_data_double[float], test_data_double[bytes]), ids=test_data_double[id]) 197 | def test_decode_value(self, value, raw_value): 198 | assert DoublePrecision(raw_value).value == value 199 | 200 | def test_decode_negative_nan_raw_value(self): 201 | assert DoublePrecision(b'\x11\xFF\xF8\x00\x00\x00\x00\x00\x00').raw_value == \ 202 | b'\x11\xFF\xF8\x00\x00\x00\x00\x00\x00' 203 | 204 | def test_decode_negative_nan_value(self): 205 | assert isnan(DoublePrecision(b'\x11\xFF\xF8\x00\x00\x00\x00\x00\x00').value) 206 | 207 | def test_decode_positive_nan_raw_value(self): 208 | assert DoublePrecision(b'\x11\x7F\xF8\x00\x00\x00\x00\x00\x00').raw_value == \ 209 | b'\x11\x7F\xF8\x00\x00\x00\x00\x00\x00' 210 | 211 | def test_decode_positive_nan_value(self, positive_nan): 212 | assert isnan(DoublePrecision(b'\x11\x7F\xF8\x00\x00\x00\x00\x00\x00').value) 213 | 214 | # === OTHER FIELDS === 215 | 216 | def test_raw_tag(self, double): 217 | assert double.raw_tag == b'\x87' 218 | 219 | def test_tag(self, double): 220 | assert double.tag == 'DoublePrecisionFloatingPoint' 221 | 222 | def test_raw_length(self, double): 223 | assert double.raw_length == b'\x09' 224 | 225 | def test_length(self, double): 226 | assert double.length == 9 227 | 228 | def test_bytes(self, double): 229 | assert bytes(double) == b'\x87\x09\x11\x40\x5E\xDD\x2F\x1A\x9F\xBE\x77' 230 | 231 | def test_len(self, double): 232 | assert len(double) == 11 233 | 234 | # === EXCEPTIONS === 235 | 236 | @mark.parametrize("value, error", zip(test_error_double[float], test_error_double['error']), ids=test_error_double[id]) 237 | def test_error(self, value, error): 238 | with raises(error): 239 | DoublePrecision(value) 240 | -------------------------------------------------------------------------------- /tests/types/test_integer.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from pytest import mark 3 | 4 | from py61850.types.integer import Signed, Unsigned 5 | 6 | 7 | class TestSigned: 8 | S_DATA = { 9 | id: [ 10 | '1_min', '1_max', 11 | '2_min', '2_max', 12 | '4_min', '4_max', 13 | '8_min', '8_max', 14 | ], 15 | int: [ 16 | -0x80, 0x7F, # 1 min/max 17 | -0x8000, 0x7FFF, # 2 min/max 18 | -0x80000000, 0x7FFFFFFF, # 4 min/max 19 | -0x80 ** 0x9, (0x80 ** 0x9) - 1, # 8 min/max 20 | ], 21 | bytes: [ 22 | b'\x80', b'\x7F', # 1 min/max 23 | b'\x80\x00', b'\x7F\xFF', # 2 min/max 24 | b'\x80\x00\x00\x00', b'\x7F\xFF\xFF\xFF', # 4 min/max 25 | b'\x80\x00\x00\x00\x00\x00\x00\x00', b'\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF', # 8 min/max 26 | ], 27 | } 28 | 29 | S_ERROR = { 30 | id: ['value_range_error', 'value_none', 'value_type', 'value_empty'], 31 | int: [0x80 ** 0x9, None, '1', b''], 32 | 'error': [ValueError, TypeError, TypeError, ValueError], 33 | } 34 | 35 | S_FIELDS = { 36 | id: ['regular', 'extreme'], 37 | int: [0x13, -0x7F ** 0x8], 38 | 'raw_length': [b'\x01', b'\x08'], 39 | 'length': [1, 8], 40 | bytes: [b'\x85\x01\x13', b'\x85\x08\xFF\x0F\x91\xBB\xA6\xF9\x03\xFF'], 41 | len: [3, 10], 42 | } 43 | 44 | # === ENCODE === 45 | 46 | @mark.parametrize("value, raw_value", zip(S_DATA[int], S_DATA[bytes]), ids=S_DATA[id]) 47 | def test_encode_raw_value(self, value, raw_value): 48 | assert Signed(value).raw_value == raw_value 49 | 50 | @mark.parametrize("value", S_DATA[int], ids=S_DATA[id]) 51 | def test_encode_value(self, value): 52 | assert Signed(value).value == value 53 | 54 | # === DECODE === 55 | 56 | @mark.parametrize("raw_value", S_DATA[bytes], ids=S_DATA[id]) 57 | def test_decode_raw_value(self, raw_value): 58 | assert Signed(raw_value).raw_value == raw_value 59 | 60 | @mark.parametrize("value, raw_value", zip(S_DATA[int], S_DATA[bytes]), ids=S_DATA[id]) 61 | def test_decode_value(self, value, raw_value): 62 | assert Signed(raw_value).value == value 63 | 64 | # === OTHER FIELDS === 65 | 66 | @mark.parametrize("value", S_FIELDS[int], ids=S_FIELDS[id]) 67 | def test_raw_tag(self, value): 68 | assert Signed(value).raw_tag == b'\x85' 69 | 70 | @mark.parametrize("value", S_FIELDS[int], ids=S_FIELDS[id]) 71 | def test_tag(self, value): 72 | assert Signed(value).tag == 'SignedInteger' 73 | 74 | @mark.parametrize("value, expected", zip(S_FIELDS[int], S_FIELDS['raw_length']), ids=S_FIELDS[id]) 75 | def test_raw_length(self, value, expected): 76 | assert Signed(value).raw_length == expected 77 | 78 | @mark.parametrize("value, expected", zip(S_FIELDS[int], S_FIELDS['length']), ids=S_FIELDS[id]) 79 | def test_length(self, value, expected): 80 | assert Signed(value).length == expected 81 | 82 | @mark.parametrize("value, expected", zip(S_FIELDS[int], S_FIELDS[bytes]), ids=S_FIELDS[id]) 83 | def test_bytes(self, value, expected): 84 | assert bytes(Signed(value)) == expected 85 | 86 | @mark.parametrize("value, expected", zip(S_FIELDS[int], S_FIELDS[len]), ids=S_FIELDS[id]) 87 | def test_len(self, value, expected): 88 | assert len(Signed(value)) == expected 89 | 90 | # === EXCEPTIONS === 91 | 92 | @mark.parametrize("value, error", zip(S_ERROR[int], S_ERROR['error']), ids=S_ERROR[id]) 93 | def test_error(self, value, error): 94 | with raises(error): 95 | Signed(value) 96 | 97 | 98 | class TestUnsigned: 99 | U_DATA = { 100 | id: [ 101 | '1_min', '1_max', 102 | '2_min', '2_max', 103 | '4_min', '4_max', 104 | ], 105 | int: [ 106 | 0, 0xFF, 107 | 0x1FF, 0xFFFF, 108 | 0x1FFFF, 0xFFFFFFFF, 109 | ], 110 | bytes: [ 111 | b'\x00', b'\xFF', 112 | b'\x01\xFF', b'\xFF\xFF', 113 | b'\x00\x01\xFF\xFF', b'\xFF\xFF\xFF\xFF', 114 | ], 115 | } 116 | 117 | U_ERROR = { 118 | id: ['value_range_below', 'value_range_above', 'value_none', 'value_type', 'value_empty'], 119 | int: [-1, 0x1FFFFFFFF, None, '1', b''], 120 | 'error': [ValueError, ValueError, TypeError, TypeError, ValueError], 121 | } 122 | 123 | U_FIELDS = { 124 | id: ['regular', 'extreme'], 125 | int: [0x13, 0x131313], 126 | 'raw_length': [b'\x01', b'\x04'], 127 | 'length': [1, 4], 128 | bytes: [b'\x86\x01\x13', b'\x86\x04\x00\x13\x13\x13'], 129 | len: [3, 6], 130 | } 131 | 132 | # === ENCODE === 133 | 134 | @mark.parametrize("value, raw_value", zip(U_DATA[int], U_DATA[bytes]), ids=U_DATA[id]) 135 | def test_encode_raw_value(self, value, raw_value): 136 | assert Unsigned(value).raw_value == raw_value 137 | 138 | @mark.parametrize("value", U_DATA[int], ids=U_DATA[id]) 139 | def test_encode_value(self, value): 140 | assert Unsigned(value).value == value 141 | 142 | # === DECODE === 143 | 144 | @mark.parametrize("raw_value", U_DATA[bytes], ids=U_DATA[id]) 145 | def test_decode_raw_value(self, raw_value): 146 | assert Unsigned(raw_value).raw_value == raw_value 147 | 148 | @mark.parametrize("value, raw_value", zip(U_DATA[int], U_DATA[bytes]), ids=U_DATA[id]) 149 | def test_decode_value(self, value, raw_value): 150 | assert Unsigned(raw_value).value == value 151 | 152 | # === OTHER FIELDS === 153 | 154 | @mark.parametrize("value", U_FIELDS[int], ids=U_FIELDS[id]) 155 | def test_raw_tag(self, value): 156 | assert Unsigned(value).raw_tag == b'\x86' 157 | 158 | @mark.parametrize("value", U_FIELDS[int], ids=U_FIELDS[id]) 159 | def test_tag(self, value): 160 | assert Unsigned(value).tag == 'UnsignedInteger' 161 | 162 | @mark.parametrize("value, expected", zip(U_FIELDS[int], U_FIELDS['raw_length']), ids=U_FIELDS[id]) 163 | def test_raw_length(self, value, expected): 164 | assert Unsigned(value).raw_length == expected 165 | 166 | @mark.parametrize("value, expected", zip(U_FIELDS[int], U_FIELDS['length']), ids=U_FIELDS[id]) 167 | def test_length(self, value, expected): 168 | assert Unsigned(value).length == expected 169 | 170 | @mark.parametrize("value, expected", zip(U_FIELDS[int], U_FIELDS[bytes]), ids=U_FIELDS[id]) 171 | def test_bytes(self, value, expected): 172 | assert bytes(Unsigned(value)) == expected 173 | 174 | @mark.parametrize("value, expected", zip(U_FIELDS[int], U_FIELDS[len]), ids=U_FIELDS[id]) 175 | def test_len(self, value, expected): 176 | assert len(Unsigned(value)) == expected 177 | 178 | # === EXCEPTIONS === 179 | 180 | @mark.parametrize("value, error", zip(U_ERROR[int], U_ERROR['error']), ids=U_ERROR[id]) 181 | def test_error(self, value, error): 182 | with raises(error): 183 | Unsigned(value) 184 | -------------------------------------------------------------------------------- /tests/types/test_times.py: -------------------------------------------------------------------------------- 1 | from pytest import mark, raises 2 | 3 | from py61850.types.times import Quality, Timestamp 4 | 5 | 6 | class TestQuality: 7 | 8 | test_data = { 9 | id: ['min', 'max', 'generic'], 10 | 'attr': [(False, False, False, 0), (True, True, True, 24), (False, False, True, 7)], 11 | bytes: [b'\x00', b'\xF8', b'\x27'], 12 | } 13 | 14 | test_error = { 15 | id: ['leap', 'fail', 'sync', 'acc', 'acc_range'], 16 | 'attr': [ 17 | ('True', True, True, 24), 18 | (True, 'True', True, 24), 19 | (True, True, 'True', 24), 20 | (True, True, True, '24'), 21 | (True, True, True, 25), 22 | ], 23 | 'error': [TypeError, TypeError, TypeError, TypeError, ValueError] 24 | } 25 | 26 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 27 | def test_encode_attr(self, attr, byte): 28 | leap_second, clock_fail, clock_not_sync, accuracy = attr 29 | assert bytes(Quality(leap_second, clock_fail, clock_not_sync, accuracy)) == byte 30 | 31 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 32 | def test_decode_leap(self, attr, byte): 33 | assert Quality(raw_value=byte).leap_seconds_known == attr[0] 34 | 35 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 36 | def test_decode_fail(self, attr, byte): 37 | assert Quality(raw_value=byte).clock_failure == attr[1] 38 | 39 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 40 | def test_decode_sync(self, attr, byte): 41 | assert Quality(raw_value=byte).clock_not_synchronized == attr[2] 42 | 43 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 44 | def test_decode_accuracy(self, attr, byte): 45 | assert Quality(raw_value=byte).time_accuracy == attr[3] 46 | 47 | def test_decode_accuracy_unspecified(self): 48 | assert Quality(time_accuracy=0x1F).time_accuracy == 'Unspecified' 49 | 50 | # === EXCEPTIONS === 51 | def test_decode_not_byte(self): 52 | assert raises(TypeError, Quality, raw_value='1') 53 | 54 | def test_decode_length(self): 55 | assert raises(ValueError, Quality, raw_value=b'') 56 | 57 | def test_decode_accuracy_range(self): 58 | assert raises(ValueError, Quality, raw_value=b'\x19') 59 | 60 | def test_encode_accuracy_range(self): 61 | assert raises(ValueError, Quality, raw_value=b'\x19') 62 | 63 | @mark.parametrize("attr, error", zip(test_error['attr'], test_error['error']), ids=test_error[id]) 64 | def test_decode_attr(self, attr, error): 65 | leap_second, clock_fail, clock_not_sync, accuracy = attr 66 | assert raises(error, Quality, leap_second, clock_fail, clock_not_sync, accuracy) 67 | 68 | 69 | class TestTimestamp: 70 | 71 | test_data = { 72 | id: ['min', 'extreme', 'generic'], 73 | 'attr': [ 74 | (0.0, Quality(raw_value=b'\x00')), 75 | (1598487698.4095, Quality(raw_value=b'\xF8')), 76 | (1598487698.0, Quality())], 77 | bytes: [ 78 | b'\x00\x00\x00\x00\x00\x00\x00\x00', 79 | b'\x5F\x46\xFC\x92\x00\x0F\xFF\xF8', 80 | b'\x5F\x46\xFC\x92\x00\x00\x00\x20'], 81 | } 82 | 83 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 84 | def test_encode_attr(self, attr, byte): 85 | timestamp, quality = attr 86 | assert Timestamp(timestamp, quality).raw_value == byte 87 | 88 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 89 | def test_decode_timestamp(self, attr, byte): 90 | assert Timestamp(byte).value == attr[0] 91 | 92 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 93 | def test_decode_leap(self, attr, byte): 94 | assert Timestamp(byte).leap_seconds_known == attr[1].leap_seconds_known 95 | 96 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 97 | def test_decode_fail(self, attr, byte): 98 | assert Timestamp(byte).clock_failure == attr[1].clock_failure 99 | 100 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 101 | def test_decode_sync(self, attr, byte): 102 | assert Timestamp(byte).clock_not_synchronized == attr[1].clock_not_synchronized 103 | 104 | @mark.parametrize("attr, byte", zip(test_data['attr'], test_data[bytes]), ids=test_data[id]) 105 | def test_decode_accuracy(self, attr, byte): 106 | assert Timestamp(byte).time_accuracy == attr[1].time_accuracy 107 | 108 | # === EXCEPTIONS === 109 | def test_encode_type(self): 110 | assert raises(TypeError, Timestamp, 123) 111 | 112 | def test_encode_missing_quality(self): 113 | assert raises(TypeError, Timestamp, 123.123) 114 | 115 | def test_decode_length(self): 116 | assert raises(ValueError, Timestamp, b'') 117 | -------------------------------------------------------------------------------- /tests/types/test_visible_string.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | 3 | from py61850.types import VisibleString 4 | 5 | 6 | @fixture 7 | def extreme(): 8 | return VisibleString('a' * 0xFF) 9 | 10 | 11 | @fixture 12 | def none(): 13 | return VisibleString(None) 14 | 15 | 16 | @fixture 17 | def string(): 18 | return VisibleString('string') 19 | 20 | 21 | @fixture 22 | def empty(): 23 | return VisibleString('') 24 | 25 | 26 | # === DECODE === 27 | 28 | def test_byte_string_min_raw_value(): 29 | assert VisibleString(b'a').raw_value == b'a' 30 | 31 | 32 | def test_byte_string_min_value(): 33 | assert VisibleString(b'a').value == 'a' 34 | 35 | 36 | def test_byte_string_max_raw_value(): 37 | assert VisibleString(b'a' * 0xFF).raw_value == b'a' * 0xFF 38 | 39 | 40 | def test_byte_string_max_value(): 41 | assert VisibleString(b'a' * 0xFF).value == 'a' * 0xFF 42 | 43 | 44 | # === NONE === 45 | 46 | def test_none_raw_value(none): 47 | assert none.raw_value is None 48 | 49 | 50 | def test_none_value(none): 51 | assert none.value is None 52 | 53 | 54 | def test_none_raw_length(none): 55 | assert none.raw_length == b'\x00' 56 | 57 | 58 | def test_none_length(none): 59 | assert none.length == 0 60 | 61 | 62 | def test_none_bytes(none): 63 | assert bytes(none) == b'\x8A\x00' 64 | 65 | 66 | def test_none_len(none): 67 | assert len(none) == 2 68 | 69 | 70 | # === EMPTY === 71 | 72 | def test_empty_raw_value(empty): 73 | assert empty.raw_value is None 74 | 75 | 76 | def test_empty_value(empty): 77 | assert empty.value is None 78 | 79 | 80 | def test_empty_raw_length(empty): 81 | assert empty.raw_length == b'\x00' 82 | 83 | 84 | def test_empty_length(empty): 85 | assert empty.length == 0 86 | 87 | 88 | def test_empty_bytes(empty): 89 | assert bytes(empty) == b'\x8A\x00' 90 | 91 | 92 | def test_empty_len(empty): 93 | assert len(empty) == 2 94 | 95 | 96 | # === REGULAR VALUES === 97 | 98 | def test_raw_tag(string): 99 | assert string.raw_tag == b'\x8A' 100 | 101 | 102 | def test_tag(string): 103 | assert string.tag == 'VisibleString' 104 | 105 | 106 | def test_raw_length(string): 107 | assert string.raw_length == b'\x06' 108 | 109 | 110 | def test_length(string): 111 | assert string.length == 6 112 | 113 | 114 | def test_bytes(string): 115 | assert bytes(string) == b'\x8A\x06string' 116 | 117 | 118 | def test_len(string): 119 | assert len(string) == 8 120 | 121 | 122 | # === EXTREME VALUES === 123 | 124 | def test_extreme_raw_length(extreme): 125 | assert extreme.raw_length == b'\x81\xFF' 126 | 127 | 128 | def test_extreme_length(extreme): 129 | assert extreme.length == 0xFF 130 | 131 | 132 | def test_extreme_bytes(extreme): 133 | assert bytes(extreme) == b'\x8A\x81\xFF' + (b'a' * 0xFF) 134 | 135 | 136 | def test_extreme_len(extreme): 137 | assert len(extreme) == 3 + 0xFF 138 | 139 | 140 | # === EXCEPTIONS === 141 | 142 | def test_encode_decode(): 143 | with raises(TypeError): 144 | VisibleString(1) 145 | 146 | 147 | def test_encode_above(): 148 | with raises(ValueError): 149 | VisibleString('a' * 0x1FF) 150 | 151 | 152 | def test_decode_above(): 153 | with raises(ValueError): 154 | VisibleString(b'a' * 0x1FF) 155 | 156 | 157 | # === NONES === 158 | def test_none_empty(): 159 | assert VisibleString().value is None 160 | 161 | 162 | def test_none_string(): 163 | assert VisibleString('').value is None 164 | 165 | 166 | def test_none_byte(): 167 | assert VisibleString(b'').value is None 168 | 169 | 170 | def test_none_none(): 171 | assert VisibleString(None).value is None 172 | --------------------------------------------------------------------------------