Skip to content

Commit 05e171a

Browse files
Copilotromanettmarcschier
authored
Fix: Enable historical data access for historizing nodes in Reference Server (#3383)
* Initial plan * Fix: Add HistoryReadRawModified override to enable historical data access Co-authored-by: romanett <[email protected]> * Refactor: Inline HistoryReadResult creation per code review Co-authored-by: romanett <[email protected]> * Test: Add HistoryReadInt32ValueNodeAsync test for historizing node Co-authored-by: romanett <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: romanett <[email protected]> Co-authored-by: Marc Schier <[email protected]>
1 parent 74c3cb0 commit 05e171a

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

Applications/Quickstarts.Servers/TestData/TestDataNodeManager.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,91 @@ protected ServiceResult HistoryReadRaw(
631631
return result.StatusCode;
632632
}
633633

634+
/// <summary>
635+
/// Reads raw history data.
636+
/// </summary>
637+
protected override void HistoryReadRawModified(
638+
ServerSystemContext context,
639+
ReadRawModifiedDetails details,
640+
TimestampsToReturn timestampsToReturn,
641+
IList<HistoryReadValueId> nodesToRead,
642+
IList<HistoryReadResult> results,
643+
IList<ServiceResult> errors,
644+
List<NodeHandle> nodesToProcess,
645+
IDictionary<NodeId, NodeState> cache)
646+
{
647+
for (int ii = 0; ii < nodesToProcess.Count; ii++)
648+
{
649+
NodeHandle handle = nodesToProcess[ii];
650+
651+
// validate node.
652+
NodeState source = ValidateNode(context, handle, cache);
653+
654+
if (source == null)
655+
{
656+
continue;
657+
}
658+
659+
// only variables can have history.
660+
if (source is not BaseVariableState variable)
661+
{
662+
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
663+
continue;
664+
}
665+
666+
// read the raw data.
667+
errors[handle.Index] = HistoryReadRaw(
668+
context,
669+
variable,
670+
details,
671+
timestampsToReturn,
672+
false,
673+
nodesToRead[handle.Index],
674+
results[handle.Index]);
675+
}
676+
}
677+
678+
/// <summary>
679+
/// Releases the continuation points for history read operations.
680+
/// </summary>
681+
protected override void HistoryReleaseContinuationPoints(
682+
ServerSystemContext context,
683+
IList<HistoryReadValueId> nodesToRead,
684+
IList<ServiceResult> errors,
685+
List<NodeHandle> nodesToProcess,
686+
IDictionary<NodeId, NodeState> cache)
687+
{
688+
for (int ii = 0; ii < nodesToProcess.Count; ii++)
689+
{
690+
NodeHandle handle = nodesToProcess[ii];
691+
692+
// validate node.
693+
NodeState source = ValidateNode(context, handle, cache);
694+
695+
if (source == null)
696+
{
697+
continue;
698+
}
699+
700+
// only variables can have history.
701+
if (source is not BaseVariableState variable)
702+
{
703+
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
704+
continue;
705+
}
706+
707+
// release the continuation point.
708+
errors[handle.Index] = HistoryReadRaw(
709+
context,
710+
variable,
711+
null,
712+
TimestampsToReturn.Neither,
713+
true,
714+
nodesToRead[handle.Index],
715+
new HistoryReadResult());
716+
}
717+
}
718+
634719
/// <summary>
635720
/// Returns true if the system must be scanning to provide updates for the monitored item.
636721
/// </summary>

Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,114 @@ public async Task ServerEventNotifierHistoryReadBitAsync()
983983
}
984984

