fix(historian): provision tags whose historization flips on or historian name changes (#600)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -579,7 +579,14 @@ public sealed class AddressSpaceApplier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-provision the historian for the added historized equipment tags. Runs on the OPC UA
|
||||
/// Resolve the historian name EXACTLY as MaterialiseRawSubtree does: a null/blank override falls
|
||||
/// back to the RawPath (== NodeId).
|
||||
/// </summary>
|
||||
private static string ResolveHistorianName(RawTagPlan tag) =>
|
||||
string.IsNullOrWhiteSpace(tag.HistorianTagname) ? tag.NodeId : tag.HistorianTagname;
|
||||
|
||||
/// <summary>
|
||||
/// Auto-provision the historian for the added (and newly-historized / renamed-historian) raw tags. Runs on the OPC UA
|
||||
/// publish actor's pinned thread, so the synchronous portion is kept to building the request
|
||||
/// list only and the gateway round-trip is dispatched fire-and-forget. The whole hook is wrapped
|
||||
/// in try/catch — a synchronously-throwing provisioner (or any request-building fault) is
|
||||
@@ -597,22 +604,39 @@ public sealed class AddressSpaceApplier
|
||||
foreach (var tag in plan.AddedRawTags)
|
||||
{
|
||||
if (!tag.IsHistorized || tag.Alarm is not null) continue;
|
||||
AddRequest(tag);
|
||||
}
|
||||
|
||||
// Parse the driver-agnostic data type from the tag's DataType string. An unparseable
|
||||
// type is skipped (logged at Debug) rather than faulting the hook.
|
||||
// #600.1: a historization flip (false->true) or a historian-name / data-type change on an
|
||||
// EXISTING raw tag never appears in AddedRawTags, so EnsureTags never fired for it. EnsureTags is
|
||||
// an upsert, so re-ensuring is safe; an unchanged pair is a no-op. Only raw deltas are used - a
|
||||
// UNS mirror delta must not double-provision the same historian tag.
|
||||
foreach (var d in plan.ChangedRawTags)
|
||||
{
|
||||
var cur = d.Current;
|
||||
if (!cur.IsHistorized || cur.Alarm is not null) continue;
|
||||
var prev = d.Previous;
|
||||
var wasProvisioned = prev.IsHistorized && prev.Alarm is null;
|
||||
if (wasProvisioned
|
||||
&& ResolveHistorianName(prev) == ResolveHistorianName(cur)
|
||||
&& string.Equals(prev.DataType, cur.DataType, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
AddRequest(cur);
|
||||
}
|
||||
|
||||
void AddRequest(RawTagPlan tag)
|
||||
{
|
||||
// An unparseable data type is skipped (logged at Debug) rather than faulting the hook.
|
||||
if (!Enum.TryParse<DriverDataType>(tag.DataType, ignoreCase: true, out var dataType))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"AddressSpaceApplier: skipping historian provisioning for an added historized tag whose data type '{DataType}' is not a DriverDataType",
|
||||
"AddressSpaceApplier: skipping historian provisioning for a historized tag whose data type '{DataType}' is not a DriverDataType",
|
||||
tag.DataType);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the historian name EXACTLY as MaterialiseRawSubtree does: a null/blank override
|
||||
// falls back to the RawPath (== NodeId).
|
||||
var historianName = string.IsNullOrWhiteSpace(tag.HistorianTagname) ? tag.NodeId : tag.HistorianTagname;
|
||||
(requests ??= new List<HistorianTagProvisionRequest>()).Add(
|
||||
new HistorianTagProvisionRequest(historianName, dataType, EngineeringUnit: null, Description: tag.Name));
|
||||
new HistorianTagProvisionRequest(ResolveHistorianName(tag), dataType, EngineeringUnit: null, Description: tag.Name));
|
||||
}
|
||||
|
||||
if (requests is null) return;
|
||||
|
||||
Reference in New Issue
Block a user