using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; /// /// Pure mapping helpers turning OPC UA Alarms & Conditions event fields into the /// protocol-neutral / transition shape. Kept /// free of any OPC UA SDK types so it is unit-testable without a live server; /// the SDK field extraction lives in RealOpcUaClient and is exercised by /// the live smoke test. /// public static class OpcUaAlarmMapper { /// Clamps an OPC UA severity (1–1000, sometimes out of range) to the unified 0–1000 scale. /// The raw OPC UA severity value to clamp. /// The severity clamped to the range [0, 1000]. public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000); /// Builds an from the orthogonal A&C sub-states. /// Whether the alarm condition is currently active. /// Whether the alarm has been acknowledged. /// Whether the alarm has been confirmed; null if not supported by the server. /// The current shelving state of the alarm. /// Whether the alarm is suppressed. /// The raw OPC UA severity (1–1000); clamped via . /// The composed . public static AlarmConditionState BuildCondition( bool active, bool acked, bool? confirmed, AlarmShelveState shelve, bool suppressed, int severity) => new(Active: active, Acknowledged: acked, Confirmed: confirmed, Shelve: shelve, Suppressed: suppressed, Severity: NormalizeSeverity(severity)); /// /// Derives the transition kind from the change in active/acked sub-states. /// Acknowledgement takes precedence over an active/inactive edge when both /// change in the same event; an unchanged event is reported as a StateChange. /// /// Acknowledged state before the event. /// Acknowledged state after the event. /// Active state before the event. /// Active state after the event. /// The that best describes the state change. public static AlarmTransitionKind DeriveKind(bool prevAcked, bool nowAcked, bool prevActive, bool nowActive) { if (!prevAcked && nowAcked) return AlarmTransitionKind.Acknowledge; if (!prevActive && nowActive) return AlarmTransitionKind.Raise; if (prevActive && !nowActive) return AlarmTransitionKind.Clear; if (prevActive && nowActive) return AlarmTransitionKind.Retrigger; return AlarmTransitionKind.StateChange; } /// Maps the OPC UA ShelvingState current-state node name to the shelve enum. /// The OPC UA ShelvingState node name, or null when unshelved. /// The corresponding ; defaults to . public static AlarmShelveState MapShelve(string? shelvingStateName) => shelvingStateName switch { "OneShotShelved" => AlarmShelveState.OneShotShelved, "TimedShelved" => AlarmShelveState.TimedShelved, // OPC UA does not expose a distinct "permanent" shelve; treat any other // shelved name as one-shot and "Unshelved"/null as unshelved. null or "Unshelved" => AlarmShelveState.Unshelved, _ => AlarmShelveState.OneShotShelved }; /// /// Picks a representative display-only limit value from the four standard /// LimitAlarmType set-point fields (HighHighLimit, HighLimit, LowLimit, /// LowLowLimit) returned by the OPC UA event SelectClause. /// /// /// The fields are absent (null raw value) on non-limit alarm types (discrete, /// off-normal, etc.). When present, the first non-null value is returned in /// priority order: HighHigh → High → Low → LowLow. The caller may use /// AlarmTypeName or ConditionName to determine which specific /// limit is active; this method intentionally returns the coarsest useful value /// for the common single-limit case without requiring callers to understand the /// OPC UA limit hierarchy. /// /// /// Raw HighHighLimit field value (null when absent). /// Raw HighLimit field value (null when absent). /// Raw LowLimit field value (null when absent). /// Raw LowLowLimit field value (null when absent). /// /// A formatted string representation of the first non-null limit value, or an /// empty string when all four fields are absent (non-limit alarm type). /// public static string PickLimitValue(object? highHighRaw, object? highRaw, object? lowRaw, object? lowLowRaw) { // Standard OPC UA LimitAlarmType limit values are numeric (Double/Float/Int). // Convert with InvariantCulture so the decimal separator is always '.' regardless // of the server's locale. foreach (var raw in new[] { highHighRaw, highRaw, lowRaw, lowLowRaw }) { if (raw is not null) return Convert.ToString(raw, System.Globalization.CultureInfo.InvariantCulture) ?? ""; } return ""; } }