diff --git a/.github/workflows/release-commhandler.yml b/.github/workflows/release-commhandler.yml index 1667f8b..197da1c 100644 --- a/.github/workflows/release-commhandler.yml +++ b/.github/workflows/release-commhandler.yml @@ -42,13 +42,13 @@ 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@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..98c3c98 100644 --- a/.github/workflows/release-specification.yml +++ b/.github/workflows/release-specification.yml @@ -41,13 +41,13 @@ 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@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/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/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/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 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..2607c9c 100644 --- a/commhandler/src/sgr_commhandler/api/data_point_api.py +++ b/commhandler/src/sgr_commhandler/api/data_point_api.py @@ -1,16 +1,25 @@ +""" +Provides the data-point-level API. +""" + +from abc import ABC, abstractmethod 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 +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(Protocol): +class DataPointValidator(object): """ Implements a base class for data point validators. """ @@ -59,21 +68,49 @@ def options(self) -> list[Any]: return [] -class DataPointProtocol(Protocol[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. - + Returns ------- TDpSpec the interface-specific specification """ - ... + return self._dp_spec + @abstractmethod async def set_val(self, value: Any): """ Writes the data point value. @@ -85,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. @@ -95,7 +133,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 @@ -112,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: """ @@ -123,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: """ @@ -134,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]: """ @@ -145,7 +193,7 @@ def dynamic_parameters(self) -> list[DynamicParameter]: list[DynamicParameter] the dynamic parameters """ - return [] + return self._dynamic_parameters def can_subscribe(self) -> bool: """ @@ -176,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 @@ -208,7 +256,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 +264,7 @@ async def get_value_async(self, parameters: Optional[dict[str, str]] = None, ski ------- Any the data point value - + Raises ------ Exception @@ -280,7 +328,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..370e7f6 100644 --- a/commhandler/src/sgr_commhandler/api/device_api.py +++ b/commhandler/src/sgr_commhandler/api/device_api.py @@ -1,6 +1,10 @@ -from collections.abc import Mapping +""" +Provides the device-level API. +""" + +from abc import ABC, abstractmethod 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 @@ -42,7 +46,7 @@ class DeviceInformation: is_local: bool -class SGrBaseInterface(Protocol): +class SGrBaseInterface(ABC): """ Defines an abstract base class for all SGr device interfaces. @@ -54,17 +58,18 @@ class SGrBaseInterface(Protocol): 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 ): + super(SGrBaseInterface, self).__init__() self.device_frame = frame self.configuration_parameters = build_configuration_parameters( frame.configuration_list @@ -92,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. @@ -115,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: @@ -158,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 @@ -172,12 +191,12 @@ 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 - for key, value in ( + (fp_name, key): value + for (key, value) in ( await fp.get_values_async(parameters) ).items() } @@ -197,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/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..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,14 +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. @@ -23,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]: """ @@ -61,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 = {} - for key, dp in self.get_data_points().items(): + 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 - except Exception as e: + 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( @@ -94,4 +106,4 @@ def get_specification(self) -> TFpSpec: TFpSpec the functional profile specification """ - ... + return self._fp_spec 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..986a8bd 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 @@ -6,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 @@ -57,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, @@ -99,8 +104,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 @@ -150,7 +155,7 @@ def eid_path(self, file_path: str) -> 'DeviceBuilder': ---------- file_path: str the path to the EID XML file - + Returns ------- DeviceBuilder @@ -168,7 +173,7 @@ def eid(self, xml: str): ---------- xml: str the EID XML content - + Returns ------- DeviceBuilder @@ -186,7 +191,7 @@ def properties_path(self, file_path: str): ---------- file_path: str the path to the property file - + Returns ------- DeviceBuilder @@ -204,7 +209,7 @@ def properties(self, properties: dict): ---------- properties: dict the properties - + Returns ------- DeviceBuilder @@ -240,7 +245,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 @@ -262,7 +267,7 @@ def parse_device_frame(content: str) -> DeviceFrame: ---------- content: str the EID XML content - + Returns ------- DeviceFrame @@ -283,13 +288,13 @@ def replace_variables(content: str, parameters: dict) -> str: the EID XML content parameters: dict the configuration parameters - + Returns ------- 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)}') @@ -308,7 +313,7 @@ def build_properties(config: list[ConfigurationParameter], properties: dict) -> the EID configuration parameters properties: dict the properties to configure - + Returns ------- dict @@ -335,5 +340,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..1e24758 100644 --- a/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/contact/contact_interface_async.py @@ -1,9 +1,10 @@ +""" +Provides the digital contact interface implementation. +""" + import logging from typing import Any, Optional -from sgr_specification.v0.generic import ( - DataDirectionProduct, Units -) from sgr_specification.v0.generic import ( DataPointBase as ContactDataPointSpec, ) @@ -44,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. """ @@ -55,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 ( @@ -74,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]): """ @@ -113,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 = [] @@ -130,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): """ @@ -154,7 +121,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/__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..9e2ae00 100644 --- a/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/generic/generic_interface_async.py @@ -1,8 +1,12 @@ +""" +Provides the generic interface implementation. +""" + import logging from typing import Any, Optional from sgr_specification.v0.generic import ( - DataDirectionProduct, Units + DataDirectionProduct ) from sgr_specification.v0.generic import ( DataPointBase as GenericDataPointSpec, @@ -44,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. """ @@ -55,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 ( @@ -74,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 ( @@ -93,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]): """ @@ -120,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 = [] @@ -161,7 +142,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/__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..519188e 100644 --- a/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py +++ b/commhandler/src/sgr_commhandler/driver/messaging/messaging_filter.py @@ -1,3 +1,8 @@ +""" +Provides message filter implementations. +""" + +from abc import ABC, abstractmethod import re import json import jmespath @@ -13,7 +18,8 @@ T = TypeVar('T') -class MessagingFilter(Generic[T]): + +class MessagingFilter(ABC, Generic[T]): """ The base class for message filter implementations. """ @@ -21,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: ... @@ -31,12 +38,12 @@ 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) 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) @@ -49,7 +56,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) @@ -65,7 +72,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) @@ -74,7 +81,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 +94,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..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 @@ -145,13 +124,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 - - def name(self) -> tuple[str, str]: - return self._fp_name, self._dp_name - - def get_specification(self) -> MessagingDataPointSpec: - return self._dp_spec + self._interface.data_point_handler((self._fp_name, self._dp_name, self._in_cmd.topic, self._on_message, self._in_filter)) # type: ignore async def get_val(self, parameters: Optional[dict[str, str]] = None, skip_cache: bool = False) -> Any: if not self._read_cmd: @@ -173,7 +146,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: @@ -203,32 +176,16 @@ 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 - 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 @@ -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): """ @@ -336,7 +282,7 @@ class SGrMessagingInterface(SGrBaseInterface): def __init__( self, frame: DeviceFrame ): - super().__init__(frame) + super(SGrMessagingInterface, self).__init__(frame) if ( self.device_frame.interface_list @@ -354,7 +300,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..622c5af 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 @@ -46,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. @@ -70,11 +74,11 @@ 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 - ) -> None: + ): """ Encodes value to be written to coil address. @@ -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): @@ -251,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( @@ -298,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 2d79531..5b3974b 100644 --- a/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py +++ b/commhandler/src/sgr_commhandler/driver/modbus/modbus_interface_async.py @@ -1,12 +1,15 @@ -from io import UnsupportedOperation +""" +Provides the Modbus interface implementation. +""" + 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 -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, ) @@ -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 @@ -126,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( @@ -155,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. @@ -181,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. @@ -196,7 +211,7 @@ def is_float_type(data_type: DataTypeProduct | ModbusDataType | None) -> bool: 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. """ @@ -207,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 @@ -265,12 +263,9 @@ 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.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 +284,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 +292,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 +320,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 +341,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) @@ -357,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]): """ @@ -387,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) @@ -398,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): """ @@ -421,8 +388,8 @@ def __init__( frame: DeviceFrame, sharedRTU: bool = False, ): - super().__init__(frame) - self._client_wrapper: ModbusClientWrapper = None # type: ignore + super(SGrModbusInterface, self).__init__(frame) + 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 @@ -566,7 +533,7 @@ async def write_data( address: int, data_type: ModbusDataType, value: Any, - ) -> None: + ): """ Writes data to the given Modbus address(es). """ diff --git a/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py b/commhandler/src/sgr_commhandler/driver/modbus/payload_decoder.py index d060d2a..c0e198b 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 @@ -14,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): """ @@ -68,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/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..ff3ce4b 100644 --- a/commhandler/src/sgr_commhandler/driver/rest/authentication.py +++ b/commhandler/src/sgr_commhandler/driver/rest/authentication.py @@ -1,8 +1,12 @@ +""" +Provides HTTP/REST authentication methods. +""" + import base64 import json import logging import re -from typing import Any, Awaitable, Callable, TypeAlias +from typing import Any, Awaitable, Callable import aiohttp import jmespath @@ -22,10 +26,6 @@ logger = logging.getLogger(__name__) -Authenticator: TypeAlias = Callable[ - [RestApiInterface, ClientSession], Awaitable[bool] -] - async def authenticate_not( interface: RestApiInterface, session: ClientSession @@ -39,7 +39,7 @@ async def authenticate_not( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -61,7 +61,7 @@ async def authenticate_with_bearer_token( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -91,68 +91,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 +173,7 @@ async def authenticate_with_basic_auth( the device interface session : ClientSession the REST client session - + Returns ------- bool @@ -199,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, @@ -219,7 +219,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..9b88941 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 @@ -10,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, @@ -38,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 ( @@ -67,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. """ @@ -78,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: @@ -194,35 +194,9 @@ 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') @@ -313,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. """ @@ -343,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 = [] @@ -360,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): """ @@ -383,7 +327,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/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..644d168 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) @@ -247,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): @@ -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) @@ -289,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]] 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..5565248 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 @@ -16,8 +20,8 @@ class UnsupportedValidator(DataPointValidator): """ def __init__(self): - super().__init__(DataTypes.UNDEFINED) - + super(UnsupportedValidator, self).__init__(DataTypes.UNDEFINED) + def validate(self, value: Any) -> bool: return False @@ -28,15 +32,18 @@ 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: 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: @@ -55,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)) @@ -82,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: @@ -103,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: @@ -123,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: @@ -143,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: @@ -157,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: @@ -172,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 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 -