Introduction

The eta_utility.connectors module is meant to provide a standardized interface for multiple different protocols which are used in factory operations or for the optimization of factory operations. Two important protocols which we encounter regularly are Modbus TCP and OPC UA. In addition to this we have also created connectors for additional API which we work with.

The eta_utility connector format has the advantage that it can be used for many different kinds of protocols and APIs, with the limitation that some of them do not support all functions (for example, specific APIs/protocols may not provide write access). Each connection can contain multiple Nodes (see below). These are used as the default data points when reading data from the connection. Read data will be returned in a pandas.DataFrame with the node names as column names.

The connectors module also provides subscription handlers which take data read from connections in regular intervals and for example store it in files or in memory for later access. These subscription handlers can handle multiple different connections (with different protocols) at the same time.

The LiveConnect class is specifically designed to combine the functionality from the eta_x and connectors modules. It can establish connections and provides an interface equivalent to the classes in the simulators module. This allows easy substitution of simulation models with actual connections to real machines (or the other way). When trying to deploy a model into operation this substitution can be very useful.

The connectors are based on the concept of Nodes to which we establish connections. A Node object is a unique description of a specific data point and includes all information required to establish a connection and read data from the specified data point. Each data point has its own node, not just each device (or connection) that we are connecting to. Therefore, Nodes are the easiest way to instantiate connections, however they can be a bit unwieldy to work with when trying to read many different data points from the same device.

See Connection instantiation for more information on how to create connections.

Nodes

Each Node object uniquely identifies a specific data point. All Node objects have some information in common. This information identifies the device which the data point belongs to and can also contain information required for authentication with the device. Depending on the protocol the Node object contains additional information to correctly identify the data points.

