feat(dcl): populate obtainable NativeAlarmTransition fields from OPC UA and MxGateway (#27, M2.13)
OPC UA (RealOpcUaClient): - Append 5 new SelectClauses at indices 13–17 (never renumber 0–12): - 13: AlarmConditionType/ActiveState/TransitionTime → OriginalRaiseTime - 14–17: LimitAlarmType HighHighLimit/HighLimit/LowLimit/LowLowLimit → LimitValue - New OpcUaAlarmMapper.PickLimitValue helper: first non-null in HiHi→Hi→Lo→LoLo priority order, InvariantCulture-formatted; empty string for non-limit alarm types. - HandleAlarmEvent reads new indices with fields.Count > N guards; hard minimum (6) unchanged so base ConditionType events still process without the limit fields. - Document unavailable-by-protocol fields (Category, Description, OperatorUser, CurrentValue) inline in BuildAlarmEventFilter and HandleAlarmEvent. MxGateway (MxGatewayAlarmMapper): - MapTransition: CurrentValue and LimitValue now populated via MxValueToString (uses MxValueExtensions.ToClrValue + InvariantCulture) from OnAlarmTransitionEvent proto fields current_value/limit_value. - MapSnapshot: same — populated from ActiveAlarmSnapshot.current_value/limit_value. - MxValueToString helper (internal): null-safe MxValue → string conversion. Tests (17 new, 40 total pass): - OpcUaAlarmMapperTests: PickLimitValue priority, InvariantCulture, all-null case. - MxGatewayAlarmMapperTests: CurrentValue/LimitValue populate from double/string MxValue; absent fields yield empty strings. - RealOpcUaClientAlarmFilterTests: index alignment assertions (count=18, per-index TypeDefinitionId+BrowsePath), regression guard on existing indices 0–12.
This commit is contained in:
@@ -393,6 +393,34 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.ConditionType, "ConditionName")); // 11
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.ConditionType, "Comment")); // 12
|
||||
|
||||
// APPENDED fields (indices 13+): optional — only present on specific derived types.
|
||||
// Guard all reads with fields.Count > N so base-ConditionType events still process.
|
||||
|
||||
// 13: AlarmConditionType/ActiveState/TransitionTime — the UTC instant the active-state
|
||||
// last flipped to TRUE. Mapped to OriginalRaiseTime; absent on non-AlarmCondition
|
||||
// events (ConditionType base events rarely carry it).
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.AlarmConditionType, "ActiveState", "TransitionTime")); // 13
|
||||
|
||||
// 14–17: LimitAlarmType limit thresholds — configuration-time set-points exposed as
|
||||
// event fields by LimitAlarmType and all its subtypes (Exclusive/NonExclusive
|
||||
// Level/Deviation/RateOfChange). Absent on non-limit alarm types (e.g. discrete,
|
||||
// off-normal) — guarded by fields.Count > N below.
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.LimitAlarmType, "HighHighLimit")); // 14
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.LimitAlarmType, "HighLimit")); // 15
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.LimitAlarmType, "LowLimit")); // 16
|
||||
filter.SelectClauses.Add(SelectField(ObjectTypeIds.LimitAlarmType, "LowLowLimit")); // 17
|
||||
|
||||
// UNAVAILABLE via standard OPC UA A&C event fields (documented here so future
|
||||
// maintainers know these were considered, not overlooked):
|
||||
// Category — not a standard event field; server-specific extensions only.
|
||||
// Description — not a per-event text field; the OPC UA Description attribute is a
|
||||
// static node property, not carried in event notifications.
|
||||
// OperatorUser — not available on the standard ConditionRefresh replay stream;
|
||||
// present on Acknowledge/Confirm method call results, but those do
|
||||
// not flow through the monitored-item subscription.
|
||||
// CurrentValue — the live process variable value is NOT a standard A&C event field;
|
||||
// it would require a separate data subscription on the source node.
|
||||
|
||||
ApplyServerSideTypeWhereClause(filter, conditionFilter);
|
||||
return filter;
|
||||
}
|
||||
@@ -507,6 +535,23 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
var shelve = OpcUaAlarmMapper.MapShelve(fields.Count > 10 ? (fields[10].Value as LocalizedText)?.Text : null);
|
||||
var comment = fields.Count > 12 ? (fields[12].Value as LocalizedText)?.Text ?? "" : "";
|
||||
|
||||
// Index 13: ActiveState/TransitionTime → OriginalRaiseTime (when active-state last
|
||||
// transitioned to TRUE). Absent on non-AlarmCondition events → guard + null fallback.
|
||||
DateTimeOffset? originalRaiseTime = null;
|
||||
if (fields.Count > 13 && fields[13].Value is DateTime activeTransitionTime)
|
||||
originalRaiseTime = new DateTimeOffset(activeTransitionTime, TimeSpan.Zero);
|
||||
|
||||
// Indices 14–17: LimitAlarmType set-point thresholds (HighHighLimit/HighLimit/
|
||||
// LowLimit/LowLowLimit). Absent on non-limit alarm types → null when missing.
|
||||
// Pick the first non-null value in priority order (HiHi > Hi > Lo > LoLo) as a
|
||||
// display-only representative limit; the caller is responsible for interpreting
|
||||
// which limit is active using AlarmTypeName or ConditionName.
|
||||
var limitValue = OpcUaAlarmMapper.PickLimitValue(
|
||||
fields.Count > 14 ? fields[14].Value : null,
|
||||
fields.Count > 15 ? fields[15].Value : null,
|
||||
fields.Count > 16 ? fields[16].Value : null,
|
||||
fields.Count > 17 ? fields[17].Value : null);
|
||||
|
||||
var inRefresh = _alarmInRefresh.GetValueOrDefault(handle);
|
||||
var lastState = _alarmLastState.GetValueOrDefault(handle);
|
||||
var (prevActive, prevAcked) = lastState != null && lastState.TryGetValue(sourceRef, out var prev) ? prev : (false, true);
|
||||
@@ -524,15 +569,18 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
AlarmTypeName: ResolveAlarmTypeName(eventType),
|
||||
Kind: kind,
|
||||
Condition: OpcUaAlarmMapper.BuildCondition(active, acked, confirmed, shelve, suppressed, severity),
|
||||
// UNAVAILABLE via standard OPC UA A&C event fields — see BuildAlarmEventFilter comments.
|
||||
Category: "",
|
||||
Description: "",
|
||||
Message: message,
|
||||
// UNAVAILABLE: OperatorUser not on refresh stream — see BuildAlarmEventFilter comments.
|
||||
OperatorUser: "",
|
||||
OperatorComment: comment,
|
||||
OriginalRaiseTime: null,
|
||||
OriginalRaiseTime: originalRaiseTime,
|
||||
TransitionTime: time,
|
||||
// UNAVAILABLE: CurrentValue not a standard A&C event field — see BuildAlarmEventFilter.
|
||||
CurrentValue: "",
|
||||
LimitValue: ""));
|
||||
LimitValue: limitValue));
|
||||
}
|
||||
|
||||
private static NativeAlarmTransition SnapshotComplete() => new(
|
||||
|
||||
Reference in New Issue
Block a user