985985
/// <summary>
986+
/// Test that the Int32Value node (ns=3;i=2808) allows historical data access.
987+
/// Verifies the fix for issue #2520 where the node was marked as historizing
988+
/// but history read operations returned BadHistoryOperationUnsupported.
989+
/// </summary>
990+
[Test]
991+
public async Task HistoryReadInt32ValueNodeAsync()
992+
{
993+
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
994+
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
995+
996+
// Get the NodeId for Data_Dynamic_Scalar_Int32Value
997+
NodeId int32ValueNodeId = new NodeId(
998+
TestData.Variables.Data_Dynamic_Scalar_Int32Value,
999+
(ushort)m_server.CurrentInstance.NamespaceUris.GetIndex(TestData.Namespaces.TestData));
1000+
1001+
logger.LogInformation("Testing history read for Int32Value node: {NodeId}", int32ValueNodeId);
1002+
1003+
// Verify the node has Historizing attribute set to true
1004+
var readIdCollection = new ReadValueIdCollection {
1005+
new ReadValueId {
1006+
AttributeId = Attributes.Historizing,
1007+
NodeId = int32ValueNodeId
1008+
},
1009+
new ReadValueId {
1010+
AttributeId = Attributes.AccessLevel,
1011+
NodeId = int32ValueNodeId
1012+
}
1013+
};
1014+
1015+
m_requestHeader.Timestamp = DateTime.UtcNow;
1016+
ReadResponse readResponse = await m_server.ReadAsync(
1017+
m_secureChannelContext,
1018+
m_requestHeader,
1019+
0,
1020+
TimestampsToReturn.Both,
1021+
readIdCollection,
1022+
CancellationToken.None).ConfigureAwait(false);
1023+
1024+
ServerFixtureUtils.ValidateResponse(readResponse.ResponseHeader, readResponse.Results, readIdCollection);
1025+
Assert.AreEqual(2, readResponse.Results.Count);
1026+
1027+
bool historizing = (bool)readResponse.Results[0].Value;
1028+
byte accessLevel = (byte)readResponse.Results[1].Value;
1029+
1030+
logger.LogInformation("Historizing: {Historizing}, AccessLevel: {AccessLevel}", historizing, accessLevel);
1031+
1032+
Assert.IsTrue(historizing, "Int32Value node should have Historizing=true");
1033+
Assert.IsTrue((accessLevel & AccessLevels.HistoryRead) != 0,
1034+
"Int32Value node should have HistoryRead access level");
1035+
1036+
// Perform a history read operation
1037+
var historyReadDetails = new ReadRawModifiedDetails {
1038+
StartTime = DateTime.UtcNow.AddHours(-1),
1039+
EndTime = DateTime.UtcNow,
1040+
NumValuesPerNode = 10,
1041+
IsReadModified = false,
1042+
ReturnBounds = false
1043+
};
1044+
1045+
var nodesToRead = new HistoryReadValueIdCollection {
1046+
new HistoryReadValueId {
1047+
NodeId = int32ValueNodeId
1048+
}
1049+
};
1050+
1051+
m_requestHeader.Timestamp = DateTime.UtcNow;
1052+
HistoryReadResponse historyReadResponse = await m_server.HistoryReadAsync(
1053+
m_secureChannelContext,
1054+
m_requestHeader,
1055+
new ExtensionObject(historyReadDetails),
1056+
TimestampsToReturn.Both,
1057+
false,
1058+
nodesToRead,
1059+
CancellationToken.None).ConfigureAwait(false);
1060+
1061+
ServerFixtureUtils.ValidateResponse(historyReadResponse.ResponseHeader, historyReadResponse.Results, nodesToRead);
1062+
Assert.AreEqual(1, historyReadResponse.Results.Count);
1063+
1064+
HistoryReadResult result = historyReadResponse.Results[0];
1065+
1066+
logger.LogInformation("History read StatusCode: {StatusCode}", result.StatusCode);
1067+
1068+
// The result should be Good or GoodMoreData (if there are more values)
1069+
Assert.IsTrue(StatusCode.IsGood(result.StatusCode),
1070+
$"History read should succeed, but got: {result.StatusCode}");
1071+
Assert.IsNotNull(result.HistoryData, "HistoryData should not be null");
1072+
1073+
// Verify we got HistoryData back
1074+
if (result.HistoryData.Body is HistoryData historyData)
1075+
{
1076+
logger.LogInformation("Retrieved {Count} history values", historyData.DataValues.Count);
1077+
Assert.IsNotNull(historyData.DataValues, "DataValues should not be null");
1078+
Assert.Greater(historyData.DataValues.Count, 0, "Should have at least one historical value");
1079+
1080+
// Verify the data values have proper timestamps
1081+
foreach (var dataValue in historyData.DataValues)
1082+
{
1083+
Assert.IsNotNull(dataValue, "DataValue should not be null");
1084+
Assert.IsTrue(dataValue.ServerTimestamp != DateTime.MinValue,
1085+
"DataValue should have a valid ServerTimestamp");
1086+
}
1087+
}
1088+
else
1089+
{
1090+
Assert.Fail("HistoryData body should be of type HistoryData");
1091+
}
1092+
}
1093+
9861094
/// Test provisioning mode - server should start with limited namespace.
9871095
/// </summary>
9881096
[Test]

0 commit comments

Comments
 (0)