Source code for openfactory.schemas.connectors.opcua
"""
OPC UA Connector Schemas
This module provides Pydantic models to define and validate configuration
schemas for OPC UA devices within OpenFactory.
Key Models:
-----------
- OPCUAServerConfig:
Configuration for the OPC UA server, including the endpoint URI and namespace URI.
- OPCUADeviceConfig:
Configuration for a device on the OPC UA server. Allows specifying the device
either by a hierarchical path or a NodeId. Variables and methods are optional.
- OPCUAConnectorSchema:
Wrapper schema that encapsulates the server and device configurations.
Validation Features:
--------------------
- Ensures that either 'path' or 'node_id' is provided for a device.
- Variables and methods are optional, providing flexibility for different server setups.
- Forbids unknown fields to ensure strict schema conformance.
YAML Example:
-------------
.. code-block:: yaml
type: opcua
server:
uri: opc.tcp://127.0.0.1:4840/freeopcua/server/
namespace_uri: http://examples.openfactory.local/opcua
device:
path: Sensors/TemperatureSensor_1
variables:
temp: Temperature
hum: Humidity
methods:
calibrate: Calibrate
.. seealso::
The runtime class of the class OPCUAConnectorSchema schema is :class:`openfactory.connectors.opcua.opcua_connector.OPCUAConnector`.
"""
import re
from typing import Optional, Dict, Literal
from pydantic import BaseModel, ConfigDict, model_validator, Field
[docs]
class OPCUAServerConfig(BaseModel):
""" OPC UA Server configuration. """
uri: str = Field(..., description="OPC UA server endpoint URI.")
namespace_uri: str = Field(..., description="Namespace URI of the OPC UA server.")
model_config = ConfigDict(extra="forbid")
[docs]
class OPCUADeviceConfig(BaseModel):
""" OPC UA Device configuration. """
path: Optional[str] = Field(
default=None,
description="Hierarchical path to the device (e.g., 'Sensors/TemperatureSensor_1')."
)
node_id: Optional[str] = Field(
default=None,
description=(
"NodeId of the device, used if 'path' is not defined. "
"Must follow the format 'ns=<namespace_index>;(i|s)=<identifier>'"
),
pattern=r'^ns=\d+;(i|s)=.+$'
)
variables: Optional[Dict[str, str]] = Field(
default=None,
description="Mapping of local names to OPC UA variable BrowseNames."
)
methods: Optional[Dict[str, str]] = Field(
default=None,
description="Mapping of local names to OPC UA method BrowseNames."
)
# Parsed fields (not in input YAML)
namespace_index: Optional[int] = Field(default=None, exclude=True, allow_mutation=False)
identifier_type: Optional[str] = Field(default=None, exclude=True, allow_mutation=False)
identifier: Optional[str] = Field(default=None, exclude=True, allow_mutation=False)
model_config = ConfigDict(extra="forbid")
[docs]
@model_validator(mode="before")
def validate_and_parse_node_id(cls, values: dict) -> dict:
"""
Ensure that exactly one of 'path' or 'node_id' is provided.
If node_id is provided, validate its format and parse namespace_index, identifier_type, and identifier.
"""
path = values.get('path')
node_id = values.get('node_id')
# XOR check: exactly one must be provided
if bool(path) == bool(node_id):
raise ValueError("Exactly one of 'path' or 'node_id' must be specified for the device")
if node_id:
# Validate format
pattern = r"^ns=\d+;(i|s)=.+$"
if not re.match(pattern, node_id):
raise ValueError("Invalid node_id format")
# Parse node_id into fields
ns_part, id_part = node_id.split(";")
ns_index = int(ns_part.replace("ns=", ""))
id_type, identifier = id_part.split("=", 1)
values["namespace_index"] = ns_index
values["identifier_type"] = id_type
values["identifier"] = identifier
return values
[docs]
class OPCUAConnectorSchema(BaseModel):
"""
OPC UA Connector schema wrapping the server and device configuration.
The `type` field is a discriminator for Pydantic to select this schema.
.. seealso::
The runtime class of the class OPCUAConnectorSchema schema is :class:`openfactory.connectors.opcua.opcua_connector.OPCUAConnector`.
"""
type: Literal['opcua'] = Field(
..., # no default, means required
description="Discriminator field to identify OPC UA connector type."
)
server: OPCUAServerConfig = Field(..., description="OPC UA server configuration.")
device: OPCUADeviceConfig = Field(..., description="Device configuration on the server.")
model_config = ConfigDict(extra="forbid")