Creating Modules
Take a look at our existing modules to get a feel for what a module can look like.
Start by creating a Python file in your corresponding development folder (see: Setup Dev Env).
Classes and Attributes
Modules have to be derived from the according parent classes.
The parent classes are called AbstractInputModule
, AbstractVariableModule
, AbstractTagModule
, AbstractOutputModule
, or AbstractProcessorModule
.
The derived child class has to be named InputModule
, VariableModule
, TagModule
, OutputModule
, or ProcessorModule
!
Each module needs the following attributes:
__version__: int = 1
"""The auto-generated version of the module."""
class ProcessorModule(AbstractProcessorModule):
version: int = __version__
"""The version of the module."""
public: bool = True
"""Is this module public?"""
description: str = "A short but powerful description of the module."
"""A short description."""
author: str = "Colin Reiff"
"""The author name."""
email: str = "colin.reiff@collectu.de"
"""The email address of the author."""
deprecated: bool = False
"""Is this module deprecated."""
third_party_requirements: list[str] = []
"""Define your requirements here."""
can_be_buffer: bool = False # Only output modules!
"""If True, the child has to implement 'store_buffer_data' and 'get_buffer_data'."""
field_requirements: list[str] = [] # Only preprocessor and output modules!
"""Data validation requirements for the fields dict."""
tag_requirements: list[str] = [] # Only preprocessor and output modules!
"""Data validation requirements for the tags dict."""
The available attributes and methods are shown in the following class diagram.
Only the _run
method is mandatory.
The general methods are described below. The methods and features specific to the module type are described in the appropriate sections.
Configuration
The configuration defines parameters, which can be adjusted by the user in order to control the behaviour of the module. A configuration is a dataclass directly located under the module class itself.
The fields of configurations always should have the following attributes: description
,
dynamic
(default: False
), and required
(default: False
).
Additionally, a validation (validate=
) can be provided.
The following validations are currently available:
OneOf | Checks if the value is one of the given possibilities. |
Range | Checks if the value is in between a min and max value (exclusive). |
Regex | Performs a regular expression validation with the given regular expression string. |
Example
from dataclasses import dataclass, field
from models.validations import Range
from modules.base.inputs.base import AbstractVariableModule, models
class VariableModule(AbstractVariableModule):
"""
A variable module.
"""
@dataclass
class Configuration(models.VariableModule):
"""
The configuration model of the module.
"""
key: str = field(
metadata=dict(description="The key of the constant.",
dynamic=True,
required=True),
default=None) # If the value has to be defined by the user (required=True), provide None as default.
sleep_time: float = field(
metadata=dict(description="The interval in seconds until the data is forwarded. "
"Has to be between (exclusive) 0 and 86400 s (24 h).",
required=False,
dynamic=False,
validate=Range(min=0, max=86400)),
default=1)
The configuration data class should be derived from the corresponding parent class shown below. Be careful not to overwrite the predefined attributes.
Nested Configuration
Nested configurations (here lists as e.g. list[Threshold]
) need a special validation.
This is achieved by calling NestedListClassValidation
on the according field.
Furthermore, the __post_init__
method has to be called in the configuration class, in order to set the validated and deserialized field and dynamic variables.
Example
from dataclasses import dataclass, field
from models.validations import validate_module, NestedListClassValidation
from modules.base.inputs.base import AbstractVariableModule, models
@dataclass
class Field:
"""
The configuration of a single field.
"""
key: str = field(
metadata=dict(description="The key of the field.",
required=True,
dynamic=True),
default=None)
value: str | float | int | bool | list = field(
metadata=dict(description="The (default) value of the field.",
required=False,
dynamic=True),
default="Your input")
def __post_init__(self):
validate_module(self)
class VariableModule(AbstractVariableModule):
"""
A variable module.
"""
@dataclass
class Configuration(models.VariableModule):
"""
The configuration model of the module.
"""
title: str = field(
metadata=dict(description="The title of the user input element.",
required=False),
default="User Input")
fields: list[Field] = field(
metadata=dict(description="The list of fields for this user input module. Example: "
'{"key": "test", "value": ["choice 1", "choice 2"]}',
required=False,
dynamic=True,
validate=NestedListClassValidation(child_class=Field, child_field_name="fields")),
default_factory=list)
def __post_init__(self):
super().__post_init__()
fields = []
for field_dict in self.fields:
fields.append(Field(**field_dict))
setattr(self, "fields", fields)
Further examples for nested module configurations are:
-
inputs.general.user_input_1
-
processors.dashboard.threshold_1
-
processors.opcua.ads_2_opcua_server_1
-
inputs.control.ads_client_struct_1
-
inputs.control.ads_client_struct_2
-
processors.general.template_var_setter_1
Note: Nested configurations are currently not rendered on the frontend. Users have to provide the nested configuration as json in the according input field.
Third Party Requirements
If third party requirements are used by the module, list them in third_party_requirements
.
Subsequently, they have to be imported in the import_third_party_requirements
method.
Make sure the requirements do not conflict with the global app requirements (listed in requirements.txt
).
They have to be the same as the global ones. You can check this by executing the test framework (see Testing).
Example
class VariableModule(AbstractVariableModule):
"""
A variable module.
"""
third_party_requirements: list[str] = ["redis==4.3.4"]
"""Define your requirements here."""
...
@classmethod
def import_third_party_requirements(cls) -> bool:
"""
Import your requirements here and make them globally accessible.
"""
try:
global redis
import redis
return True
except Exception:
raise ImportError("Could not import required packages. Please install '{0}'."
.format(' '.join(map(str, cls.third_party_requirements))))
AUTO_INSTALL=1
).
Get Config Data
The static get_config_data
method is used by the front end to retrieve possible configuration options.
Example
@staticmethod
def get_config_data(input_module_instance=None) -> dict[str, Any]:
"""
Retrieve options for selected configuration parameters of this module.
:param input_module_instance: If it is a variable or tag module, the input_module_instance is given here,
if it is required for that module.
:returns: A dictionary containing the parameters as key and a list of options as value.
"""
return {"qos": [0, 1, 2],
"data_type": ['str', 'bool', 'int', 'float', 'list/int', 'list/str', 'list/bool', 'list/float']}
Start and Stop
The start and stop methods are called by the underlying framework. If something goes wrong during the start phase, throw an exception with a meaningful error message. If the user has configured a retry procedure, the start method is called at a defined interval until the method is executed without exceptions.
The modules are started in the following order (and stopped in the reverse order):
- Buffer modules
- Output modules
- Procsesor modules
- Input modules
- Tag modules
- Variable modules
When a module is stopped, the self.active
attribute is set to false.
Dynamic Variables
If a configuration variable can be dynamic (dynamic=True
), you have to call self._dyn(self.configuration.value)
in order to try to replace the marker (e.g. ${module_id.key}
) with the actual value in your code.
If the dynamic variable could not be replaced, a DynamicVariableException
is raised.
Example
from modules.base.base import DynamicVariableException
try:
field_key = self._dyn(self.configuration.key)
except DynamicVariableException as e:
...
Setup Dev Env
Data Requirements