Загрузка данных
"""
OPC UA клиент для чтения выходных сигналов СОУ (обратный обмен СДКУ).
Подключается к OPC UA серверу по opc.tcp и читает значения тегов
(Value + StatusCode + SourceTimestamp). Используется как в setup-части
(проверка доступности OPC), так и в сценариях имитации выходных сигналов.
"""
import asyncio
import logging
from typing import Any, Optional
from asyncua import Client, ua
from constants.architecture_constants import OpcUaConstants as OpcConst
logger = logging.getLogger(__name__)
def build_opc_node_id(
ost_name: str,
stand_name: str,
address: str,
suffix: str,
namespace_index: int = OpcConst.NAMESPACE_INDEX,
) -> str:
"""
Собирает NodeId вида ns=2;s=CHTNTEST4:CHTN_linearParts_9.isLeakDetected.
ost_name: имя ОСТ (например "CHTN").
stand_name: имя стенда (например "TEST4").
address: тег участка из конфигурации трубы (например "CHTN_linearParts_9").
suffix: суффикс сигнала (например "isLeakDetected").
namespace_index: номер пространства имён (по умолчанию NS=2).
"""
return f"ns={namespace_index};s={ost_name}{stand_name.upper()}:{address}.{suffix}"
def read_data_value_sync(
opc_url: str,
node_id: str,
timeout: float = OpcConst.CONNECT_TIMEOUT_S,
) -> ua.DataValue:
"""
Синхронное подключение и чтение DataValue тега. Используется в setup-части
(синхронный контекст pytest), чтобы понять, что OPC UA сервер жив и отдаёт данные.
"""
async def _read() -> ua.DataValue:
client = Client(url=opc_url, timeout=timeout)
try:
await client.connect()
logger.info("[OPC] Подключено к %s", opc_url)
return await client.get_node(node_id).read_data_value()
finally:
await client.disconnect()
return asyncio.run(_read())
class OpcUaClient:
"""Асинхронный OPC UA клиент с переподключением."""
def __init__(self, url: str, timeout: float = OpcConst.OPERATION_TIMEOUT_S) -> None:
self._url = url
self._timeout = timeout
self._client: Optional[Client] = None
async def connect(self, attempts: int = OpcConst.RECONNECT_ATTEMPTS) -> None:
"""Подключается к OPC UA серверу с ограниченным числом попыток."""
last_error: Optional[Exception] = None
for attempt in range(1, attempts + 1):
try:
self._client = Client(url=self._url, timeout=self._timeout)
await self._client.connect()
logger.info("[OPC] [OK] Подключено к %s", self._url)
return
except Exception as error:
last_error = error
logger.warning("[OPC] [WARNING] Подключение не удалось (попытка %s/%s): %s", attempt, attempts, error)
if attempt < attempts:
await asyncio.sleep(OpcConst.RECONNECT_INTERVAL_S)
raise ConnectionError(f"Не удалось подключиться к OPC UA серверу {self._url}: {last_error}") from last_error
async def disconnect(self) -> None:
if self._client is not None:
await self._client.disconnect()
self._client = None
async def read_data_value(self, node_id: str) -> ua.DataValue:
"""Читает DataValue (Value + StatusCode + SourceTimestamp) тега."""
if self._client is None:
raise RuntimeError("OPC UA клиент не подключён")
return await self._client.get_node(node_id).read_data_value()
async def read_value(self, node_id: str) -> Any:
"""Читает только значение тега."""
if self._client is None:
raise RuntimeError("OPC UA клиент не подключён")
return await self._client.get_node(node_id).read_value()