SIENTIAPDE-1811

Enhance OPC UA communication and metrics tracking

- Updated README.md to include new OPC UA Communication section and detailed metrics for session and write diagnostics.
- Added new metrics in laborious/metrics.py for tracking OPC UA session states and write attempts.
- Refactored OPC activity in laborious/activities/opc.py to handle session errors and improve error reporting.
- Updated e2e tests to cover new scenarios for OPC session/channel errors and reconnect handling.
- Modified .gitignore to include relatorio files and mlruns directory.
- Added ipykernel to requirements-dev.txt for Jupyter notebook support.
This commit is contained in:
vitor-aignosi
2026-05-15 15:28:28 -03:00
parent 473bd0b03f
commit 638d5b70b4
15 changed files with 1370 additions and 424 deletions

View File

@@ -5,7 +5,15 @@ from pandas import DataFrame
from pytest import mark
from sientia_do.notifications.models import NotificationLevel
from laborious.activities.opc import OPC
from laborious.activities.opc import (
OPC,
OPC_COMMENT_SEPARATOR,
OPC_RECONNECT_IN_PROGRESS_COMMENT,
OPC_SESSION_BAD_COMMENT_PREFIX,
OPC_SESSION_BAD_CONFIDENCE,
OPC_WRITTING_ERROR_CONFIDENCE,
OPC_WRITTING_ERROR_MESSAGE,
)
metadata = {
'metadata': {
@@ -206,7 +214,7 @@ WRITE_DATA_CASES = [
async def test_write_data_success(opc, tag, data_type, data):
opc.opc_repository['server1'].write_data.return_value = (True, {'response_time': 0.1})
result = await opc.write_data(
response_time, error_info = await opc.write_data(
server_id='server1',
tag=tag,
data=data,
@@ -214,10 +222,9 @@ async def test_write_data_success(opc, tag, data_type, data):
tag_type='prediction',
metadata=metadata,
)
assert result == 0.1
opc.opc_repository['server1'].write_data.assert_called_once_with(
tag, data, data_type, opc.logger, metadata
)
assert response_time == 0.1
assert error_info is None
opc.opc_repository['server1'].write_data.assert_called_once_with(tag, data, data_type, metadata)
@mark.asyncio
@@ -233,7 +240,7 @@ async def test_write_data_failed(opc):
},
)
result = await opc.write_data(
response_time, error_info = await opc.write_data(
server_id='server1',
tag='tag1',
data=50,
@@ -241,7 +248,8 @@ async def test_write_data_failed(opc):
tag_type='prediction',
metadata=metadata,
)
assert result is None
assert response_time is None
assert error_info is not None
opc.send_notification_async.assert_called_once_with(
metadata=metadata,
@@ -283,7 +291,7 @@ async def test_write_data_exception(opc):
@mark.asyncio
async def test_manage_output_tags_success(opc):
opc.write_data = AsyncMock(return_value=0.1)
opc.write_data = AsyncMock(return_value=(0.1, None))
data = DataFrame({'prediction': [0.75], 'prediction_confidence': [0.95]})
config = {
@@ -291,7 +299,7 @@ async def test_manage_output_tags_success(opc):
'confidence_tags': {'tag2': {'data_type': 'float'}},
}
output_data, opc_metrics = await opc.manage_output_tags(
output_data, opc_metrics, _, _, _ = await opc.manage_output_tags(
server_id='server1',
config=config,
data=data,
@@ -323,7 +331,13 @@ async def test_manage_output_tags_success(opc):
@mark.asyncio
@mark.parametrize('side_effect', [[0.1, None], [None, 0.2]])
@mark.parametrize(
'side_effect',
[
[(0.1, None), (None, {})],
[(None, {}), (0.2, None)],
],
)
async def test_manage_output_tags_failed(opc, side_effect):
opc.write_data = AsyncMock(side_effect=side_effect)
data = DataFrame({'prediction': [0.75], 'prediction_confidence': [0.95]})
@@ -331,14 +345,14 @@ async def test_manage_output_tags_failed(opc, side_effect):
'prediction_tags': {'tag1': {'data_type': 'float'}},
'confidence_tags': {'tag2': {'data_type': 'float'}},
}
output_data, opc_metrics = await opc.manage_output_tags(
output_data, opc_metrics, _, _, _ = await opc.manage_output_tags(
server_id='server1',
config=config,
data=data,
metadata=metadata['metadata'],
)
assert output_data is False
assert opc_metrics == {'tag1': side_effect[0], 'tag2': side_effect[1]}
assert opc_metrics == {'tag1': side_effect[0][0], 'tag2': side_effect[1][0]}
opc.write_data.assert_has_calls(
[
call(
@@ -363,12 +377,12 @@ async def test_manage_output_tags_failed(opc, side_effect):
@mark.asyncio
async def test_manage_output_tags_do_nothing(opc):
opc.write_data = AsyncMock(return_value=0.1)
opc.write_data = AsyncMock(return_value=(0.1, None))
data = DataFrame({'prediction': [0.75], 'prediction_confidence': [0.95]})
config = {
'_invalid_key': {'tag1': {'data_type': 'float'}},
}
output_data, opc_metrics = await opc.manage_output_tags(
output_data, opc_metrics, _, _, _ = await opc.manage_output_tags(
server_id='server1',
config=config,
data=data,
@@ -395,7 +409,9 @@ async def test_write_opc_data_success(mock_dataframe, opc):
}
# Act
opc.manage_output_tags = AsyncMock(return_value=(True, {'tag1': 0.1, 'tag2': 0.2}))
opc.manage_output_tags = AsyncMock(
return_value=(True, {'tag1': 0.1, 'tag2': 0.2}, False, None, False)
)
opc.process_confidence = MagicMock(return_value={'data': 'data'})
output_data, opc_metrics = await opc.write_opc_data(input_data)
@@ -413,6 +429,9 @@ async def test_write_opc_data_success(mock_dataframe, opc):
mock_dataframe.return_value,
True,
metadata['metadata'],
session_bad=False,
opc_status=None,
reconnect_in_progress=False,
)
@@ -462,13 +481,130 @@ async def test_write_opc_data_no_validate_server(opc):
],
)
def test_process_confidence(opc, data, success, expected):
# Act
result = opc.process_confidence(data, success, metadata)
# Assert
result = opc.process_confidence(data, success, metadata['metadata'])
assert result['prediction_confidence'][0] == expected
def test_process_confidence_session_bad(opc):
data = DataFrame({'prediction_confidence': [0.9]})
result = opc.process_confidence(
data,
False,
metadata['metadata'],
session_bad=True,
opc_status='BadSessionIdInvalid',
)
assert result['prediction_confidence'][0] == OPC_SESSION_BAD_CONFIDENCE
assert result['comments'][0].startswith(OPC_SESSION_BAD_COMMENT_PREFIX)
assert 'BadSessionIdInvalid' in result['comments'][0]
def test_process_confidence_generic_failure(opc):
data = DataFrame({'prediction_confidence': [0.9]})
result = opc.process_confidence(data, False, metadata['metadata'])
assert result['prediction_confidence'][0] == OPC_WRITTING_ERROR_CONFIDENCE
assert result['comments'][0] == OPC_WRITTING_ERROR_MESSAGE
@mark.asyncio
async def test_manage_output_tags_session_bad(opc):
opc.write_data = AsyncMock(
side_effect=[
(
None,
{
'opc_error_kind': 'session_bad',
'opc_status': 'BadSessionIdInvalid',
'notification_id': 'OPC_WRITE_DATA_ERROR_1',
'message': 'bad',
'block': 'opc_repository',
'level': NotificationLevel.ERROR,
},
),
(0.2, None),
]
)
data = DataFrame({'prediction': [0.75], 'prediction_confidence': [0.95]})
config = {
'prediction_tags': {'tag1': {'data_type': 'float'}},
'confidence_tags': {'tag2': {'data_type': 'float'}},
}
(
success,
metrics,
session_bad_seen,
opc_status,
reconnect_in_progress,
) = await opc.manage_output_tags('server1', config, data, metadata['metadata'])
assert success is False
assert session_bad_seen is True
assert reconnect_in_progress is False
assert opc_status == 'BadSessionIdInvalid'
assert metrics['tag1'] is None
assert metrics['tag2'] == 0.2
@mark.asyncio
async def test_manage_output_tags_reconnect_in_progress(opc):
opc.write_data = AsyncMock(
return_value=(
None,
{
'opc_error_kind': 'reconnect_in_progress',
'notification_id': 'OPC_WRITE_RECONNECT_IN_PROGRESS_1',
'message': 'skipped',
'block': 'opc_repository',
'level': NotificationLevel.WARNING,
},
)
)
data = DataFrame({'prediction': [0.75], 'prediction_confidence': [0.95]})
config = {'prediction_tags': {'tag1': {'data_type': 'float'}}}
(
success,
metrics,
session_bad_seen,
opc_status,
reconnect_in_progress,
) = await opc.manage_output_tags('server1', config, data, metadata['metadata'])
assert success is False
assert session_bad_seen is False
assert reconnect_in_progress is True
assert opc_status is None
assert metrics['tag1'] is None
def test_process_confidence_reconnect_in_progress(opc):
data = DataFrame({'prediction_confidence': [0.9]})
result = opc.process_confidence(
data,
False,
metadata['metadata'],
reconnect_in_progress=True,
)
assert result['prediction_confidence'][0] == OPC_SESSION_BAD_CONFIDENCE
assert result['comments'][0] == OPC_RECONNECT_IN_PROGRESS_COMMENT
def test_process_confidence_concatenates_multiple_comments(opc):
data = DataFrame({'prediction_confidence': [0.9]})
session_comment = f'{OPC_SESSION_BAD_COMMENT_PREFIX} BadSessionIdInvalid'
result = opc.process_confidence(
data,
False,
metadata['metadata'],
session_bad=True,
opc_status='BadSessionIdInvalid',
reconnect_in_progress=True,
)
assert result['prediction_confidence'][0] == OPC_SESSION_BAD_CONFIDENCE
assert result['comments'][0] == OPC_COMMENT_SEPARATOR.join(
[session_comment, OPC_RECONNECT_IN_PROGRESS_COMMENT]
)
@mark.asyncio
async def test_validate_server(opc):
assert await opc.validate_server('server1', metadata) is True