From 416b9a8469c633304f67b2206621a5df259ec54b Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 10:51:22 +0200 Subject: [PATCH 01/10] fixed linting and added more documentation. set workflow python version to 3.9, the minimal version. --- .github/workflows/release-commhandler.yml | 6 +- .github/workflows/release-specification.yml | 8 +- .vscode/settings.json | 5 +- commhandler/docs/index.rst | 10 +- commhandler/src/sgr_commhandler/__init__.py | 6 + .../src/sgr_commhandler/api/__init__.py | 6 +- .../api/configuration_parameter.py | 8 +- .../src/sgr_commhandler/api/data_point_api.py | 14 +- .../src/sgr_commhandler/api/data_types.py | 4 + .../src/sgr_commhandler/api/device_api.py | 4 + .../sgr_commhandler/api/dynamic_parameter.py | 10 +- .../api/functional_profile_api.py | 3 +- .../sgr_commhandler/declaration_library.py | 7 + .../src/sgr_commhandler/device_builder.py | 20 +-- .../src/sgr_commhandler/driver/__init__.py | 3 + .../driver/contact/__init__.py | 4 + .../driver/contact/contact_interface_async.py | 4 + .../driver/generic/__init__.py | 4 + .../driver/generic/generic_interface_async.py | 4 + .../driver/messaging/__init__.py | 4 + .../messaging/messaging_client_async.py | 4 + .../driver/messaging/messaging_filter.py | 11 +- .../messaging/messaging_interface_async.py | 12 +- .../sgr_commhandler/driver/modbus/__init__.py | 4 + .../driver/modbus/modbus_client_async.py | 26 ++-- .../driver/modbus/modbus_interface_async.py | 40 ++++-- .../driver/modbus/payload_decoder.py | 5 + .../driver/modbus/shared_client.py | 72 +++++++++- .../sgr_commhandler/driver/rest/__init__.py | 4 + .../driver/rest/authentication.py | 136 +++++++++--------- .../sgr_commhandler/driver/rest/request.py | 6 +- .../driver/rest/restapi_interface_async.py | 8 +- .../src/sgr_commhandler/utils/__init__.py | 4 +- .../sgr_commhandler/utils/jmespath_mapping.py | 20 ++- .../src/sgr_commhandler/utils/template.py | 5 +- .../src/sgr_commhandler/utils/value_util.py | 4 + .../sgr_commhandler/validators/__init__.py | 4 + .../sgr_commhandler/validators/resolver.py | 4 + .../sgr_commhandler/validators/validator.py | 19 ++- .../tests/test_devices/test_device_builder.py | 24 +++- .../test_devices/test_device_introspection.py | 4 + commhandler/tests/test_utils/test_jmespath.py | 2 +- 42 files changed, 399 insertions(+), 153 deletions(-) diff --git a/.github/workflows/release-commhandler.yml b/.github/workflows/release-commhandler.yml index 1667f8b..a3828f3 100644 --- a/.github/workflows/release-commhandler.yml +++ b/.github/workflows/release-commhandler.yml @@ -46,9 +46,9 @@ jobs: echo "::set-output name=semver::$SEMVER" - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: '3.9' - name: Install Dependencies run: | @@ -60,7 +60,7 @@ jobs: python --version - name: Checkout SGrPython Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ steps.set_tag.outputs.original_tag }} diff --git a/.github/workflows/release-specification.yml b/.github/workflows/release-specification.yml index 9662c85..d05a57b 100644 --- a/.github/workflows/release-specification.yml +++ b/.github/workflows/release-specification.yml @@ -45,9 +45,9 @@ jobs: echo "::set-output name=semver::$SEMVER" - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: '3.9' - name: Install Dependencies run: | @@ -60,14 +60,14 @@ jobs: pip show xsdata - name: Checkout SGrSpecifications Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'SmartGridready/SGrSpecifications' ref: ${{ steps.set_tag.outputs.original_tag }} path: SGrSpecifications - name: Checkout SGrPython Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: SGrPython diff --git a/.vscode/settings.json b/.vscode/settings.json index a6735e5..b820a95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "python.analysis.typeCheckingMode": "off" + "python.analysis.typeCheckingMode": "off", + "python.analysis.extraPaths": [ + "./commhandler/src" + ] } \ No newline at end of file diff --git a/commhandler/docs/index.rst b/commhandler/docs/index.rst index 82d33c4..936a056 100644 --- a/commhandler/docs/index.rst +++ b/commhandler/docs/index.rst @@ -3,15 +3,15 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -SGr CommHandler documentation +SGr CommHandler Documentation ============================= -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. +This is the API documentation of the `sgr-commhandler `_ Python package, +providing the *SmartGridready communications handler library*. + +See `SGrPython `_ Github repository for details. .. toctree:: :maxdepth: 2 :caption: Contents: - diff --git a/commhandler/src/sgr_commhandler/__init__.py b/commhandler/src/sgr_commhandler/__init__.py index e69de29..9a81d3a 100644 --- a/commhandler/src/sgr_commhandler/__init__.py +++ b/commhandler/src/sgr_commhandler/__init__.py @@ -0,0 +1,6 @@ +""" +Provides the CommHandler library. + +Users can utilize the device_builder and declaration_library modules to +create devices and access the declaration library. +""" diff --git a/commhandler/src/sgr_commhandler/api/__init__.py b/commhandler/src/sgr_commhandler/api/__init__.py index cf9d88f..5986f7f 100644 --- a/commhandler/src/sgr_commhandler/api/__init__.py +++ b/commhandler/src/sgr_commhandler/api/__init__.py @@ -1,3 +1,7 @@ +""" +Contains the CommHandler API, which is intended to be utilized by the user. +""" + __all__ = [ "SGrBaseInterface", "FunctionalProfile", @@ -26,4 +30,4 @@ ) from sgr_commhandler.api.functional_profile_api import ( FunctionalProfile -) \ No newline at end of file +) diff --git a/commhandler/src/sgr_commhandler/api/configuration_parameter.py b/commhandler/src/sgr_commhandler/api/configuration_parameter.py index 033aefa..bda32aa 100644 --- a/commhandler/src/sgr_commhandler/api/configuration_parameter.py +++ b/commhandler/src/sgr_commhandler/api/configuration_parameter.py @@ -1,3 +1,7 @@ +""" +Provides device configuration parameters. +""" + from typing import Optional from sgr_specification.v0.product import ( @@ -14,7 +18,7 @@ def build_configuration_parameters(params: Optional[ConfigurationList]) -> list[ ---------- params : Optional[ConfigurationList] The configuration list of an EID - + Returns ------- list[ConfigurationParameter] @@ -40,7 +44,7 @@ def __init__(self, parameter: ConfigurationListElement): ---------- parameter : ConfigurationListElement A configuration list element of the SGr specification - """ + """ translation = parameter.configuration_description self.label = translation[0].label self.name = parameter.name diff --git a/commhandler/src/sgr_commhandler/api/data_point_api.py b/commhandler/src/sgr_commhandler/api/data_point_api.py index 34b1323..216c886 100644 --- a/commhandler/src/sgr_commhandler/api/data_point_api.py +++ b/commhandler/src/sgr_commhandler/api/data_point_api.py @@ -1,3 +1,7 @@ +""" +Provides the data-point-level API. +""" + from collections.abc import Callable from typing import Any, Generic, NoReturn, Optional, Protocol, TypeVar @@ -66,7 +70,7 @@ class DataPointProtocol(Protocol[TDpSpec]): def get_specification(self) -> TDpSpec: """ Gets the data point specification. - + Returns ------- TDpSpec @@ -95,7 +99,7 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: optional dynamic parameters of the request skip_cache : bool does not use cache if true - + Returns ------- Any @@ -208,7 +212,7 @@ def name(self) -> tuple[str, str]: """ return self._protocol.name() - async def get_value_async(self, parameters: Optional[dict[str, str]] = None, skip_cache = False) -> Any: + async def get_value_async(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: """ Gets the data point value asynchronously. @@ -216,7 +220,7 @@ async def get_value_async(self, parameters: Optional[dict[str, str]] = None, ski ------- Any the data point value - + Raises ------ Exception @@ -280,7 +284,7 @@ def data_type(self) -> DataTypes: the data point data type """ return self._validator.data_type() - + def unit(self) -> Units: """ Gets the unit of measurement of the data point. diff --git a/commhandler/src/sgr_commhandler/api/data_types.py b/commhandler/src/sgr_commhandler/api/data_types.py index b8452ba..213c901 100644 --- a/commhandler/src/sgr_commhandler/api/data_types.py +++ b/commhandler/src/sgr_commhandler/api/data_types.py @@ -1,3 +1,7 @@ +""" +Provides SGr data types. +""" + from enum import Enum diff --git a/commhandler/src/sgr_commhandler/api/device_api.py b/commhandler/src/sgr_commhandler/api/device_api.py index 68a66ba..09632c0 100644 --- a/commhandler/src/sgr_commhandler/api/device_api.py +++ b/commhandler/src/sgr_commhandler/api/device_api.py @@ -1,3 +1,7 @@ +""" +Provides the device-level API. +""" + from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Optional, Protocol diff --git a/commhandler/src/sgr_commhandler/api/dynamic_parameter.py b/commhandler/src/sgr_commhandler/api/dynamic_parameter.py index e655a45..57069e5 100644 --- a/commhandler/src/sgr_commhandler/api/dynamic_parameter.py +++ b/commhandler/src/sgr_commhandler/api/dynamic_parameter.py @@ -1,3 +1,7 @@ +""" +Provides dynamic request parameters. +""" + import logging from typing import Optional @@ -17,7 +21,7 @@ def build_dynamic_parameters(params: Optional[DynamicParameterDescriptionList]) ---------- params : Optional[DynamicParameterDescriptionList] The dynamic parameter list of a data point - + Returns ------- list[DynamicParameter] @@ -42,7 +46,7 @@ def build_dynamic_parameter_substitutions(dynamic_parameters: list['DynamicParam the dynamic parameters as specified input_parameters: Optional[dict[str, str]] the actual parameters given to the request - + Returns ------- Dict[str, str] @@ -72,7 +76,7 @@ def __init__(self, parameter: DynamicParameterDescriptionListElement): ---------- parameter : DynamicParameterDescriptionListElement A dynamic parameter list element of the SGr specification - """ + """ translation = parameter.parameter_description self.label = translation[0].label self.name = parameter.name diff --git a/commhandler/src/sgr_commhandler/api/functional_profile_api.py b/commhandler/src/sgr_commhandler/api/functional_profile_api.py index 3e6ff69..b292844 100644 --- a/commhandler/src/sgr_commhandler/api/functional_profile_api.py +++ b/commhandler/src/sgr_commhandler/api/functional_profile_api.py @@ -9,6 +9,7 @@ """Defines a generic data type.""" TFpSpec = TypeVar('TFpSpec', covariant=True, bound=FunctionalProfileBase) + class FunctionalProfile(Protocol[TFpSpec]): """ Implements a functional profile. @@ -66,7 +67,7 @@ async def get_values_async(self, parameters: Optional[dict[str, str]] = None) -> try: value = await dp.get_value_async(parameters) data[key] = value - except Exception as e: + except Exception: # TODO log error - None should not be a valid DP value data[key] = None return data diff --git a/commhandler/src/sgr_commhandler/declaration_library.py b/commhandler/src/sgr_commhandler/declaration_library.py index 486e8f1..0d90509 100644 --- a/commhandler/src/sgr_commhandler/declaration_library.py +++ b/commhandler/src/sgr_commhandler/declaration_library.py @@ -1,7 +1,14 @@ +""" +Provides a client for the declaration library's REST API. +""" + from typing import Any import requests + LIB_BASE_URL = 'https://library.smartgridready.ch' +"""The base URL of the library REST API.""" + def get_product_eid_xml(name: str) -> str: """ diff --git a/commhandler/src/sgr_commhandler/device_builder.py b/commhandler/src/sgr_commhandler/device_builder.py index 8acb200..a69b57c 100644 --- a/commhandler/src/sgr_commhandler/device_builder.py +++ b/commhandler/src/sgr_commhandler/device_builder.py @@ -1,3 +1,7 @@ +""" +Provides a device builder to create device instances from external interface descriptions (EID). +""" + import logging import configparser import re @@ -150,7 +154,7 @@ def eid_path(self, file_path: str) -> 'DeviceBuilder': ---------- file_path: str the path to the EID XML file - + Returns ------- DeviceBuilder @@ -168,7 +172,7 @@ def eid(self, xml: str): ---------- xml: str the EID XML content - + Returns ------- DeviceBuilder @@ -186,7 +190,7 @@ def properties_path(self, file_path: str): ---------- file_path: str the path to the property file - + Returns ------- DeviceBuilder @@ -204,7 +208,7 @@ def properties(self, properties: dict): ---------- properties: dict the properties - + Returns ------- DeviceBuilder @@ -262,7 +266,7 @@ def parse_device_frame(content: str) -> DeviceFrame: ---------- content: str the EID XML content - + Returns ------- DeviceFrame @@ -283,7 +287,7 @@ def replace_variables(content: str, parameters: dict) -> str: the EID XML content parameters: dict the configuration parameters - + Returns ------- str @@ -308,7 +312,7 @@ def build_properties(config: list[ConfigurationParameter], properties: dict) -> the EID configuration parameters properties: dict the properties to configure - + Returns ------- dict @@ -335,5 +339,5 @@ def validate_schema(content: str): the EID XML content """ xsd_path = importlib.resources.files(sgr_schema).joinpath('SGrIncluder.xsd') - xsd = xmlschema.XMLSchema(xsd_path) + xsd = xmlschema.XMLSchema(str(xsd_path)) xsd.validate(content) diff --git a/commhandler/src/sgr_commhandler/driver/__init__.py b/commhandler/src/sgr_commhandler/driver/__init__.py index e69de29..8f492a7 100644 --- a/commhandler/src/sgr_commhandler/driver/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/__init__.py @@ -0,0 +1,3 @@ +""" +Contains all communication interface driver implementations. +""" diff --git a/commhandler/src/sgr_commhandler/driver/contact/__init__.py b/commhandler/src/sgr_commhandler/driver/contact/__init__.py index 2da0cf8..7c1e3b9 100644 --- a/commhandler/src/sgr_commhandler/driver/contact/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/contact/__init__.py @@ -1,3 +1,7 @@ +""" +Provides the digital contact interface driver. +""" + __all__ = ["ContactDataPoint", "ContactFunctionalProfile", "SGrContactInterface"] from .contact_interface_async import ( diff --git a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py index b67dbba..c7d3373 100644 --- a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py @@ -1,3 +1,7 @@ +""" +Provides the digital contact interface implementation. +""" + import logging from typing import Any, Optional diff --git a/commhandler/src/sgr_commhandler/driver/generic/__init__.py b/commhandler/src/sgr_commhandler/driver/generic/__init__.py index 1d46108..9b8a5f7 100644 --- a/commhandler/src/sgr_commhandler/driver/generic/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/generic/__init__.py @@ -1,3 +1,7 @@ +""" +Provides the generic interface driver. +""" + __all__ = ["GenericDataPoint", "GenericFunctionalProfile", "SGrGenericInterface"] from .generic_interface_async import ( diff --git a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py index 9746ffa..950f79f 100644 --- a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py @@ -1,3 +1,7 @@ +""" +Provides the generic interface implementation. +""" + import logging from typing import Any, Optional diff --git a/commhandler/src/sgr_commhandler/driver/messaging/__init__.py b/commhandler/src/sgr_commhandler/driver/messaging/__init__.py index e55be89..4bc9fd4 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/__init__.py @@ -1,3 +1,7 @@ +""" +Provides the messaging interface driver. +""" + __all__ = ["MessagingDataPoint", "MessagingFunctionalProfile", "SGrMessagingInterface"] from .messaging_interface_async import ( diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_client_async.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_client_async.py index 885d6c0..b529200 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_client_async.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_client_async.py @@ -1,3 +1,7 @@ +""" +Provides the messaging client implementation. +""" + import logging from abc import ABC from typing import Any, Callable, NoReturn, Optional diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py index ad0527a..8594d2d 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py @@ -1,3 +1,7 @@ +""" +Provides message filter implementations. +""" + import re import json import jmespath @@ -13,6 +17,7 @@ T = TypeVar('T') + class MessagingFilter(Generic[T]): """ The base class for message filter implementations. @@ -36,7 +41,7 @@ def __init__(self, filter_spec: JmespathFilterType): def is_filter_match(self, payload: Any) -> bool: ret_value = str(payload) regex = self._filter_spec.matches_regex or '.' - if self._filter_spec.query: + if self._filter_spec.query: ret_value = json.dumps(jmespath.search(self._filter_spec.query, json.loads(payload))) match = re.match(regex, ret_value) @@ -74,7 +79,7 @@ def is_filter_match(self, payload: Any) -> bool: query_match = re.match(self._filter_spec.query, ret_value) if query_match is not None: ret_value = query_match.group() - + match = re.match(regex, ret_value) return match is not None @@ -87,7 +92,7 @@ def get_messaging_filter(filter: MessageFilter) -> Optional[MessagingFilter]: ---------- filter : MessageFilter the filter specification - + Returns ------- Optional[MessagingFilter] diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py index 6c04f9f..3053ed3 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py @@ -145,7 +145,7 @@ def __init__( self._handle_message: Optional[Callable[[DataPointProtocol, Any], NoReturn]] = None self._interface = interface if self._in_cmd and self._in_cmd.topic: - self._interface.data_point_handler((self._fp_name, self._dp_name, self._in_cmd.topic, self._on_message, self._in_filter)) # type: ignore + self._interface.data_point_handler((self._fp_name, self._dp_name, self._in_cmd.topic, self._on_message, self._in_filter)) # type: ignore def name(self) -> tuple[str, str]: return self._fp_name, self._dp_name @@ -173,7 +173,7 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: await self._read_response_event.wait() logger.debug('finished active reading...') - return self._cached_value + return self._cached_value async def set_val(self, value: Any): if not self._write_cmd or not self._write_cmd.topic: @@ -222,13 +222,13 @@ def unit(self) -> Units: def can_subscribe(self) -> bool: return True - def subscribe(self, fn: Callable[[DataPointProtocol, Any], NoReturn]): # type: ignore + def subscribe(self, fn: Callable[[DataPointProtocol, Any], NoReturn]): # type: ignore self._handle_message = fn - def unsubscribe(self): # type: ignore + def unsubscribe(self): # type: ignore self._handle_message = None - def _on_message(self, payload: Any): # type: ignore + def _on_message(self, payload: Any): # type: ignore ret_value: Any if ( self._in_cmd @@ -354,7 +354,7 @@ def __init__( # configure interface self._client = get_messaging_client(str(self.device_frame.device_name), desc) - self._client.set_message_handler(self.handle_client_message) # type: ignore + self._client.set_message_handler(self.handle_client_message) # type: ignore # subscribe to topic once, multiple handlers self._subscribed_topics: set[str] = set() diff --git a/commhandler/src/sgr_commhandler/driver/modbus/__init__.py b/commhandler/src/sgr_commhandler/driver/modbus/__init__.py index 086612d..0295a96 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/__init__.py @@ -1,3 +1,7 @@ +""" +Provides the Modbus RTU/TCP interface driver. +""" + __all__ = ["ModbusDataPoint", "ModbusFunctionalProfile", "SGrModbusInterface"] from .modbus_interface_async import ( diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py index c22f277..59b4d7e 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py @@ -1,7 +1,11 @@ +""" +Provides the Modbus client implementation. +""" + import logging import threading from abc import ABC -from typing import Any, Optional +from typing import Any from pymodbus import FramerType from pymodbus.client import AsyncModbusSerialClient, AsyncModbusTcpClient @@ -70,7 +74,7 @@ async def write_holding_registers( address+self._addr_offset, builder.to_registers(), slave=slave_id, no_response_expected=True ) if response and response.isError(): - logger.warning(f'Modbus write exception {response.status}') + logger.warning(f'Modbus write exception {response.function_code}') async def write_coils( self, slave_id: int, address: int, data_type: ModbusDataType, value: Any @@ -98,7 +102,7 @@ async def write_coils( address+self._addr_offset, builder.to_coils(), slave=slave_id, no_response_expected=True ) if response and response.isError(): - logger.warning(f'Modbus write exception {response.status}') + logger.warning(f'Modbus write exception {response.function_code}') async def read_input_registers( self, slave_id: int, address: int, size: int, data_type: ModbusDataType @@ -116,7 +120,7 @@ async def read_input_registers( The number of registers to read data_type : ModbusDataType The modbus type to decode - + Returns ------- Any @@ -134,7 +138,7 @@ async def read_input_registers( ) return decoder.decode(data_type, 0) elif response and response.isError(): - logger.warning(f'Modbus read exception {response.status}') + logger.warning(f'Modbus read exception {response.function_code}') async def read_holding_registers( self, slave_id: int, address: int, size: int, data_type: ModbusDataType @@ -152,7 +156,7 @@ async def read_holding_registers( The number of registers to read data_type : ModbusDataType The modbus type to decode - + Returns ------- Any @@ -170,7 +174,7 @@ async def read_holding_registers( ) return decoder.decode(data_type, 0) elif response and response.isError(): - logger.warning(f'Modbus read exception {response.status}') + logger.warning(f'Modbus read exception {response.function_code}') async def read_coils( self, slave_id: int, address: int, size: int, data_type: ModbusDataType @@ -188,7 +192,7 @@ async def read_coils( The number of coils to read data_type : ModbusDataType The modbus type to decode - + Returns ------- Any @@ -206,7 +210,7 @@ async def read_coils( ) return decoder.decode(data_type, 0) elif response and response.isError(): - logger.warning(f'Modbus read exception {response.status}') + logger.warning(f'Modbus read exception {response.function_code}') async def read_discrete_inputs( self, slave_id: int, address: int, size: int, data_type: ModbusDataType @@ -224,7 +228,7 @@ async def read_discrete_inputs( The number of inputs to read data_type : ModbusDataType The modbus type to decode - + Returns ------- Any @@ -242,7 +246,7 @@ async def read_discrete_inputs( ) return decoder.decode(data_type, 0) elif response and response.isError(): - logger.warning(f'Modbus read exception {response.status}') + logger.warning(f'Modbus read exception {response.function_code}') class SGrModbusTCPClient(SGrModbusClient): diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py index 2d79531..4b945e4 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -1,4 +1,7 @@ -from io import UnsupportedOperation +""" +Provides the Modbus interface implementation. +""" + import logging import random import string @@ -52,7 +55,12 @@ def get_rtu_slave_id(modbus_rtu: ModbusRtu) -> int: """ - returns the selected slave address + Returns the selected slave address. + + Returns + ------- + int + the slave ID """ if modbus_rtu.slave_addr is None: raise Exception('No RTU slave address configured') @@ -61,7 +69,12 @@ def get_rtu_slave_id(modbus_rtu: ModbusRtu) -> int: def get_tcp_slave_id(modbus_tcp: ModbusTcp) -> int: """ - returns the selected slave address + Returns the selected slave address. + + Returns + ------- + int + the slave ID """ if modbus_tcp.slave_id is None: raise Exception('No slave id configured') @@ -70,7 +83,12 @@ def get_tcp_slave_id(modbus_tcp: ModbusTcp) -> int: def get_endian(modbus: ModbusInterfaceDescription) -> BitOrder: """ - returns the byte order. + Returns the byte order (endianness). + + Returns + ------- + BitOrder + the byte order """ if modbus.bit_order: return modbus.bit_order @@ -270,7 +288,7 @@ def get_specification(self) -> ModbusDataPointSpec: async def set_val(self, value: Any): # special case enum - convert to ordinal - if self._dp_spec.data_point and self._dp_spec.data_point.data_type.enum: + if self._dp_spec.data_point and self._dp_spec.data_point.data_type and self._dp_spec.data_point.data_type.enum: enum_spec = self._dp_spec.data_point.data_type.enum if isinstance(value, str): rec = next(filter(lambda e: e.literal is not None and e.ordinal is not None and e.literal == value, enum_spec.enum_entry), None) @@ -289,7 +307,7 @@ async def set_val(self, value: Any): and self._dp_spec.data_point.unit_conversion_multiplicator ) else 1.0 if unit_conv_factor != 1.0: - value = float(value) / unit_conv_factor + value = float(value) / unit_conv_factor # scaling scaling_factor = self._dp_spec.modbus_attributes.scaling_factor if ( @@ -297,7 +315,7 @@ async def set_val(self, value: Any): and self._dp_spec.modbus_attributes.scaling_factor ) else None if scaling_factor is not None: - value = float(value) / (scaling_factor.multiplicator * pow(10, scaling_factor.powerof10)) + value = float(value) / (scaling_factor.multiplicator or 1 * pow(10, scaling_factor.powerof10 or 0)) # round to int if modbus type is int and DP type is not if is_float_type( @@ -325,7 +343,7 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: and self._dp_spec.modbus_attributes.scaling_factor ) else None if scaling_factor is not None: - ret_value = float(ret_value) * scaling_factor.multiplicator * pow(10, scaling_factor.powerof10) + ret_value = float(ret_value) * (scaling_factor.multiplicator or 1 * pow(10, scaling_factor.powerof10 or 0)) # convert to DP units unit_conv_factor = self._dp_spec.data_point.unit_conversion_multiplicator if ( @@ -346,7 +364,7 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: ret_value = value_util.round_to_int(float(ret_value)) # special case enum - convert to literal - if self._dp_spec.data_point and self._dp_spec.data_point.data_type.enum: + if self._dp_spec.data_point and self._dp_spec.data_point.data_type and self._dp_spec.data_point.data_type.enum: enum_spec = self._dp_spec.data_point.data_type.enum ret_value = int(ret_value) rec = next(filter(lambda e: e.ordinal is not None and e.literal is not None and e.ordinal == ret_value, enum_spec.enum_entry), None) @@ -367,7 +385,7 @@ def direction(self) -> DataDirectionProduct: ): raise Exception('missing data direction') return self._dp_spec.data_point.data_direction - + def unit(self) -> Units: if ( self._dp_spec.data_point is None @@ -422,7 +440,7 @@ def __init__( sharedRTU: bool = False, ): super().__init__(frame) - self._client_wrapper: ModbusClientWrapper = None # type: ignore + self._client_wrapper: ModbusClientWrapper = None # type: ignore if ( self.device_frame.interface_list is None or self.device_frame.interface_list.modbus_interface is None diff --git a/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py b/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py index d060d2a..362502e 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py @@ -1,3 +1,8 @@ +""" +Provides a payload encoder and decoder to convert between external and Modbus data types. +This implementation is only supported up to pymodbus 3.8. +""" + import logging from typing import Any diff --git a/commhandler/src/sgr_commhandler/driver/modbus/shared_client.py b/commhandler/src/sgr_commhandler/driver/modbus/shared_client.py index a7cbca8..92d01dd 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/shared_client.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/shared_client.py @@ -1,3 +1,7 @@ +""" +Provides shared Modbus RTU clients. +""" + import logging from threading import Lock @@ -26,6 +30,14 @@ def __init__( self.connected_devices = set() async def connect_async(self, device_id: str): + """ + Connects the device to the underlying transport. + + Parameters + ---------- + device_id : str + the unique device identifier + """ if self.shared: if device_id not in self.registered_devices: return @@ -39,6 +51,14 @@ async def connect_async(self, device_id: str): await self.client.connect() async def disconnect_async(self, device_id: str): + """ + Disconnects the device from the underlying transport. + + Parameters + ---------- + device_id : str + the unique device identifier + """ if self.shared: if device_id not in self.registered_devices: return @@ -56,6 +76,19 @@ async def disconnect_async(self, device_id: str): await self.client.disconnect() def is_connected(self, device_id: str) -> bool: + """ + Tells if the device is connected to the transport. + + Parameters + ---------- + device_id : str + the unique device identifier + + Returns + ------- + bool + the connection state + """ if self.shared: return (device_id in self.registered_devices) and ( device_id is self.connected_devices @@ -64,14 +97,36 @@ def is_connected(self, device_id: str) -> bool: return self.client.is_connected() -# singleton objects _global_shared_lock = Lock() +"""global singleton for locking.""" + _global_shared_rtu_clients: dict[str, ModbusClientWrapper] = dict() +"""global singleton containing registered clients.""" def register_shared_client( serial_port: str, parity: str, baudrate: int, device_id: str ) -> ModbusClientWrapper: + """ + Registers a device at a shared transport. + Creates the transport if it does not exist. + + Parameters + ---------- + serial_port : str + the serial port device name + parity : str + the serial port parity + baudrate : int + the serial port baudrate + device_id : str + the unique device identifier + + Returns + ------- + ModbusClientWrapper + a wrapper for the transport + """ global _global_shared_lock global _global_shared_rtu_clients with _global_shared_lock: @@ -81,7 +136,7 @@ def register_shared_client( serial_port, parity, baudrate, - BitOrder.BIG_ENDIAN, # TODO bit order was missing, i just added want. + BitOrder.BIG_ENDIAN, ) client_wrapper = ModbusClientWrapper( serial_port, modbus_client, shared=True @@ -95,7 +150,18 @@ def register_shared_client( return client_wrapper -def unregister_shared_client(serial_port: str, device_id: str) -> None: +def unregister_shared_client(serial_port: str, device_id: str): + """ + Unregisters a device from a shared transport. + Removes the transport if no other device uses it. + + Parameters + ---------- + serial_port : str + the serial port device name + device_id : str + the unique device identifier + """ global _global_shared_lock global _global_shared_rtu_clients with _global_shared_lock: diff --git a/commhandler/src/sgr_commhandler/driver/rest/__init__.py b/commhandler/src/sgr_commhandler/driver/rest/__init__.py index afb8daa..6899f8a 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/__init__.py +++ b/commhandler/src/sgr_commhandler/driver/rest/__init__.py @@ -1,3 +1,7 @@ +""" +Provides the HTTP / REST interface driver. +""" + __all__ = ["RestDataPoint", "RestFunctionalProfile", "SGrRestInterface"] from .restapi_interface_async import ( diff --git a/commhandler/src/sgr_commhandler/driver/rest/authentication.py b/commhandler/src/sgr_commhandler/driver/rest/authentication.py index 78a6c39..d96f54f 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/authentication.py +++ b/commhandler/src/sgr_commhandler/driver/rest/authentication.py @@ -1,3 +1,7 @@ +""" +Provides HTTP/REST authentication methods. +""" + import base64 import json import logging @@ -39,7 +43,7 @@ async def authenticate_not( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -61,7 +65,7 @@ async def authenticate_with_bearer_token( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -91,68 +95,68 @@ async def authenticate_with_bearer_token( request = build_rest_request(service_call, base_url, {}) async with session.request( - request.method, - request.url, - headers=request.headers, - params=request.query_parameters, - data=request.body - ) as req: - if 200 <= req.status < 300: - logger.info(f"Bearer authentication successful: Status {req.status}") - try: - res_body = await req.text() - res_headers = CIMultiDict() - for name, value in req.headers.items(): - res_headers.add(name, value) - - response = RestResponse(headers=res_headers, body=res_body) - - # extract token from response body - if response.body is not None: - token: Any - if ( - service_call.response_query - and service_call.response_query.query_type == ResponseQueryType.JMESPATH_EXPRESSION - ): - # JMESPath expression - query_expression = service_call.response_query.query if service_call.response_query.query else '' - token = jmespath.search(query_expression, json.loads(response.body)) - elif ( - service_call.response_query - and service_call.response_query.query_type == ResponseQueryType.JMESPATH_MAPPING - ): - # JMESPath mappings - mappings = service_call.response_query.jmes_path_mappings.mapping if service_call.response_query.jmes_path_mappings else [] - token = jmespath_mapping.map_json_response(response.body, mappings) - elif ( - service_call.response_query - and service_call.response_query.query_type == ResponseQueryType.REGULAR_EXPRESSION - ): - # regex - query_expression = service_call.response_query.query if service_call.response_query.query else '' - query_match = re.match(query_expression, response.body) - token = query_match.group() if query_match is not None else None - else: - # plaintext - token = response.body - - if token is not None: - # update authorization header in active session - session.headers.update( - {"Authorization": f"Bearer {token}"} - ) - logger.info("Bearer token retrieved successfully") - return True - - logger.warning("Bearer token not found in the response") - return False - except json.JSONDecodeError: - logger.error("Failed to decode JSON response") - except JMESPathError: - logger.error("Failed to search JSON data using JMESPath") - else: - logger.warning(f"Bearer authentication failed: Status {req.status}") - logger.debug(f"Response: {await req.text()}") + request.method, + request.url, + headers=request.headers, + params=request.query_parameters, + data=request.body + ) as req: + if 200 <= req.status < 300: + logger.info(f"Bearer authentication successful: Status {req.status}") + try: + res_body = await req.text() + res_headers = CIMultiDict() + for name, value in req.headers.items(): + res_headers.add(name, value) + + response = RestResponse(headers=res_headers, body=res_body) + + # extract token from response body + if response.body is not None: + token: Any + if ( + service_call.response_query + and service_call.response_query.query_type == ResponseQueryType.JMESPATH_EXPRESSION + ): + # JMESPath expression + query_expression = service_call.response_query.query if service_call.response_query.query else '' + token = jmespath.search(query_expression, json.loads(response.body)) + elif ( + service_call.response_query + and service_call.response_query.query_type == ResponseQueryType.JMESPATH_MAPPING + ): + # JMESPath mappings + mappings = service_call.response_query.jmes_path_mappings.mapping if service_call.response_query.jmes_path_mappings else [] + token = jmespath_mapping.map_json_response(response.body, mappings) + elif ( + service_call.response_query + and service_call.response_query.query_type == ResponseQueryType.REGULAR_EXPRESSION + ): + # regex + query_expression = service_call.response_query.query if service_call.response_query.query else '' + query_match = re.match(query_expression, response.body) + token = query_match.group() if query_match is not None else None + else: + # plaintext + token = response.body + + if token is not None: + # update authorization header in active session + session.headers.update( + {"Authorization": f"Bearer {token}"} + ) + logger.info("Bearer token retrieved successfully") + return True + + logger.warning("Bearer token not found in the response") + return False + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + except JMESPathError: + logger.error("Failed to search JSON data using JMESPath") + else: + logger.warning(f"Bearer authentication failed: Status {req.status}") + logger.debug(f"Response: {await req.text()}") except aiohttp.ClientError as e: logger.error(f"Network error occurred: {e}") except Exception as e: @@ -173,7 +177,7 @@ async def authenticate_with_basic_auth( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -219,7 +223,7 @@ async def setup_authentication( the device interface session : ClientSession the REST client session - + Returns ------- bool diff --git a/commhandler/src/sgr_commhandler/driver/rest/request.py b/commhandler/src/sgr_commhandler/driver/rest/request.py index 6002cf9..07c7993 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/request.py +++ b/commhandler/src/sgr_commhandler/driver/rest/request.py @@ -1,3 +1,7 @@ +""" +Provides HTTP request and response implementations. +""" + from typing import Optional from urllib.parse import urlencode from multidict import CIMultiDict @@ -69,7 +73,7 @@ def build_rest_request(call_spec: RestApiServiceCall, base_url: str, substitutio headers = call_spec.request_header if call_spec.request_header else HeaderList() query = call_spec.request_query if call_spec.request_query else ParameterList() form = call_spec.request_form if call_spec.request_form else ParameterList() - body=str(call_spec.request_body) if call_spec.request_body else None + body = str(call_spec.request_body) if call_spec.request_body else None # All headers into dictionary, with substitution request_headers: CIMultiDict[str] = CIMultiDict() diff --git a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py index b362706..de00019 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py @@ -1,3 +1,7 @@ +""" +Provides the HTTP/REST interface implementation. +""" + import json import logging import re @@ -194,7 +198,7 @@ def __init__( if not self._read_call and not self._write_call: raise Exception('No REST service call configured') - + self._dynamic_parameters = build_dynamic_parameters( self._dp_spec.data_point.parameter_list if self._dp_spec.data_point @@ -320,7 +324,7 @@ def direction(self) -> DataDirectionProduct: ): raise Exception('missing data direction') return self._dp_spec.data_point.data_direction - + def unit(self) -> Units: if ( self._dp_spec.data_point is None diff --git a/commhandler/src/sgr_commhandler/utils/__init__.py b/commhandler/src/sgr_commhandler/utils/__init__.py index 8b13789..73977d1 100644 --- a/commhandler/src/sgr_commhandler/utils/__init__.py +++ b/commhandler/src/sgr_commhandler/utils/__init__.py @@ -1 +1,3 @@ - +""" +Contains utility functions. +""" diff --git a/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py b/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py index 86674ca..85ba0fc 100644 --- a/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py +++ b/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py @@ -1,3 +1,7 @@ +""" +Provides an SGr-specific implementation that transforms JSON data using JMESpath mappings. +""" + import collections import json import re @@ -115,7 +119,7 @@ def _enhance_with_namings(flat_representation: dict[RecordKey, dict[str, Any]], def _flatten_named_records(cur_key: int, values: dict[str, Any], names: dict[str, str]) -> tuple[int, dict[int, dict[str, Any]]]: flat_records: dict[int, dict[str, Any]] = collections.OrderedDict() unnamed_values: dict[str, Any] = collections.OrderedDict() - + for (k, v) in values.items(): if k not in names: unnamed_values[k] = v @@ -151,9 +155,7 @@ def _parse_json_tree(node: Any, parent_data: Optional[dict[RecordKey, dict[str, def _get_keywords_for_iteration(iteration: int, keyword_map: dict[str, str]) -> list[tuple[str, str]]: - iteration_depth = iteration - count_is_iteration = lambda item : item[1].count('[*]') == iteration_depth - return list(filter(count_is_iteration, keyword_map.items())) + return list(filter(lambda item: item[1].count('[*]') == iteration, keyword_map.items())) def _determine_required_iterations(keyword_map: dict[str, str]) -> int: @@ -175,7 +177,13 @@ def _get_number_of_elements(node: Any, parent_idx: int, keyword: tuple[str, str] return int(result) -def _process_child_elements(node: Any, iteration: int, record_map: dict[RecordKey, dict[str, Any]], keywords: list[tuple[str, str]], parent_idx: int, parent_rec: Optional[tuple[RecordKey, dict[str, Any]]]): +def _process_child_elements( + node: Any, + iteration: int, + record_map: dict[RecordKey, dict[str, Any]], + keywords: list[tuple[str, str]], + parent_idx: int, parent_rec: Optional[tuple[RecordKey, dict[str, Any]]] +): if len(keywords) > 0: kw = keywords[0] n_elem = _get_number_of_elements(node, parent_idx, kw, iteration) @@ -261,7 +269,7 @@ def _add_second_level_nodes(first_level_node: dict, flat_records_belonging_to_gr elif isinstance(val, int): object_node[child_name_mapping[mapping_entry[0]]] = int(val) elif val is not None: - object_node[child_name_mapping[mapping_entry[0]]] = str(val) + object_node[child_name_mapping[mapping_entry[0]]] = str(val) array_node.append(object_node) diff --git a/commhandler/src/sgr_commhandler/utils/template.py b/commhandler/src/sgr_commhandler/utils/template.py index 4862f33..2834b77 100644 --- a/commhandler/src/sgr_commhandler/utils/template.py +++ b/commhandler/src/sgr_commhandler/utils/template.py @@ -1,4 +1,7 @@ -# template utils +""" +Provides a function to substitute placeholders in a template. +""" + def substitute(template: str, substitutions: dict[str, str]) -> str: """ diff --git a/commhandler/src/sgr_commhandler/utils/value_util.py b/commhandler/src/sgr_commhandler/utils/value_util.py index 4966aff..e48cce5 100644 --- a/commhandler/src/sgr_commhandler/utils/value_util.py +++ b/commhandler/src/sgr_commhandler/utils/value_util.py @@ -1,3 +1,7 @@ +""" +Provides data value manipulation functions. +""" + import logging from enum import Enum from math import ceil, floor diff --git a/commhandler/src/sgr_commhandler/validators/__init__.py b/commhandler/src/sgr_commhandler/validators/__init__.py index ef7f70b..29e2219 100644 --- a/commhandler/src/sgr_commhandler/validators/__init__.py +++ b/commhandler/src/sgr_commhandler/validators/__init__.py @@ -1,3 +1,7 @@ +""" +Contains validators for the different data types returned by data points. +""" + __all__ = ["build_validator"] from sgr_commhandler.validators.resolver import build_validator diff --git a/commhandler/src/sgr_commhandler/validators/resolver.py b/commhandler/src/sgr_commhandler/validators/resolver.py index 2542b4a..f3a3345 100644 --- a/commhandler/src/sgr_commhandler/validators/resolver.py +++ b/commhandler/src/sgr_commhandler/validators/resolver.py @@ -1,3 +1,7 @@ +""" +Provides function to create validators. +""" + from typing import Optional from sgr_specification.v0.generic import DataTypeProduct diff --git a/commhandler/src/sgr_commhandler/validators/validator.py b/commhandler/src/sgr_commhandler/validators/validator.py index f26fe09..35f23b5 100644 --- a/commhandler/src/sgr_commhandler/validators/validator.py +++ b/commhandler/src/sgr_commhandler/validators/validator.py @@ -1,3 +1,7 @@ +""" +Provides data type validators. +""" + from datetime import datetime from typing import Any, Optional @@ -17,7 +21,7 @@ class UnsupportedValidator(DataPointValidator): def __init__(self): super().__init__(DataTypes.UNDEFINED) - + def validate(self, value: Any) -> bool: return False @@ -29,14 +33,17 @@ class EnumValidator(DataPointValidator): def __init__(self, type: EnumMapProduct): super().__init__(DataTypes.ENUM) + self._valid_literals: set[str] = set() + self._valid_ordinals: set[int] = set() if type and type.enum_entry: self._options = list(map(lambda e: (e.literal, e.ordinal), type.enum_entry)) - self._valid_literals: set[str] = set(map(lambda e: e[0], filter(lambda e: e[0] is not None, self._options))) - self._valid_ordinals: set[int] = set(map(lambda e: e[1], filter(lambda e: e[1] is not None, self._options))) + for o in self._options: + if o[0]: + self._valid_literals.add(o[0]) + if o[1]: + self._valid_ordinals.add(o[1]) else: - self._valid_literals: set[str] = set() - self._valid_ordinals: set[int] = set() - self._options: list[tuple[str, int]] = [] + self._options: list[tuple[Optional[str], Optional[int]]] = [] def validate(self, value: Any) -> bool: if value is None: diff --git a/commhandler/tests/test_devices/test_device_builder.py b/commhandler/tests/test_devices/test_device_builder.py index 1968207..b8f655e 100644 --- a/commhandler/tests/test_devices/test_device_builder.py +++ b/commhandler/tests/test_devices/test_device_builder.py @@ -28,7 +28,10 @@ async def test_device_builder_modbus_tcp_dict(): assert device_info.name == "ABB B23 TCP" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.modbus_interface is not None + assert device_frame.interface_list.modbus_interface.modbus_interface_description is not None + assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp is not None assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.slave_id == '1' assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.address == '127.0.0.1' assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.port == '502' @@ -57,7 +60,10 @@ async def test_device_builder_modbus_tcp_ini(): assert device_info.name == "ABB B23 TCP" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.modbus_interface is not None + assert device_frame.interface_list.modbus_interface.modbus_interface_description is not None + assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp is not None assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.slave_id == '1' assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.address == '127.0.0.1' assert device_frame.interface_list.modbus_interface.modbus_interface_description.modbus_tcp.port == '502' @@ -81,7 +87,9 @@ async def test_device_builder_rest_dict(): assert device_info.name == "Shelly 1PM Local" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.rest_api_interface is not None + assert device_frame.interface_list.rest_api_interface.rest_api_interface_description is not None assert device_frame.interface_list.rest_api_interface.rest_api_interface_description.rest_api_uri == 'http://127.0.0.1' @@ -108,7 +116,9 @@ async def test_device_builder_rest_ini(): assert device_info.name == "Shelly 1PM Local" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.rest_api_interface is not None + assert device_frame.interface_list.rest_api_interface.rest_api_interface_description is not None assert device_frame.interface_list.rest_api_interface.rest_api_interface_description.rest_api_uri == 'http://127.0.0.1' @@ -133,8 +143,13 @@ async def test_device_builder_messaging_dict(): assert device_info.name == "HiveMQ Test Cloud" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.messaging_interface is not None - assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element[0].host == '152f30e8c480481886072e4f8250d91a.s1.eu.hivemq.cloud' + assert device_frame.interface_list.messaging_interface.messaging_interface_description is not None + assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list is not None + assert len(device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element) > 0 + assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element[0].host \ + == '152f30e8c480481886072e4f8250d91a.s1.eu.hivemq.cloud' @pytest.mark.asyncio @@ -158,8 +173,13 @@ async def test_device_builder_messaging_ini(): assert device_info.name == "HiveMQ Test Cloud" device_frame = test_device.device_frame + assert device_frame.interface_list is not None assert device_frame.interface_list.messaging_interface is not None - assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element[0].host == '152f30e8c480481886072e4f8250d91a.s1.eu.hivemq.cloud' + assert device_frame.interface_list.messaging_interface.messaging_interface_description is not None + assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list is not None + assert len(device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element) > 0 + assert device_frame.interface_list.messaging_interface.messaging_interface_description.message_broker_list.message_broker_list_element[0].host \ + == '152f30e8c480481886072e4f8250d91a.s1.eu.hivemq.cloud' @pytest.mark.asyncio diff --git a/commhandler/tests/test_devices/test_device_introspection.py b/commhandler/tests/test_devices/test_device_introspection.py index b972d1d..00c6867 100644 --- a/commhandler/tests/test_devices/test_device_introspection.py +++ b/commhandler/tests/test_devices/test_device_introspection.py @@ -15,6 +15,7 @@ os.path.dirname(os.path.realpath(__file__)), "eids" ) + def _load_device(): eid_path = os.path.join( EID_BASE_PATH, "SGr_00_0016_dddd_ABB_B23_ModbusTCP_V0.4.xml" @@ -22,6 +23,7 @@ def _load_device(): eid_properties = dict(slave_id="1", tcp_address="127.0.0.1", tcp_port="502") return DeviceBuilder().eid_path(eid_path).properties(eid_properties).build() + @pytest.mark.asyncio async def test_device_introspection_device(): device = _load_device() @@ -31,6 +33,7 @@ async def test_device_introspection_device(): device_frame = device.get_specification() assert isinstance(device_frame, DeviceFrame) + @pytest.mark.asyncio async def test_device_introspection_fp(): device = _load_device() @@ -43,6 +46,7 @@ async def test_device_introspection_fp(): fp_frame = fp.get_specification() assert isinstance(fp_frame, ModbusFunctionalProfileSpec) + @pytest.mark.asyncio async def test_device_introspection_dp(): device = _load_device() diff --git a/commhandler/tests/test_utils/test_jmespath.py b/commhandler/tests/test_utils/test_jmespath.py index 2c531ba..72530ef 100644 --- a/commhandler/tests/test_utils/test_jmespath.py +++ b/commhandler/tests/test_utils/test_jmespath.py @@ -11,6 +11,7 @@ Test JMESPath mapping. """ + def test_jmespath_mapping_1(): jmes_mappings = [ JmespathMappingRecord(from_value="[*].start_timestamp", to="[*].start_timestamp"), @@ -139,4 +140,3 @@ def test_jmespath_mapping_2(): json_mapped = jmespath_mapping.map_json_response(json_input, jmes_mappings) assert json_mapped == json_expected - From c63ca7464291af552f854a8842dda243d7c41019 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 10:55:42 +0200 Subject: [PATCH 02/10] downgraded sphinxdoc for python 3.9 compatibility. --- commhandler/requirements-doc.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commhandler/requirements-doc.txt b/commhandler/requirements-doc.txt index d93a6c9..b925000 100644 --- a/commhandler/requirements-doc.txt +++ b/commhandler/requirements-doc.txt @@ -1,3 +1,3 @@ -sphinx>=8.2.0,<9.0.0 +sphinx>=7.4.0,<8.0.0 sphinx-autoapi>=3.6.0,<4.0.0 sphinx-rtd-theme>=3.0.0,<4.0.0 From f29a566fa2554bb9bf516bbec488030e68568a33 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 11:02:31 +0200 Subject: [PATCH 03/10] removed match/case for compatibility. --- .../driver/modbus/modbus_interface_async.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py index 4b945e4..57a3037 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -144,17 +144,14 @@ def get_rtu_parity(modbus_rtu: ModbusRtu) -> str: returns the parity. """ parity = modbus_rtu.parity_selected - if not parity: + if parity is None or parity == Parity.NONE.name: return 'N' - match parity: - case Parity.NONE.name: - return 'N' - case Parity.EVEN.name: - return 'E' - case Parity.ODD.name: - return 'O' - case _: - raise NotImplementedError + elif parity == Parity.EVEN.name: + return 'E' + elif parity == Parity.ODD.name: + return 'O' + else: + raise NotImplementedError def build_modbus_data_point( From be698b3c93d24a3644bb2f8d914030fe83ba9fba Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 11:24:23 +0200 Subject: [PATCH 04/10] code compatibility. --- commhandler/src/sgr_commhandler/device_builder.py | 6 +++--- .../sgr_commhandler/driver/modbus/modbus_client_async.py | 4 ++-- .../driver/modbus/modbus_interface_async.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/commhandler/src/sgr_commhandler/device_builder.py b/commhandler/src/sgr_commhandler/device_builder.py index a69b57c..389d5f5 100644 --- a/commhandler/src/sgr_commhandler/device_builder.py +++ b/commhandler/src/sgr_commhandler/device_builder.py @@ -10,7 +10,7 @@ import sgr_schema from collections.abc import Callable from enum import Enum -from typing import cast +from typing import Optional, Union, cast from sgr_specification.v0.product import DeviceFrame from xsdata.formats.dataclass.context import XmlContext @@ -103,8 +103,8 @@ def __init__(self): """ Constructs a new device builder. """ - self._eid_source: str | None = None - self._properties_source: str | dict | None = None + self._eid_source: Optional[str] = None + self._properties_source: Union[str, dict, None] = None self._eid_type: SGrXmlSource = SGrXmlSource.UNKNOWN self._properties_type: SGrPropertiesSource = SGrPropertiesSource.UNKNOWN diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py index 59b4d7e..cb0dd6f 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py @@ -50,7 +50,7 @@ def is_connected(self) -> bool: ... async def write_holding_registers( self, slave_id: int, address: int, data_type: ModbusDataType, value: Any - ) -> None: + ): """ Encodes value to be written to holding register address. @@ -78,7 +78,7 @@ async def write_holding_registers( async def write_coils( self, slave_id: int, address: int, data_type: ModbusDataType, value: Any - ) -> None: + ): """ Encodes value to be written to coil address. diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py index 57a3037..8684cb3 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -5,7 +5,7 @@ import logging import random import string -from typing import Any, Optional +from typing import Any, Optional, Union from math import pow from sgr_specification.v0.generic import DataDirectionProduct, Parity @@ -170,7 +170,7 @@ def build_modbus_data_point( return DataPoint(protocol, validator) -def is_integer_type(data_type: DataTypeProduct | ModbusDataType | None) -> bool: +def is_integer_type(data_type: Union[DataTypeProduct, ModbusDataType, None]) -> bool: """ Checks if a data type is an integer. @@ -196,7 +196,7 @@ def is_integer_type(data_type: DataTypeProduct | ModbusDataType | None) -> bool: ) -def is_float_type(data_type: DataTypeProduct | ModbusDataType | None) -> bool: +def is_float_type(data_type: Union[DataTypeProduct, ModbusDataType, None]) -> bool: """ Checks if a data type is a floating point value. @@ -581,7 +581,7 @@ async def write_data( address: int, data_type: ModbusDataType, value: Any, - ) -> None: + ): """ Writes data to the given Modbus address(es). """ From 963c5f26e52896d782160da523764a229d0171c9 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 11:36:41 +0200 Subject: [PATCH 05/10] code compatibility. --- commhandler/src/sgr_commhandler/api/device_api.py | 2 +- .../src/sgr_commhandler/api/functional_profile_api.py | 2 +- commhandler/src/sgr_commhandler/device_builder.py | 4 ++-- .../src/sgr_commhandler/driver/rest/authentication.py | 8 ++------ .../src/sgr_commhandler/utils/jmespath_mapping.py | 10 +++++----- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/commhandler/src/sgr_commhandler/api/device_api.py b/commhandler/src/sgr_commhandler/api/device_api.py index 09632c0..af01a8c 100644 --- a/commhandler/src/sgr_commhandler/api/device_api.py +++ b/commhandler/src/sgr_commhandler/api/device_api.py @@ -181,7 +181,7 @@ async def get_values_async(self, parameters: Optional[dict[str, str]] = None) -> data.update( { (fp.name(), key): value - for key, value in ( + for (key, value) in ( await fp.get_values_async(parameters) ).items() } diff --git a/commhandler/src/sgr_commhandler/api/functional_profile_api.py b/commhandler/src/sgr_commhandler/api/functional_profile_api.py index b292844..2cc0165 100644 --- a/commhandler/src/sgr_commhandler/api/functional_profile_api.py +++ b/commhandler/src/sgr_commhandler/api/functional_profile_api.py @@ -63,7 +63,7 @@ async def get_values_async(self, parameters: Optional[dict[str, str]] = None) -> all data point values by name """ data = {} - for key, dp in self.get_data_points().items(): + for (key, dp) in self.get_data_points().items(): try: value = await dp.get_value_async(parameters) data[key] = value diff --git a/commhandler/src/sgr_commhandler/device_builder.py b/commhandler/src/sgr_commhandler/device_builder.py index 389d5f5..dd845ba 100644 --- a/commhandler/src/sgr_commhandler/device_builder.py +++ b/commhandler/src/sgr_commhandler/device_builder.py @@ -244,7 +244,7 @@ def _load_properties(self) -> dict: config.optionxform = lambda optionstr: optionstr config.read(prop_path) properties = {} - for section_name, section in config.items(): + for (section_name, section) in config.items(): for param_name in section: param_value = config.get(section_name, param_name) properties[param_name] = param_value @@ -293,7 +293,7 @@ def replace_variables(content: str, parameters: dict) -> str: str the updated EID XML content """ - for name, value in parameters.items(): + for (name, value) in parameters.items(): pattern = re.compile(r'{{' + str(name) + r'}}') content = pattern.sub(str(value), content) logger.debug(f'replaced parameter: {str(name)} = {str(value)}') diff --git a/commhandler/src/sgr_commhandler/driver/rest/authentication.py b/commhandler/src/sgr_commhandler/driver/rest/authentication.py index d96f54f..ff3ce4b 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/authentication.py +++ b/commhandler/src/sgr_commhandler/driver/rest/authentication.py @@ -6,7 +6,7 @@ import json import logging import re -from typing import Any, Awaitable, Callable, TypeAlias +from typing import Any, Awaitable, Callable import aiohttp import jmespath @@ -26,10 +26,6 @@ logger = logging.getLogger(__name__) -Authenticator: TypeAlias = Callable[ - [RestApiInterface, ClientSession], Awaitable[bool] -] - async def authenticate_not( interface: RestApiInterface, session: ClientSession @@ -203,7 +199,7 @@ async def authenticate_with_basic_auth( supported_authentication_methods: dict[ - RestApiAuthenticationMethod, Authenticator + RestApiAuthenticationMethod, Callable[[RestApiInterface, ClientSession], Awaitable[bool]] ] = { RestApiAuthenticationMethod.NO_SECURITY_SCHEME: authenticate_not, RestApiAuthenticationMethod.BEARER_SECURITY_SCHEME: authenticate_with_bearer_token, diff --git a/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py b/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py index 85ba0fc..644d168 100644 --- a/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py +++ b/commhandler/src/sgr_commhandler/utils/jmespath_mapping.py @@ -255,13 +255,13 @@ def _add_second_level_nodes(first_level_node: dict, flat_records_belonging_to_gr second_level_groups[ck] = record_group second_level_group_elements = _get_second_level_elements(keywords_for_iteration) - for parent_name, child_name_mapping in second_level_group_elements.items(): + for (parent_name, child_name_mapping) in second_level_group_elements.items(): array_node: list[Any] = list() first_level_node[parent_name] = array_node - for group_key, flat_records_of_group in second_level_groups.items(): + for (group_key, flat_records_of_group) in second_level_groups.items(): object_node = collections.OrderedDict() for flat_record in flat_records_of_group: - for value_names, values in flat_record.items(): + for (value_names, values) in flat_record.items(): for mapping_entry in keywords_for_iteration: val = flat_record[mapping_entry[0]] if isinstance(val, float): @@ -297,11 +297,11 @@ def _build_json_node(keyword_map: dict[str, str], flat_data_records: list[dict[s # build the json node, assume the root node is an array root_node: list[Any] = [] - for group_key, flat_records_belonging_to_group in first_level_groups.items(): + for (group_key, flat_records_belonging_to_group) in first_level_groups.items(): # add a node for each group to the array node first_level_node = collections.OrderedDict() for frg in flat_records_belonging_to_group: - for names, values in frg.items(): + for (names, values) in frg.items(): # pick all first level elements, map the elementNames get the values and add the elements to the firstLevel node for mapping_entry in keywords_for_iteration: val = frg[mapping_entry[0]] From 4562bb8fc3d3ac3e6c84694f79d81a19a63d3b4a Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 11:40:39 +0200 Subject: [PATCH 06/10] code compatibility. --- commhandler/src/sgr_commhandler/device_builder.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/commhandler/src/sgr_commhandler/device_builder.py b/commhandler/src/sgr_commhandler/device_builder.py index dd845ba..986a8bd 100644 --- a/commhandler/src/sgr_commhandler/device_builder.py +++ b/commhandler/src/sgr_commhandler/device_builder.py @@ -61,13 +61,14 @@ class SGrDeviceProtocol(Enum): UNKNOWN = 5 -SGrInterfaces = ( - SGrRestInterface - | SGrModbusInterface - | SGrMessagingInterface - | SGrContactInterface - | SGrGenericInterface -) +SGrInterfaces = Union[ + SGrRestInterface, + SGrModbusInterface, + SGrMessagingInterface, + SGrContactInterface, + SGrGenericInterface +] + device_builders: dict[ SGrDeviceProtocol, From c00ee3a3fbd23de606f8989f8041e448d7a845f4 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 11:52:34 +0200 Subject: [PATCH 07/10] compatibility of superclass attributes. --- .../driver/contact/contact_interface_async.py | 2 +- .../driver/generic/generic_interface_async.py | 2 +- .../driver/messaging/messaging_filter.py | 6 +++--- .../messaging/messaging_interface_async.py | 2 +- .../driver/modbus/modbus_client_async.py | 4 ++-- .../driver/modbus/modbus_interface_async.py | 2 +- .../driver/modbus/payload_decoder.py | 4 ++-- .../driver/rest/restapi_interface_async.py | 2 +- .../sgr_commhandler/validators/validator.py | 18 +++++++++--------- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py index c7d3373..0b59fd7 100644 --- a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py @@ -158,7 +158,7 @@ class SGrContactInterface(SGrBaseInterface): def __init__( self, frame: DeviceFrame ): - super().__init__(frame) + super(SGrContactInterface, self).__init__(frame) if ( self.device_frame.interface_list diff --git a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py index 950f79f..6a77b24 100644 --- a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py @@ -165,7 +165,7 @@ class SGrGenericInterface(SGrBaseInterface): def __init__( self, frame: DeviceFrame ): - super().__init__(frame) + super(SGrGenericInterface, self).__init__(frame) if ( self.device_frame.interface_list diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py index 8594d2d..1fee05d 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py @@ -36,7 +36,7 @@ class JMESPathMessagingFilter(MessagingFilter[JmespathFilterType]): """ def __init__(self, filter_spec: JmespathFilterType): - super().__init__(filter_spec) + super(JMESPathMessagingFilter, self).__init__(filter_spec) def is_filter_match(self, payload: Any) -> bool: ret_value = str(payload) @@ -54,7 +54,7 @@ class PlaintextMessagingFilter(MessagingFilter[PlaintextFilterType]): """ def __init__(self, filter_spec: PlaintextFilterType): - super().__init__(filter_spec) + super(PlaintextMessagingFilter, self).__init__(filter_spec) def is_filter_match(self, payload: Any) -> bool: ret_value = str(payload) @@ -70,7 +70,7 @@ class RegexMessagingFilter(MessagingFilter[RegexFilterType]): """ def __init__(self, filter_spec: RegexFilterType): - super().__init__(filter_spec) + super(RegexMessagingFilter, self).__init__(filter_spec) def is_filter_match(self, payload: Any) -> bool: ret_value = str(payload) diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py index 3053ed3..acf2c2e 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py @@ -336,7 +336,7 @@ class SGrMessagingInterface(SGrBaseInterface): def __init__( self, frame: DeviceFrame ): - super().__init__(frame) + super(SGrMessagingInterface, self).__init__(frame) if ( self.device_frame.interface_list diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py index cb0dd6f..622c5af 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_client_async.py @@ -255,7 +255,7 @@ class SGrModbusTCPClient(SGrModbusClient): """ def __init__(self, ip: str, port: int, endianness: BitOrder = BitOrder.BIG_ENDIAN, addr_offset: int = 0): - super().__init__( + super(SGrModbusTCPClient, self).__init__( endianness, addr_offset, AsyncModbusTcpClient( @@ -302,7 +302,7 @@ class SGrModbusRTUClient(SGrModbusClient): def __init__( self, serial_port: str, parity: str, baudrate: int, endianness: BitOrder = BitOrder.BIG_ENDIAN, addr_offset: int = 0 ): - super().__init__( + super(SGrModbusRTUClient, self).__init__( endianness, addr_offset, AsyncModbusSerialClient( diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py index 8684cb3..e563a2e 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -436,7 +436,7 @@ def __init__( frame: DeviceFrame, sharedRTU: bool = False, ): - super().__init__(frame) + super(SGrModbusInterface, self).__init__(frame) self._client_wrapper: ModbusClientWrapper = None # type: ignore if ( self.device_frame.interface_list is None diff --git a/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py b/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py index 362502e..c0e198b 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py @@ -19,7 +19,7 @@ class PayloadDecoder(BinaryPayloadDecoder): """ def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(PayloadDecoder, self).__init__(*args, **kwargs) def decode(self, modbus_type: ModbusDataType, byte_count: int): """ @@ -73,7 +73,7 @@ class PayloadBuilder(BinaryPayloadBuilder): """ def __init__(self, *args, **kwarg): - super().__init__(*args, **kwarg) + super(PayloadBuilder, self).__init__(*args, **kwarg) def sgr_encode( self, value: Any, modbus_type: ModbusDataType diff --git a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py index de00019..04e9db7 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py @@ -387,7 +387,7 @@ class SGrRestInterface(SGrBaseInterface): def __init__( self, frame: DeviceFrame ): - super().__init__(frame) + super(SGrRestInterface, self).__init__(frame) self._session = None self._cache = TTLCache(maxsize=100, ttl=5) diff --git a/commhandler/src/sgr_commhandler/validators/validator.py b/commhandler/src/sgr_commhandler/validators/validator.py index 35f23b5..5565248 100644 --- a/commhandler/src/sgr_commhandler/validators/validator.py +++ b/commhandler/src/sgr_commhandler/validators/validator.py @@ -20,7 +20,7 @@ class UnsupportedValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.UNDEFINED) + super(UnsupportedValidator, self).__init__(DataTypes.UNDEFINED) def validate(self, value: Any) -> bool: return False @@ -32,7 +32,7 @@ class EnumValidator(DataPointValidator): """ def __init__(self, type: EnumMapProduct): - super().__init__(DataTypes.ENUM) + super(EnumValidator, self).__init__(DataTypes.ENUM) self._valid_literals: set[str] = set() self._valid_ordinals: set[int] = set() if type and type.enum_entry: @@ -62,7 +62,7 @@ class IntValidator(DataPointValidator): """ def __init__(self, size: int, signed: bool = True): - super().__init__(DataTypes.INT) + super(IntValidator, self).__init__(DataTypes.INT) self._size = size if size in INT_SIZES else next(iter(INT_SIZES)) if signed: self._lower_bound = -(2 ** (self._size - 1)) @@ -89,7 +89,7 @@ class FloatValidator(DataPointValidator): """ def __init__(self, size: int): - super().__init__(DataTypes.FLOAT) + super(FloatValidator, self).__init__(DataTypes.FLOAT) self._size = size if size in FLOAT_SIZES else next(iter(FLOAT_SIZES)) def validate(self, value: Any) -> bool: @@ -110,7 +110,7 @@ class StringValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.STRING) + super(StringValidator, self).__init__(DataTypes.STRING) def validate(self, value: Any) -> bool: if value is None: @@ -130,7 +130,7 @@ class BooleanValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.BOOLEAN) + super(BooleanValidator, self).__init__(DataTypes.BOOLEAN) def validate(self, value: Any) -> bool: if value is None: @@ -150,7 +150,7 @@ class BitmapValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.BITMAP) + super(BitmapValidator, self).__init__(DataTypes.BITMAP) def validate(self, value: Any) -> bool: if value is None: @@ -164,7 +164,7 @@ class DateTimeValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.DATE_TIME) + super(DateTimeValidator, self).__init__(DataTypes.DATE_TIME) def validate(self, value: Any) -> bool: if value is None: @@ -179,7 +179,7 @@ class JsonValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.JSON) + super(JsonValidator, self).__init__(DataTypes.JSON) def validate(self, value: Any) -> bool: # can be anything From 40651dadb41f7f17eb78aa209f47d6426fcac0a6 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 13:15:15 +0200 Subject: [PATCH 08/10] removed Protocol as superclass, because it caused errors accessing attributes, and it works just fine without it. --- commhandler/src/sgr_commhandler/api/data_point_api.py | 6 +++--- commhandler/src/sgr_commhandler/api/device_api.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/commhandler/src/sgr_commhandler/api/data_point_api.py b/commhandler/src/sgr_commhandler/api/data_point_api.py index 216c886..70e2b60 100644 --- a/commhandler/src/sgr_commhandler/api/data_point_api.py +++ b/commhandler/src/sgr_commhandler/api/data_point_api.py @@ -3,7 +3,7 @@ """ from collections.abc import Callable -from typing import Any, Generic, NoReturn, Optional, Protocol, TypeVar +from typing import Any, Generic, NoReturn, Optional, TypeVar from sgr_specification.v0.generic import DataDirectionProduct, Units, DataPointBase @@ -14,7 +14,7 @@ TDpSpec = TypeVar('TDpSpec', covariant=True, bound=DataPointBase) -class DataPointValidator(Protocol): +class DataPointValidator(object): """ Implements a base class for data point validators. """ @@ -63,7 +63,7 @@ def options(self) -> list[Any]: return [] -class DataPointProtocol(Protocol[TDpSpec]): +class DataPointProtocol(Generic[TDpSpec]): """ Implements a base class for data point protocols. """ diff --git a/commhandler/src/sgr_commhandler/api/device_api.py b/commhandler/src/sgr_commhandler/api/device_api.py index af01a8c..599ec9a 100644 --- a/commhandler/src/sgr_commhandler/api/device_api.py +++ b/commhandler/src/sgr_commhandler/api/device_api.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Optional, Protocol +from typing import Any, Optional from sgr_specification.v0.generic import DataDirectionProduct, DeviceCategory from sgr_specification.v0.product.product import DeviceFrame @@ -46,7 +46,7 @@ class DeviceInformation: is_local: bool -class SGrBaseInterface(Protocol): +class SGrBaseInterface(object): """ Defines an abstract base class for all SGr device interfaces. @@ -69,6 +69,7 @@ class SGrBaseInterface(Protocol): def __init__( self, frame: DeviceFrame ): + super(SGrBaseInterface, self).__init__() self.device_frame = frame self.configuration_parameters = build_configuration_parameters( frame.configuration_list From 22bf1b853195d3c2df38e415dfa78ce02a311e9b Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Wed, 15 Oct 2025 14:06:06 +0200 Subject: [PATCH 09/10] migrated set-output in github workflows. --- .github/workflows/release-commhandler.yml | 4 ++-- .github/workflows/release-specification.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-commhandler.yml b/.github/workflows/release-commhandler.yml index a3828f3..197da1c 100644 --- a/.github/workflows/release-commhandler.yml +++ b/.github/workflows/release-commhandler.yml @@ -42,8 +42,8 @@ jobs: fi echo "Semver: $SEMVER" echo "Tag: $TAG" - echo "::set-output name=original_tag::$TAG" - echo "::set-output name=semver::$SEMVER" + echo "original_tag=${TAG}" >> $GITHUB_OUTPUT + echo "semver=${SEMVER}" >> $GITHUB_OUTPUT - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/release-specification.yml b/.github/workflows/release-specification.yml index d05a57b..98c3c98 100644 --- a/.github/workflows/release-specification.yml +++ b/.github/workflows/release-specification.yml @@ -41,8 +41,8 @@ jobs: fi echo "Semver: $SEMVER" echo "Tag: $TAG" - echo "::set-output name=original_tag::$TAG" - echo "::set-output name=semver::$SEMVER" + echo "original_tag=${TAG}" >> $GITHUB_OUTPUT + echo "semver=${SEMVER}" >> $GITHUB_OUTPUT - name: Set up Python uses: actions/setup-python@v6 From 442a7ddbcab8e6d1467d91bc02940386eeb3639b Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Mon, 20 Oct 2025 11:27:55 +0200 Subject: [PATCH 10/10] refactored class inheritance to avoid code duplication. --- commhandler/CHANGELOG.md | 11 +++ .../src/sgr_commhandler/api/data_point_api.py | 68 +++++++++++++++---- .../src/sgr_commhandler/api/device_api.py | 36 +++++++--- .../api/functional_profile_api.py | 27 +++++--- .../driver/contact/contact_interface_async.py | 43 +----------- .../driver/generic/generic_interface_async.py | 31 ++------- .../driver/messaging/messaging_filter.py | 4 +- .../messaging/messaging_interface_async.py | 64 ++--------------- .../driver/modbus/modbus_interface_async.py | 58 ++-------------- .../driver/rest/restapi_interface_async.py | 68 ++----------------- 10 files changed, 135 insertions(+), 275 deletions(-) diff --git a/commhandler/CHANGELOG.md b/commhandler/CHANGELOG.md index b7e21db..34d3185 100644 --- a/commhandler/CHANGELOG.md +++ b/commhandler/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.1] - 2025-10-20 + +### Fixed + +- Ensured code compatibility with Python 3.9, as defined in project + +### Changed + +- removed `Protocol` as base class of CommHandler APIs, refactored class inheritance + + ## [0.4.0] - 2025-10-01 ### Fixed diff --git a/commhandler/src/sgr_commhandler/api/data_point_api.py b/commhandler/src/sgr_commhandler/api/data_point_api.py index 70e2b60..2607c9c 100644 --- a/commhandler/src/sgr_commhandler/api/data_point_api.py +++ b/commhandler/src/sgr_commhandler/api/data_point_api.py @@ -2,16 +2,21 @@ Provides the data-point-level API. """ +from abc import ABC, abstractmethod from collections.abc import Callable from typing import Any, Generic, NoReturn, Optional, TypeVar -from sgr_specification.v0.generic import DataDirectionProduct, Units, DataPointBase +from sgr_specification.v0.generic import DataDirectionProduct, Units, DataPointBase, FunctionalProfileBase -from sgr_commhandler.api.dynamic_parameter import DynamicParameter +from sgr_commhandler.api.dynamic_parameter import DynamicParameter, build_dynamic_parameters from sgr_commhandler.api.data_types import DataTypes -"""Defines a generic data type.""" + +TFpSpec = TypeVar('TFpSpec', covariant=True, bound=FunctionalProfileBase) +"""Defines a generic functional profile data type.""" + TDpSpec = TypeVar('TDpSpec', covariant=True, bound=DataPointBase) +"""Defines a generic data point data type.""" class DataPointValidator(object): @@ -63,10 +68,37 @@ def options(self) -> list[Any]: return [] -class DataPointProtocol(Generic[TDpSpec]): +class DataPointProtocol(ABC, Generic[TFpSpec, TDpSpec]): """ Implements a base class for data point protocols. """ + + _fp_spec: TFpSpec + _dp_spec: TDpSpec + _fp_name: str + _dp_name: str + _dynamic_parameters: list[DynamicParameter] + + def __init__(self, fp_spec: TFpSpec, dp_spec: TDpSpec): + self._fp_spec = fp_spec + self._dp_spec = dp_spec + + self._fp_name = fp_spec.functional_profile.functional_profile_name if ( + fp_spec.functional_profile is not None + and fp_spec.functional_profile.functional_profile_name is not None + ) else '' + + self._dp_name = dp_spec.data_point.data_point_name if ( + dp_spec.data_point is not None + and dp_spec.data_point.data_point_name is not None + ) else '' + + self._dynamic_parameters = build_dynamic_parameters( + self._dp_spec.data_point.parameter_list + if self._dp_spec.data_point + else None + ) + def get_specification(self) -> TDpSpec: """ Gets the data point specification. @@ -76,8 +108,9 @@ def get_specification(self) -> TDpSpec: TDpSpec the interface-specific specification """ - ... + return self._dp_spec + @abstractmethod async def set_val(self, value: Any): """ Writes the data point value. @@ -89,6 +122,7 @@ async def set_val(self, value: Any): """ ... + @abstractmethod async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: """ Reads the data point value. @@ -116,7 +150,7 @@ def name(self) -> tuple[str, str]: tuple[str, str] the functional profile and data point names as tuple """ - ... + return (self._fp_name, self._dp_name) def direction(self) -> DataDirectionProduct: """ @@ -127,7 +161,12 @@ def direction(self) -> DataDirectionProduct: DataDirectionProduct the data point direction """ - ... + if ( + self._dp_spec.data_point is None + or self._dp_spec.data_point.data_direction is None + ): + raise Exception('missing data direction') + return self._dp_spec.data_point.data_direction def unit(self) -> Units: """ @@ -138,7 +177,12 @@ def unit(self) -> Units: Units the unit """ - ... + if ( + self._dp_spec.data_point is None + or self._dp_spec.data_point.unit is None + ): + return Units.NONE + return self._dp_spec.data_point.unit def dynamic_parameters(self) -> list[DynamicParameter]: """ @@ -149,7 +193,7 @@ def dynamic_parameters(self) -> list[DynamicParameter]: list[DynamicParameter] the dynamic parameters """ - return [] + return self._dynamic_parameters def can_subscribe(self) -> bool: """ @@ -180,20 +224,20 @@ def unsubscribe(self): raise Exception('unsubscribe() is not supported') -class DataPoint(Generic[TDpSpec]): +class DataPoint(Generic[TFpSpec, TDpSpec]): """ Implements a data point of a generic data type. """ def __init__( - self, protocol: DataPointProtocol[TDpSpec], validator: DataPointValidator + self, protocol: DataPointProtocol[TFpSpec, TDpSpec], validator: DataPointValidator ): """ Constructs a data point. Parameters ---------- - protocol : DataPointProtocol[TDpSpec] + protocol : DataPointProtocol[TFpSpec, TDpSpec] the underlying protocol validator : DataPointValidator the data point's value validator diff --git a/commhandler/src/sgr_commhandler/api/device_api.py b/commhandler/src/sgr_commhandler/api/device_api.py index 599ec9a..370e7f6 100644 --- a/commhandler/src/sgr_commhandler/api/device_api.py +++ b/commhandler/src/sgr_commhandler/api/device_api.py @@ -2,7 +2,7 @@ Provides the device-level API. """ -from collections.abc import Mapping +from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Optional @@ -46,7 +46,7 @@ class DeviceInformation: is_local: bool -class SGrBaseInterface(object): +class SGrBaseInterface(ABC): """ Defines an abstract base class for all SGr device interfaces. @@ -58,13 +58,13 @@ class SGrBaseInterface(object): the configuration parameters with default values device_information : DeviceInformation the device information - functional_profiles : Mapping[str, FunctionalProfile] + functional_profiles : dict[str, FunctionalProfile] the configured functional profiles """ device_frame: DeviceFrame configuration_parameters: list[ConfigurationParameter] device_information: DeviceInformation - functional_profiles: Mapping[str, FunctionalProfile] + functional_profiles: dict[str, FunctionalProfile] def __init__( self, frame: DeviceFrame @@ -97,18 +97,21 @@ def __init__( else False, ) + @abstractmethod async def connect_async(self): """ Connects the device asynchronously. """ ... + @abstractmethod async def disconnect_async(self): """ Disconnects the device asynchronously. """ ... + @abstractmethod def is_connected(self) -> bool: """ Gets the connection state. @@ -120,6 +123,17 @@ def is_connected(self) -> bool: """ ... + def get_functional_profiles(self) -> dict[str, FunctionalProfile]: + """ + Gets all functional profiles. + + Returns + ------- + dict[str, FunctionalProfile] + all functional profiles + """ + return self.functional_profiles + def get_functional_profile( self, functional_profile_name: str ) -> FunctionalProfile: @@ -163,7 +177,7 @@ def get_data_points(self) -> dict[tuple[str, str], DataPoint]: dict[tuple[str, str], DataPoint] all data points """ - data_points = {} + data_points: dict[tuple[str, str], DataPoint] = dict() for fp in self.functional_profiles.values(): data_points.update(fp.get_data_points()) return data_points @@ -177,11 +191,11 @@ async def get_values_async(self, parameters: Optional[dict[str, str]] = None) -> dict[tuple[str, str], Any] all data point values """ - data = {} - for fp in self.functional_profiles.values(): + data: dict[tuple[str, str], Any] = dict() + for (fp_name, fp) in self.functional_profiles.items(): data.update( { - (fp.name(), key): value + (fp_name, key): value for (key, value) in ( await fp.get_values_async(parameters) ).items() @@ -202,11 +216,11 @@ def describe( tuple[str, dict[str, dict[str, tuple[DataDirectionProduct, DataTypes]]]] a tuple of device name and all data point values """ - data = {} + data: dict[str, dict[str, tuple[DataDirectionProduct, DataTypes]]] = dict() for fp in self.functional_profiles.values(): - key, dps = fp.describe() + (key, dps) = fp.describe() data[key] = dps - return self.device_information.name, data + return (self.device_information.name, data) def get_specification(self) -> DeviceFrame: """ diff --git a/commhandler/src/sgr_commhandler/api/functional_profile_api.py b/commhandler/src/sgr_commhandler/api/functional_profile_api.py index 2cc0165..bb81586 100644 --- a/commhandler/src/sgr_commhandler/api/functional_profile_api.py +++ b/commhandler/src/sgr_commhandler/api/functional_profile_api.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Protocol, TypeVar +from typing import Any, Generic, Optional, TypeVar from sgr_specification.v0.generic import DataDirectionProduct from sgr_specification.v0.generic.functional_profile import FunctionalProfileBase @@ -6,15 +6,21 @@ from sgr_commhandler.api.data_point_api import DataPoint from sgr_commhandler.api.data_types import DataTypes -"""Defines a generic data type.""" + TFpSpec = TypeVar('TFpSpec', covariant=True, bound=FunctionalProfileBase) +"""Defines a generic functional profile data type.""" -class FunctionalProfile(Protocol[TFpSpec]): +class FunctionalProfile(Generic[TFpSpec]): """ Implements a functional profile. """ + _fp_spec: TFpSpec + + def __init__(self, fp_spec: TFpSpec): + self._fp_spec = fp_spec + def name(self) -> str: """ Gets the functional profile name. @@ -24,7 +30,12 @@ def name(self) -> str: str the functional profile name """ - ... + if ( + self._fp_spec.functional_profile + and self._fp_spec.functional_profile.functional_profile_name + ): + return self._fp_spec.functional_profile.functional_profile_name + return '' def get_data_points(self) -> dict[tuple[str, str], DataPoint]: """ @@ -62,14 +73,14 @@ async def get_values_async(self, parameters: Optional[dict[str, str]] = None) -> dict[str, Any] all data point values by name """ - data = {} + data: dict[str, Any] = dict() for (key, dp) in self.get_data_points().items(): try: value = await dp.get_value_async(parameters) - data[key] = value + data[key[1]] = value except Exception: # TODO log error - None should not be a valid DP value - data[key] = None + data[key[1]] = None return data def describe( @@ -95,4 +106,4 @@ def get_specification(self) -> TFpSpec: TFpSpec the functional profile specification """ - ... + return self._fp_spec diff --git a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py index 0b59fd7..1e24758 100644 --- a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py @@ -5,9 +5,6 @@ import logging from typing import Any, Optional -from sgr_specification.v0.generic import ( - DataDirectionProduct, Units -) from sgr_specification.v0.generic import ( DataPointBase as ContactDataPointSpec, ) @@ -48,7 +45,7 @@ def build_contact_data_point( return DataPoint(protocol, validator) -class ContactDataPoint(DataPointProtocol[ContactDataPointSpec]): +class ContactDataPoint(DataPointProtocol[ContactFunctionalProfileSpec, ContactDataPointSpec]): """ Implements a data point of a contact interface. """ @@ -59,8 +56,7 @@ def __init__( fp_spec: ContactFunctionalProfileSpec, interface: 'SGrContactInterface', ): - self._dp_spec = dp_spec - self._fp_spec = fp_spec + super(ContactDataPoint, self).__init__(fp_spec, dp_spec) self._fp_name = '' if ( @@ -78,34 +74,12 @@ def __init__( self._interface = interface - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def get_specification(self) -> ContactDataPointSpec: - return self._dp_spec - async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: raise Exception('Not implemented') async def set_val(self, value: Any): raise Exception('Not implemented') - def direction(self) -> DataDirectionProduct: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.data_direction is None - ): - raise Exception('missing data direction') - return self._dp_spec.data_point.data_direction - - def unit(self) -> Units: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.unit is None - ): - return Units.NONE - return self._dp_spec.data_point.unit - class ContactFunctionalProfile(FunctionalProfile[ContactFunctionalProfileSpec]): """ @@ -117,7 +91,7 @@ def __init__( fp_spec: ContactFunctionalProfileSpec, interface: 'SGrContactInterface', ): - self._fp_spec = fp_spec + super(ContactFunctionalProfile, self).__init__(fp_spec) self._interface = interface raw_dps = [] @@ -134,20 +108,9 @@ def __init__( self._data_points = {dp.name(): dp for dp in dps} - def name(self) -> str: - if ( - self._fp_spec.functional_profile - and self._fp_spec.functional_profile.functional_profile_name - ): - return self._fp_spec.functional_profile.functional_profile_name - return '' - def get_data_points(self) -> dict[tuple[str, str], DataPoint]: return self._data_points - def get_specification(self) -> ContactFunctionalProfileSpec: - return self._fp_spec - class SGrContactInterface(SGrBaseInterface): """ diff --git a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py index 6a77b24..9e2ae00 100644 --- a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py @@ -6,7 +6,7 @@ from typing import Any, Optional from sgr_specification.v0.generic import ( - DataDirectionProduct, Units + DataDirectionProduct ) from sgr_specification.v0.generic import ( DataPointBase as GenericDataPointSpec, @@ -48,7 +48,7 @@ def build_generic_data_point( return DataPoint(protocol, validator) -class GenericDataPoint(DataPointProtocol[GenericDataPointSpec]): +class GenericDataPoint(DataPointProtocol[GenericFunctionalProfileSpec, GenericDataPointSpec]): """ Implements a data point of a generic interface. """ @@ -59,8 +59,7 @@ def __init__( fp_spec: GenericFunctionalProfileSpec, interface: 'SGrGenericInterface', ): - self._dp_spec = dp_spec - self._fp_spec = fp_spec + super(GenericDataPoint, self).__init__(fp_spec, dp_spec) self._fp_name = '' if ( @@ -78,12 +77,6 @@ def __init__( self._interface = interface - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def get_specification(self) -> GenericDataPointSpec: - return self._dp_spec - async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: # supports at least constant DPs if ( @@ -97,22 +90,6 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: async def set_val(self, value: Any): raise Exception('Not supported') - def direction(self) -> DataDirectionProduct: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.data_direction is None - ): - raise Exception('missing data direction') - return self._dp_spec.data_point.data_direction - - def unit(self) -> Units: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.unit is None - ): - return Units.NONE - return self._dp_spec.data_point.unit - class GenericFunctionalProfile(FunctionalProfile[GenericFunctionalProfileSpec]): """ @@ -124,7 +101,7 @@ def __init__( fp_spec: GenericFunctionalProfileSpec, interface: 'SGrGenericInterface', ): - self._fp_spec = fp_spec + super(GenericFunctionalProfile, self).__init__(fp_spec) self._interface = interface raw_dps = [] diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py index 1fee05d..519188e 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py @@ -2,6 +2,7 @@ Provides message filter implementations. """ +from abc import ABC, abstractmethod import re import json import jmespath @@ -18,7 +19,7 @@ T = TypeVar('T') -class MessagingFilter(Generic[T]): +class MessagingFilter(ABC, Generic[T]): """ The base class for message filter implementations. """ @@ -26,6 +27,7 @@ class MessagingFilter(Generic[T]): def __init__(self, filter_spec: T): self._filter_spec = filter_spec + @abstractmethod def is_filter_match(self, payload: Any) -> bool: ... diff --git a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py index acf2c2e..d84428a 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_interface_async.py @@ -6,7 +6,7 @@ from collections.abc import Callable from typing import Any, NoReturn, Optional -from sgr_specification.v0.generic import DataDirectionProduct, Units, ResponseQueryType +from sgr_specification.v0.generic import ResponseQueryType from sgr_specification.v0.product import ( DeviceFrame, MessagingDataPoint as MessagingDataPointSpec, @@ -17,7 +17,7 @@ OutMessage ) -from sgr_commhandler.api.dynamic_parameter import build_dynamic_parameter_substitutions, build_dynamic_parameters +from sgr_commhandler.api.dynamic_parameter import build_dynamic_parameter_substitutions from sgr_commhandler.utils import jmespath_mapping, template from sgr_commhandler.driver.messaging.messaging_filter import ( MessagingFilter, @@ -87,7 +87,7 @@ def get_messaging_client(name: str, desc: MessagingInterfaceDescription) -> SGrM raise Exception('no supported platform') -class MessagingDataPoint(DataPointProtocol[MessagingDataPointSpec]): +class MessagingDataPoint(DataPointProtocol[MessagingFunctionalProfileSpec, MessagingDataPointSpec]): """ Implements a data point of a messaging interface. """ @@ -98,33 +98,12 @@ def __init__( fp_spec: MessagingFunctionalProfileSpec, interface: 'SGrMessagingInterface', ): - self._dp_spec = dp_spec - self._fp_spec = fp_spec + super(MessagingDataPoint, self).__init__(fp_spec, dp_spec) dp_config = self._dp_spec.messaging_data_point_configuration if not dp_config: raise Exception('Messaging data point configuration missing') - self._dynamic_parameters = build_dynamic_parameters( - self._dp_spec.data_point.parameter_list - if self._dp_spec.data_point - else None - ) - - self._fp_name = '' - if ( - fp_spec.functional_profile is not None - and fp_spec.functional_profile.functional_profile_name is not None - ): - self._fp_name = fp_spec.functional_profile.functional_profile_name - - self._dp_name = '' - if ( - dp_spec.data_point is not None - and dp_spec.data_point.data_point_name is not None - ): - self._dp_name = dp_spec.data_point.data_point_name - self._in_cmd: Optional[InMessage] = None self._read_cmd: Optional[OutMessage] = None self._write_cmd: Optional[OutMessage] = None @@ -147,12 +126,6 @@ def __init__( if self._in_cmd and self._in_cmd.topic: self._interface.data_point_handler((self._fp_name, self._dp_name, self._in_cmd.topic, self._on_message, self._in_filter)) # type: ignore - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def get_specification(self) -> MessagingDataPointSpec: - return self._dp_spec - async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: if not self._read_cmd: # passive reading only @@ -203,22 +176,6 @@ async def set_val(self, value: Any): await self._interface.write_message(self._write_cmd.topic, value) self._cached_value = value - def direction(self) -> DataDirectionProduct: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.data_direction is None - ): - raise Exception('missing data direction') - return self._dp_spec.data_point.data_direction - - def unit(self) -> Units: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.unit is None - ): - return Units.NONE - return self._dp_spec.data_point.unit - def can_subscribe(self) -> bool: return True @@ -296,7 +253,7 @@ def __init__( fp_spec: MessagingFunctionalProfileSpec, interface: 'SGrMessagingInterface', ): - self._fp_spec = fp_spec + super(MessagingFunctionalProfile, self).__init__(fp_spec) self._interface = interface raw_dps = [] @@ -313,20 +270,9 @@ def __init__( self._data_points = {dp.name(): dp for dp in dps} - def name(self) -> str: - if ( - self._fp_spec.functional_profile - and self._fp_spec.functional_profile.functional_profile_name - ): - return self._fp_spec.functional_profile.functional_profile_name - return '' - def get_data_points(self) -> dict[tuple[str, str], DataPoint]: return self._data_points - def get_specification(self) -> MessagingFunctionalProfileSpec: - return self._fp_spec - class SGrMessagingInterface(SGrBaseInterface): """ diff --git a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py index e563a2e..5b3974b 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -8,8 +8,8 @@ from typing import Any, Optional, Union from math import pow -from sgr_specification.v0.generic import DataDirectionProduct, Parity -from sgr_specification.v0.generic.base_types import DataTypeProduct, Units +from sgr_specification.v0.generic import Parity +from sgr_specification.v0.generic.base_types import DataTypeProduct from sgr_specification.v0.product import ( DeviceFrame, ) @@ -211,7 +211,7 @@ def is_float_type(data_type: Union[DataTypeProduct, ModbusDataType, None]) -> bo return data_type.float32 is not None or data_type.float64 is not None -class ModbusDataPoint(DataPointProtocol[ModbusDataPointSpec]): +class ModbusDataPoint(DataPointProtocol[ModbusFunctionalProfileSpec, ModbusDataPointSpec]): """ Implements a data point of a Modbus interface. """ @@ -222,26 +222,9 @@ def __init__( fp_spec: ModbusFunctionalProfileSpec, interface: 'SGrModbusInterface', ): - self._dp_spec = dp_spec - self._fp_spec = fp_spec + super(ModbusDataPoint, self).__init__(fp_spec, dp_spec) self._interface = interface - self._dp_name: str = '' - if ( - self._dp_spec.data_point - and self._dp_spec.data_point.data_point_name - ): - self._dp_name = self._dp_spec.data_point.data_point_name - - self._fp_name: str = '' - if ( - self._fp_spec.functional_profile - and self._fp_spec.functional_profile.functional_profile_name - ): - self._fp_name = ( - self._fp_spec.functional_profile.functional_profile_name - ) - self._address: int = -1 if ( self._dp_spec.modbus_data_point_configuration @@ -280,9 +263,6 @@ def __init__( else: raise ValueError('Modbus register type not defined') - def get_specification(self) -> ModbusDataPointSpec: - return self._dp_spec - async def set_val(self, value: Any): # special case enum - convert to ordinal if self._dp_spec.data_point and self._dp_spec.data_point.data_type and self._dp_spec.data_point.data_type.enum: @@ -372,25 +352,6 @@ async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: return ret_value - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def direction(self) -> DataDirectionProduct: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.data_direction is None - ): - raise Exception('missing data direction') - return self._dp_spec.data_point.data_direction - - def unit(self) -> Units: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.unit is None - ): - return Units.NONE - return self._dp_spec.data_point.unit - class ModbusFunctionalProfile(FunctionalProfile[ModbusFunctionalProfileSpec]): """ @@ -402,7 +363,7 @@ def __init__( fp_spec: ModbusFunctionalProfileSpec, interface: 'SGrModbusInterface', ): - self._fp_spec = fp_spec + super(ModbusFunctionalProfile, self).__init__(fp_spec) self._interface = interface dps = [ build_modbus_data_point(dp, self._fp_spec, self._interface) @@ -413,18 +374,9 @@ def __init__( ] self._data_points = {dp.name(): dp for dp in dps} - def name(self) -> str: - return self._fp_spec.functional_profile.functional_profile_name if ( - self._fp_spec.functional_profile - and self._fp_spec.functional_profile.functional_profile_name - ) else '' - def get_data_points(self) -> dict[tuple[str, str], DataPoint]: return self._data_points - def get_specification(self) -> ModbusFunctionalProfileSpec: - return self._fp_spec - class SGrModbusInterface(SGrBaseInterface): """ diff --git a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py index 04e9db7..9b88941 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/rest/restapi_interface_async.py @@ -14,7 +14,6 @@ from aiohttp import ClientConnectionError, ClientResponseError from cachetools import TTLCache from multidict import CIMultiDict -from sgr_specification.v0.generic import DataDirectionProduct, Units from sgr_specification.v0.generic.base_types import ResponseQueryType from sgr_specification.v0.product import ( DeviceFrame, @@ -42,8 +41,6 @@ SGrBaseInterface ) from sgr_commhandler.api.dynamic_parameter import ( - DynamicParameter, - build_dynamic_parameters, build_dynamic_parameter_substitutions ) from sgr_commhandler.driver.rest.request import ( @@ -71,7 +68,7 @@ def build_rest_data_point( return DataPoint(protocol, validator) -class RestDataPoint(DataPointProtocol[RestApiDataPointSpec]): +class RestDataPoint(DataPointProtocol[RestApiFunctionalProfileSpec, RestApiDataPointSpec]): """ Implements a data point of a REST API interface. """ @@ -82,8 +79,7 @@ def __init__( fp_spec: RestApiFunctionalProfileSpec, interface: 'SGrRestInterface', ): - self._dp_spec = dp_spec - self._fp_spec = fp_spec + super(RestDataPoint, self).__init__(fp_spec, dp_spec) dp_config = self._dp_spec.rest_api_data_point_configuration if not dp_config: @@ -199,34 +195,8 @@ def __init__( if not self._read_call and not self._write_call: raise Exception('No REST service call configured') - self._dynamic_parameters = build_dynamic_parameters( - self._dp_spec.data_point.parameter_list - if self._dp_spec.data_point - else None - ) - - self._fp_name = '' - if ( - fp_spec.functional_profile is not None - and fp_spec.functional_profile.functional_profile_name is not None - ): - self._fp_name = fp_spec.functional_profile.functional_profile_name - - self._dp_name = '' - if ( - dp_spec.data_point is not None - and dp_spec.data_point.data_point_name is not None - ): - self._dp_name = dp_spec.data_point.data_point_name - self._interface = interface - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def get_specification(self) -> RestApiDataPointSpec: - return self._dp_spec - async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: if not self._read_call: raise Exception('No read call') @@ -317,27 +287,8 @@ async def set_val(self, value: Any): # TODO use response body await self._interface.execute_request(request, skip_cache=True) - def direction(self) -> DataDirectionProduct: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.data_direction is None - ): - raise Exception('missing data direction') - return self._dp_spec.data_point.data_direction - - def unit(self) -> Units: - if ( - self._dp_spec.data_point is None - or self._dp_spec.data_point.unit is None - ): - return Units.NONE - return self._dp_spec.data_point.unit - - def dynamic_parameters(self) -> list[DynamicParameter]: - return self._dynamic_parameters - -class RestFunctionalProfile(FunctionalProfile): +class RestFunctionalProfile(FunctionalProfile[RestApiFunctionalProfileSpec]): """ Implements a functional profile of a REST API interface. """ @@ -347,7 +298,7 @@ def __init__( fp_spec: RestApiFunctionalProfileSpec, interface: 'SGrRestInterface', ): - self._fp_spec = fp_spec + super(RestFunctionalProfile, self).__init__(fp_spec) self._interface = interface raw_dps = [] @@ -364,20 +315,9 @@ def __init__( self._data_points = {dp.name(): dp for dp in dps} - def name(self) -> str: - if ( - self._fp_spec.functional_profile - and self._fp_spec.functional_profile.functional_profile_name - ): - return self._fp_spec.functional_profile.functional_profile_name - return '' - def get_data_points(self) -> dict[tuple[str, str], DataPoint]: return self._data_points - def get_specification(self) -> RestApiFunctionalProfileSpec: - return self._fp_spec - class SGrRestInterface(SGrBaseInterface): """