The URL may contain the username and password (schema://username:password@hostname:port/path). This is handled automatically by the connectors and the username and password will be removed before creating a connection.

The Node class should always be used to instantiate nodes. The type of the node can be specified using the protocol parameter.

class eta_utility.connectors.Node(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

The node objects represents a single variable. Valid keyword arguments depend on the protocol.

The following classes are there to document the required parameters for each type of node.

Note

Always use the Node class to instantiate nodes! (not its subclasses)

class eta_utility.connectors.node.NodeLocal(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Local Node (no specific protocol), useful for example to manually provide data to subscription handlers.

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeModbus(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the Modbus protocol.

mb_slave: int | None

Modbus Slave ID

mb_register: str

Modbus Register name. One of input, discrete_input, coils and holding. Note that only coils and holding can be written to.

mb_channel: int

Modbus Channel (Address of the value)

mb_bit_length: int

Length of the value in bits (default 32). This determines, how much data is read from the server. The value must be a multiple of 16.

mb_byteorder: str

Byteorder of values returned by modbus

mb_wordorder: str

Wordorder of values returned by modbus

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeOpcUa(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the OPC UA protocol.

opc_id: str | None

Node ID of the OPC UA Node.

opc_path_str: str | None

Path to the OPC UA node.

opc_ns: int | None

Namespace of the OPC UA Node.

opc_id_type: str

Type of the OPC UA Node ID Specification.

opc_name: str

Name of the OPC UA Node.

opc_path: list[NodeOpcUa]

Path to the OPC UA node in list representation. Nodes in this list can be used to access any parent objects.

evolve(**kwargs: Any) Node[source]

Returns a new node instance by copying the current node and changing only specified keyword arguments.

This allows for seamless node instantiation with only a few changes.

Adjusted attributes handling according to OpcUa node instantiation logic as in ‘__attrs_post_init__’.

Parameters:

kwargs – Keyword arguments to change.

Returns:

New instance of the node.

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeEnEffCo(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the EnEffCo API.

eneffco_code: str

EnEffCo datapoint code / ID.

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeEntsoE(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the EntsoE API (see ENTSO-E Transparency Platform API).

Available endpoint

Endpoint

Description

ActualGenerationPerType

Actual Generation Per Energy Type

Price

Price day ahead

Currently, there is only two endpoints available, due to the parameter managing required by the API documentation. The other possible endpoints are listed in

eta_utility.connectors.entso_e._ConnectionConfiguration._doc_types

Main bidding zone

Bidding Zone

Description

DEU-LUX

Deutschland-Luxemburg

The other possible bidding zones are listed in

eta_utility.connectors.entso_e._ConnectionConfiguration._bidding_zones

endpoint: str

REST endpoint.

bidding_zone: str

Bidding zone.

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeWetterdienstObservation(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the Wetterdienst API to get weather observations. For more information see: https://wetterdienst.readthedocs.io/en/latest/data/coverage/dwd/observation.html

interval: str

Redeclare interval attribute, but don’t allow it to be optional

static convert_interval_to_resolution(interval: int | str | timedelta) str[source]
parameter: str

Parameter to read from wetterdienst (e.g HUMIDITY or TEMPERATURE_AIR_200)

station_id: str | None

The id of the weather station

latlon: str | None

latitude and longitude (not necessarily a weather station)

number_of_stations: int | None

Number of stations to be used for the query

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeWetterdienstPrediction(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the Wetterdienst API to get weather predictions. For more information see: https://wetterdienst.readthedocs.io/en/latest/data/coverage/dwd/mosmix.html

mosmix_type: str

Type of the MOSMIX prediction. Either ‘SMALL’ or ‘LARGE’

parameter: str

Parameter to read from wetterdienst (e.g HUMIDITY or TEMPERATURE_AIR_200)

station_id: str | None

The id of the weather station

latlon: str | None

latitude and longitude (not necessarily a weather station)

number_of_stations: int | None

Number of stations to be used for the query

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeEmonio(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for the emonio. The parameter to read is specified by the name of the node. Available parameters are defined in the parameter_map class attribute. Additionally, the phase of the parameter can be specified, with ‘a’, ‘b’, ‘c’ or ‘abc’.

https://wiki.emonio.de/de/Emonio_P3

address: int

Modbus address of the parameter to read

phase: str

Phase of the parameter (a, b, c). If not set, all phases are read

evolve(**kwargs: Any) Node

Returns a new node instance by copying the current node and changing only specified keyword arguments.

This allows for seamless node instantiation with only a few changes.

Parameters:

kwargs – Keyword arguments to change.

Returns:

New instance of the node.

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

class eta_utility.connectors.node.NodeForecastSolar(name: str, url: str, protocol: str, *args: Any, **kwargs: Any)[source]

Node for using the Forecast.Solar API.

Mandatory parameters are:

  • The location of the forecast solar plane(s): latitude, longitude,

  • Plane parameters: declination, azimuth and kwp.

Additionally api_key must be set for endpoints other than ‘estimate’, multiple planes or if requests capacity is exceeded.

For multiple planes, the parameters shall be passed as lists of the same length (e.g. [0, 30], [180, 180], [5, 5]).

api_key: str | None

API key for the Forecast.Solar API; string

endpoint: str

Endpoint in (estimate, history, clearsky), defaults to estimate; string

data: str | None

What data to query, i.e. only watts, watt hours, watt hours per period or watt hours per day; string

latitude: int

Latitude of plane location, -90 (south) … 90 (north); handled with a precision of 0.0001 or abt. 10 m

longitude: int

Longitude of plane location, -180 (west) … 180 (east); handled with a precision of 0.0001 or abt. 10 m

declination: int | list[int]

Plane declination, 0 (horizontal) … 90 (vertical) - always in relation to earth’s surface; integer

azimuth: int | list[int]

Plane azimuth, -180 … 180 (-180 = north, -90 = east, 0 = south, 90 = west, 180 = north); integer

kwp: float | list[float]

Installed modules power of plane in kilo watt; float

no_sun: int | None

Format of timestamps in the response, see API doc for values; string Forecast for full day or only sunrise to sunset, 0|1 (API defaults to 0); int

damping_morning: float | None

Damping factor for the morning (API defaults to 0.0)

damping_evening: float | None

Damping factor for the evening (API defaults to 0.0)

horizon: int | list[int] | None

Horizon information; string, (comma-separated list of numerics) See API doc

inverter: float | None

Maximum of inverter in kilowatts or kVA; float > 0

actual: float | None

Actual production until now; float >= 0

name: str

Name for the node.

url: str

URL of the connection.

url_parsed: ParseResult

Parse result object of the URL (in case more post-processing is required).

usr: str | None

Username for login to the connection (default: None).

pwd: str | None

Password for login to the connection (default: None).

interval: str | None

Interval

dtype: Callable | None

Data type of the node (for value conversion). Note that strings will be interpreted as utf-8 encoded. If you do not want this behaviour, use ‘bytes’.

Node Typing

Eta-utility provides a generic type Nodes[N] which can be used to type a sequence or set of nodes. This is useful when working with multiple nodes and you want to ensure that all nodes are of the same type.

from eta_utility.connectors import Node, NodeModbus, NodeOpcUa
from eta_utility.type_hints import Nodes

# Example of typing nodes
modbus_node: NodeModbus = Node("modbus://localhost:502/0/0", "modbus", ...)
opcua_node: NodeOpcUa = Node("opcua://localhost:4840/0/0", "opcua", ...)
nodes: Nodes[NodeModbus] = [modbus_node]

# This will raise a type error
nodes: Nodes[NodeModbus] = [modbus_node, opcua_node]

# Use *Node* to allow different types of nodes
nodes: Nodes[Node] = [modbus_node, opcua_node]
# or explicitly list the types
nodes: Nodes[NodeModbus | NodeOpcUa] = [modbus_node, opcua_node]

Connection Instantiation

There are multiple ways to instantiate connections, depending on the use case. The two most common methods are Connection.from_node() and Connection.from_nodes().
Instantiation with from_node(s) is useful if you already have created some node(s) and would like to create connection(s) from them.
Each connection class also has its own _from_node() method, since the necessary/accepted keywords might differ. To create connections, a password and a username are often required. For setting these the following prioritization applies:
  • If a password is given in the node, take it.

  • If there is no password there, take as “default” a password from the arguments.

  • If there is neither, the username and password are empty.

Creating one or more Connections from node(s)

If you have one or multiple nodes, use from_nodes. Create all of the Node objects first and pass them in a list. from_nodes then returns a dictionary of connections and automatically assigns the nodes to their correct connection. It requires less duplicate information than direct instantiation.

Connection.from_nodes(nodes: Nodes[Node], **kwargs: Any) dict[str, Connection[Node]][source]
Returns a dictionary of connections for nodes with the same url netloc.

This method handles different Connections, unlike from_node(). The keys of the dictionary are the netlocs of the nodes and each connection contains the nodes with the same netloc. (Uses from_node to initialize connections from nodes.)

Parameters:
  • nodes – List of nodes to initialize from.

  • kwargs – Other arguments are ignored.

Returns:

Dictionary of Connection objects with the netloc as key.

Create one Connection

If you have one or more Node objects for the same hostname/protocol and just want to create one connection, you should use the from_node method of the Connection class.

Connection.from_node(node: Nodes[Node], usr: str | None = None, pwd: str | None = None, **kwargs: Any) Connection[source]
Return a single connection for nodes with the same url netloc.

Initialize the connection object from a node object. When a list of Node objects is provided, from_node checks if all nodes match the same connection; it throws an error if they don’t. A node matches a connection if it has the same url netloc.

Parameters:
  • node – Node to initialize from.

  • kwargs – Other arguments are ignored.

Raises:

ValueError: if not all nodes match the same connection.

Returns:

Connection object

Using from_ids

The from_ids() method is helpful if you do not require access to the nodes and just want to quickly create a single connection.

Note

This is not available for all connectors, since the concept of IDs does not apply universally. An example is shown here. Refer to the API documentation of the connector you would like to use to see if the method exists and which parameters are required.

EnEffCoConnection.from_ids(ids: Sequence[str], url: str, usr: str, pwd: str, api_token: str) EnEffCoConnection[source]

Initialize the connection object from an EnEffCo protocol through the node IDs

Parameters:
  • ids – Identification of the Node.

  • url – URL for EnEffco connection.

  • usr – Username for EnEffCo login.

  • pwd – Password for EnEffCo login.

  • api_token – Token for API authentication.

Returns:

EnEffCoConnection object.