From 90ded5f12456a50fb0f6bc648cb563eb5f5c2495 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 18 Oct 2023 07:30:51 +0200 Subject: [PATCH] 1.3 - Updated some obis descriptions - Fixed some type hints - Updated readme - Update CI --- .github/workflows/publish-pypi.yml | 8 ++-- .github/workflows/run_tox.yml | 20 ++++++++-- .pre-commit-config.yaml | 23 +++++++++-- MANIFEST.in | 1 + readme.md | 6 ++- requirements.txt | 4 +- setup.py | 3 ++ src/smllib/__version__.py | 2 +- src/smllib/builder/_builder.py | 4 +- src/smllib/builder/default_context.py | 4 +- src/smllib/const.py | 55 ++++++++++++++++++--------- src/smllib/reader.py | 2 +- src/smllib/sml/message.py | 3 +- src/smllib/sml/sml_choice.py | 8 ++-- src/smllib/sml_frame.py | 2 +- tests/builder/test_build.py | 6 +-- tests/sml/test_format.py | 4 +- tests/test_frames.py | 23 ++++++++++- tests/test_sml_stream_reader.py | 6 +-- 19 files changed, 130 insertions(+), 54 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index c7e3a30..15a12ec 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: master - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: '3.10' - name: Install setuptools run: | diff --git a/.github/workflows/run_tox.yml b/.github/workflows/run_tox.yml index b1dfb09..653dbbb 100644 --- a/.github/workflows/run_tox.yml +++ b/.github/workflows/run_tox.yml @@ -3,17 +3,29 @@ name: Tests on: [push, pull_request] jobs: - test: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: pre-commit/action@v3.0.0 + + + tests: + needs: pre-commit runs-on: ubuntu-latest strategy: max-parallel: 2 matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 515bb5d..52c7914 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,33 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.5.0 hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-docstring-first + - id: check-merge-conflict + # - id: check-toml - id: check-yaml + - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.12.0 hooks: - id: isort name: isort (python) - - repo: https://gitlab.com/PyCQA/flake8 - rev: '3.9.1' + - repo: https://github.com/PyCQA/flake8 + rev: '6.1.0' hooks: - id: flake8 + additional_dependencies: + - flake8-bugbear==23.9.16 + - flake8-comprehensions==3.14 + - flake8-noqa==1.3 + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/MANIFEST.in b/MANIFEST.in index 0f8cae2..029c3b4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE include src/smllib/py.typed +include py.typed diff --git a/readme.md b/readme.md index c463124..7376cb2 100644 --- a/readme.md +++ b/readme.md @@ -30,7 +30,8 @@ if sml_frame is None: stream.add(b'BytesFromSerialPort') sml_frame = stream.get_frame() -# Shortcut to extract all values without parsing the whole frame +# A quick Shortcut to extract all values without parsing the whole frame +# In rare cases this might raise an InvalidBufferPos exception, then you have to use sml_frame.parse_frame() obis_values = sml_frame.get_obis() # return all values but slower @@ -39,6 +40,9 @@ for msg in parsed_msgs: # prints a nice overview over the received values print(msg.format_msg()) +# In the parsed message the obis values are typically found like this +obis_values = parsed_msgs[1].message_body.val_list + # The obis attribute of the SmlListEntry carries different obis representations as attributes list_entry = obis_values[0] print(list_entry.obis) # 0100010800ff diff --git a/requirements.txt b/requirements.txt index ccab2e5..149ad68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pre-commit==2.20.0 -pytest==7.1.3 +pytest == 7.4.2 +pre-commit == 3.5.0 diff --git a/setup.py b/setup.py index 97b5f9a..8164c39 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,9 @@ def load_version() -> str: "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Home Automation" ], diff --git a/src/smllib/__version__.py b/src/smllib/__version__.py index 64477cf..6f4fa58 100644 --- a/src/smllib/__version__.py +++ b/src/smllib/__version__.py @@ -1 +1 @@ -__version__ = '1.2' +__version__ = '1.3' diff --git a/src/smllib/builder/_builder.py b/src/smllib/builder/_builder.py index 6e26383..7cebca8 100644 --- a/src/smllib/builder/_builder.py +++ b/src/smllib/builder/_builder.py @@ -1,4 +1,4 @@ -from typing import Dict, Generic, Tuple, Type, TypeVar +from typing import Dict, Generic, Type, TypeVar from smllib.errors import WrongArgCount, WrongValueType from smllib.sml import inspect_obj, SmlBaseObj, SmlObjFieldInfo, T_SML_OBJ @@ -19,7 +19,7 @@ def build(self, obj: SmlFrameSnippet, classes: Dict[Type[SmlBaseObj], 'SmlObjBui raise WrongArgCount() out = self.BUILDS() - for i, a in enumerate(self.fields.items()): # type: int, Tuple[str, SmlObjFieldInfo] + for i, a in enumerate(self.fields.items()): # type: int, tuple[str, SmlObjFieldInfo] name, field = a value = lst[i].value diff --git a/src/smllib/builder/default_context.py b/src/smllib/builder/default_context.py index e2e77f9..517133c 100644 --- a/src/smllib/builder/default_context.py +++ b/src/smllib/builder/default_context.py @@ -1,11 +1,11 @@ from typing import Dict, Type -from smllib.sml import SmlCloseResponse, SmlGetListResponse, SmlListEntry, SmlMessage, SmlOpenResponse, T_SML_OBJ +from smllib.sml import SmlBaseObj, SmlCloseResponse, SmlGetListResponse, SmlListEntry, SmlMessage, SmlOpenResponse from . import SmlCloseResponseBuilder, SmlGetListResponseBuilder, \ SmlListEntryBuilder, SmlMessageBuilder, SmlOpenResponseBuilder, T_SML_BUILDER -CTX_HINT = Dict[Type[T_SML_OBJ], T_SML_BUILDER] +CTX_HINT = Dict[Type[SmlBaseObj], T_SML_BUILDER] def create_context() -> CTX_HINT: diff --git a/src/smllib/const.py b/src/smllib/const.py index dd9c876..7aa0483 100644 --- a/src/smllib/const.py +++ b/src/smllib/const.py @@ -72,24 +72,46 @@ # https://www.promotic.eu/en/pmdoc/Subsystems/Comm/PmDrivers/IEC62056_OBIS.htm OBIS_NAMES = { '0100000009ff': 'Geräteeinzelidentifikation', - '0100010800ff': 'Zählerstand Total', - '0100010801ff': 'Zählerstand Tarif 1', - '0100010802ff': 'Zählerstand Tarif 2', + '0100010800ff': 'Zählerstand Bezug Total', + '0100010801ff': 'Zählerstand Bezug Tarif 1', + '0100010802ff': 'Zählerstand Bezug Tarif 2', + '0100010803ff': 'Zählerstand Bezug Tarif 3', + '0100010804ff': 'Zählerstand Bezug Tarif 4', '0100011100ff': 'Total-Zählerstand', - '0100020800ff': 'Wirkenergie Total', + '0100020800ff': 'Zählerstand Einspeisung Total', + '0100020801ff': 'Zählerstand Einspeisung Tarif 1', + '0100020802ff': 'Zählerstand Einspeisung Tarif 2', + '0100020803ff': 'Zählerstand Einspeisung Tarif 3', + '0100020804ff': 'Zählerstand Einspeisung Tarif 4', + '01000b0600ff': 'Maximalstrom Total', + '01000b0700ff': 'Momentanstrom Total', + '01000c0700ff': 'Momentanspannung Total', + '01000e0700ff': 'Netzfrequenz', '0100100700ff': 'aktuelle Wirkleistung', - '0100170700ff': 'Momentanblindleistung L1', - '01001f0700ff': 'Strom L1', - '0100200700ff': 'Spannung L1', - '0100240700ff': 'Wirkleistung L1', - '01002b0700ff': 'Momentanblindleistung L2', - '0100330700ff': 'Strom L2', - '0100340700ff': 'Spannung L2', - '0100380700ff': 'Wirkleistung L2', - '01003f0700ff': 'Momentanblindleistung L3', - '0100470700ff': 'Strom L3', - '0100480700ff': 'Spannung L3', - '01004c0700ff': 'Wirkleistung L3', + '0100150700ff': 'Momentanwirkleistung Bezug L1', + '0100160700ff': 'Momentanwirkleistung Einspeisung L1', + '0100170700ff': 'Momentanblindleistung Bezug L1', + '0100180700ff': 'Momentanblindleistung Einspeisung L1', + '01001f0600ff': 'Maximalstrom L1', + '01001f0700ff': 'Momentanstrom L1', + '0100200700ff': 'Momentanspannung L1', + '0100240700ff': 'Summenwirkleistung L1', + '0100290700ff': 'Momentanwirkleistung Bezug L2', + '01002a0700ff': 'Momentanwirkleistung Einspeisung L2', + '01002b0700ff': 'Momentanblindleistung Bezug L2', + '01002c0700ff': 'Momentanblindleistung Einspeisung L2', + '0100330600ff': 'Maximalstrom L2', + '0100330700ff': 'Momentanstrom L2', + '0100340700ff': 'Momentanspannung L2', + '0100380700ff': 'Summenwirkleistung L2', + '01003d0700ff': 'Momentanwirkleistung Bezug L3', + '01003e0700ff': 'Momentanwirkleistung Einspeisung L3', + '01003f0700ff': 'Momentanblindleistung Bezug L3', + '0100400700ff': 'Momentanblindleistung Einspeisung L3', + '0100470600ff': 'Maximalstrom L3', + '0100470700ff': 'Momentanstrom L3', + '0100480700ff': 'Momentanspannung L3', + '01004c0700ff': 'Summenwirkleistung L3', '0100510701ff': 'Phasenabweichung Spannungen L1/L2', '0100510702ff': 'Phasenabweichung Spannungen L1/L3', '0100510704ff': 'Phasenabweichung Strom/Spannung L1', @@ -101,7 +123,6 @@ '010060320005': 'Gemittelte Chiptemperatur', '010060320303': 'Spannungsminimum', '010060320304': 'Spannungsmaximum', - '01000e0700ff': 'Netz Frequenz', '8181c78203ff': 'Hersteller-Identifikation', '8181c78205ff': 'Öffentlicher Schlüssel', } diff --git a/src/smllib/reader.py b/src/smllib/reader.py index e7afd7f..1c16365 100644 --- a/src/smllib/reader.py +++ b/src/smllib/reader.py @@ -9,7 +9,7 @@ class SmlStreamReader: MAX_SIZE = 50 * 1024 - def __init__(self, build_ctx: CTX_HINT = None): + def __init__(self, build_ctx: Optional[CTX_HINT] = None): self.bytes: bytes = b'' self.build_ctx: CTX_HINT = build_ctx if build_ctx is not None else create_context() diff --git a/src/smllib/sml/message.py b/src/smllib/sml/message.py index 3ecca1b..e173ed3 100644 --- a/src/smllib/sml/message.py +++ b/src/smllib/sml/message.py @@ -1,9 +1,8 @@ from typing import Dict, Type, Union from smllib.sml import SmlBaseObj, SmlChoice, SmlCloseResponse, SmlGetListResponse, SmlObjFieldInfo, SmlOpenResponse -from smllib.sml._base_obj import T_SML_OBJ as _T_SML_OBJ -MSG_TYPES: Dict[int, Type[_T_SML_OBJ]] = { +MSG_TYPES: Dict[int, Type[SmlBaseObj]] = { # 0x0100: 'SmlOpenRequest', 0x0101: SmlOpenResponse, # 0x0200: 'SmlCloseRequest', diff --git a/src/smllib/sml/sml_choice.py b/src/smllib/sml/sml_choice.py index a424248..87be9c1 100644 --- a/src/smllib/sml/sml_choice.py +++ b/src/smllib/sml/sml_choice.py @@ -1,15 +1,15 @@ -from typing import Any, List, Mapping, Tuple, Type, Union +from typing import Final, Generic, List, Mapping, Tuple, Type from smllib.errors import UnsupportedChoiceValue, WrongArgCount from smllib.sml import T_SML_OBJ from smllib.sml_frame_snippet import SmlFrameSnippet -class SmlChoice: +class SmlChoice(Generic[T_SML_OBJ]): def __init__(self, choices: Mapping[int, Type[T_SML_OBJ]]): - self.choices = choices + self.choices: Final = choices - def get(self, obj: List[SmlFrameSnippet]) -> Union[Tuple[None, Any], Tuple[Type[T_SML_OBJ], Any]]: + def get(self, obj: List[SmlFrameSnippet]) -> Tuple[Type[T_SML_OBJ], SmlFrameSnippet]: if len(obj) != 2: raise WrongArgCount() diff --git a/src/smllib/sml_frame.py b/src/smllib/sml_frame.py index 68028b5..63df94b 100644 --- a/src/smllib/sml_frame.py +++ b/src/smllib/sml_frame.py @@ -7,7 +7,7 @@ class SmlFrame: - def __init__(self, buffer: bytes, build_ctx: CTX_HINT = None, msg_ctx: Optional[bytes] = None): + def __init__(self, buffer: bytes, build_ctx: Optional[CTX_HINT] = None, msg_ctx: Optional[bytes] = None): self.bytes = buffer self.buffer = memoryview(buffer) self.buf_len = len(buffer) diff --git a/tests/builder/test_build.py b/tests/builder/test_build.py index cac2fed..0cec764 100644 --- a/tests/builder/test_build.py +++ b/tests/builder/test_build.py @@ -1,6 +1,6 @@ from smllib.builder import SmlCloseResponseBuilder, SmlGetListResponseBuilder, \ SmlListEntryBuilder, SmlMessageBuilder, SmlObjBuilder -from smllib.sml import EndOfSmlMsg, SmlCloseResponse, SmlGetListResponse, SmlListEntry +from smllib.sml import EndOfSmlMsg, SmlCloseResponse, SmlListEntry from tests.helper import in_snip @@ -21,7 +21,7 @@ def test_build_entry_list(): builder = SmlGetListResponseBuilder() - obj = builder.build(data, {SmlListEntry: SmlListEntryBuilder()}) # type: SmlGetListResponse + obj = builder.build(data, {SmlListEntry: SmlListEntryBuilder()}) assert obj.server_id == 'server' assert obj.val_list[0].obis == '0100010800ff' assert obj.val_list[0].value == 'val1' @@ -36,7 +36,7 @@ def build(self, obj: list, classes): ret.obis += '_patched' return ret - obj = builder.build(data, {SmlListEntry: PatchedBuilder()}) # type: SmlGetListResponse + obj = builder.build(data, {SmlListEntry: PatchedBuilder()}) assert obj.server_id == 'server' assert obj.val_list[0].obis == '0100010800ff_patched' assert obj.val_list[0].value == 'val1' diff --git a/tests/sml/test_format.py b/tests/sml/test_format.py index 3aaaf2b..59fd9e9 100644 --- a/tests/sml/test_format.py +++ b/tests/sml/test_format.py @@ -46,7 +46,7 @@ def test_list_entry(): ' scaler : None\n' \ ' value : val1\n' \ ' value_signature: None\n' \ - ' -> (Zählerstand Total)\n' \ + ' -> (Zählerstand Bezug Total)\n' \ ' \n' \ ' obis : 0100010801ff (1-0:1.8.1*255)\n' \ ' status : None\n' \ @@ -55,6 +55,6 @@ def test_list_entry(): ' scaler : None\n' \ ' value : val2\n' \ ' value_signature: None\n' \ - ' -> (Zählerstand Tarif 1)\n' \ + ' -> (Zählerstand Bezug Tarif 1)\n' \ ' list_signature : None\n' \ ' act_gateway_time: None\n' diff --git a/tests/test_frames.py b/tests/test_frames.py index d374c1e..26cd2f2 100644 --- a/tests/test_frames.py +++ b/tests/test_frames.py @@ -58,6 +58,18 @@ b'019EBD01010163F08A007605011FBC8562006200726500000201710163E38F000000001B1B1B1B1A03509F', id='Frame Issue #8 (FIXED)' ), + # + # This is a frame where the shortcut fails + # + # pytest.param( + # b'1b1b1b1b010101017605077707006200620072630101760107ffffffffffff05027d02560b0a01454d4800009f3846726201650' + # b'27d082b62016312980076050777070162006200726307017707ffffffffffff0b0a01454d4800009f3846070100620affff7262' + # b'0165027d082b7577070100603201010101010104454d480177070100600100ff010101010b0a01454d4800009f3846017707010' + # b'0010800ff641c010472620165027d082b621e52ff6501ddf5f40177070100020800ff0172620165027d082b621e52ff6501d4dc' + # b'0d0177070100100700ff0101621b520053039f01010163ec0100760507770702620062007263020171016344d5001b1b1b1b1a0' + # b'08aa9', + # id='Frame Issue #15 (FIXED)' + # ), ) ) def test_frames(frame): @@ -112,6 +124,14 @@ def test_frames(frame): b'020171016357f100', id='Frame3' ), + pytest.param( + b'76050550750262006200726301017601010501c57c560b0a0149534b0005020de27262016501c57c07620163c497007605055075' + b'03620062007263070177010b0a0149534b0005020de2070100620affff7262016501c57c07757707010060320101010101010449' + b'534b0177070100600100ff010101010b0a0149534b0005020de20177070100010800ff65001c410401621e52ff6501d3c8d80177' + b'070100020800ff0101621e52ff650001fe010177070100100700ff0101621b52005300ad010101639e9c00760505507504620062' + b'0072630201710163b71b00', + id='Frame3' + ), ) ) def test_frame_only(data): @@ -128,5 +148,6 @@ def test_frame_only(data): assert len(obis_values) >= 4, obis_values for obis in obis_values: - obis.format_msg() + print(obis.format_msg()) obis.get_value() + break diff --git a/tests/test_sml_stream_reader.py b/tests/test_sml_stream_reader.py index ba23e17..c934924 100644 --- a/tests/test_sml_stream_reader.py +++ b/tests/test_sml_stream_reader.py @@ -41,6 +41,6 @@ def test_msg_long_list(): r = SmlStreamReader() r.add(msg2) - f = r.get_frame() - for f in f.parse_frame(): - f.format_msg() + frame = r.get_frame() + for msg in frame.parse_frame(): + msg.format_msg()