using System.Globalization;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ProtoConditionState = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState;
using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;
// Alias the Commons alarm types so their simple names bind here, unambiguous
// against the colliding gateway proto enums above.
using NativeAlarmTransition = ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms.NativeAlarmTransition;
using AlarmConditionState = ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms.AlarmConditionState;
using AlarmTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
using AlarmShelveState = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmShelveState;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
///
/// Pure mapping from MxAccess Gateway alarm-feed proto messages to the
/// protocol-neutral shape. The gateway proto
/// enums (AlarmConditionState / AlarmTransitionKind) collide with the Commons
/// alarm enums, so they are aliased here. Unit-tested without a live gateway.
///
public static class MxGatewayAlarmMapper
{
/// Clamps the gateway severity onto the unified 0–1000 scale.
/// Raw gateway severity value to clamp.
/// The severity clamped to the range [0, 1000].
public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000);
/// Maps a gateway condition-state + severity to the unified condition.
/// Gateway proto condition state to map.
/// Raw gateway severity value; normalized to 0–1000.
/// The protocol-neutral equivalent.
public static AlarmConditionState MapConditionState(ProtoConditionState state, int severity)
{
var (active, acked) = state switch
{
ProtoConditionState.Active => (true, false),
ProtoConditionState.ActiveAcked => (true, true),
ProtoConditionState.Inactive => (false, true),
_ => (false, true)
};
return new AlarmConditionState(active, acked, Confirmed: null,
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
}
/// Maps a gateway transition kind to the unified transition kind.
/// Gateway proto transition kind to map.
/// The protocol-neutral equivalent.
public static AlarmTransitionKind MapKind(ProtoTransitionKind kind) => kind switch
{
ProtoTransitionKind.Raise => AlarmTransitionKind.Raise,
ProtoTransitionKind.Acknowledge => AlarmTransitionKind.Acknowledge,
ProtoTransitionKind.Clear => AlarmTransitionKind.Clear,
ProtoTransitionKind.Retrigger => AlarmTransitionKind.Retrigger,
_ => AlarmTransitionKind.StateChange
};
/// Derives the mirrored condition from a transition kind + severity.
private static AlarmConditionState ConditionFromKind(ProtoTransitionKind kind, int severity)
{
var (active, acked) = kind switch
{
ProtoTransitionKind.Raise => (true, false),
ProtoTransitionKind.Acknowledge => (true, true),
ProtoTransitionKind.Retrigger => (true, false),
ProtoTransitionKind.Clear => (false, true),
_ => (false, true)
};
return new AlarmConditionState(active, acked, Confirmed: null,
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
}
///
/// Derives the ack timestamp mirrored onto NativeAlarmTransition.AckTime
/// (MES alarm-status API §6.4).
///
///
/// Unlike OPC UA A&C, the MxAccess Gateway alarm feed carries NO dedicated ack
/// timestamp — an acknowledgement arrives as an ACTIVE_ACKED condition state on a
/// transition whose only time is (the gateway's
/// transition timestamp, or the DCL's own receipt time when the gateway omits it). That
/// observation time is what gets stamped: accurate to when the system saw the ack, never
/// fabricated, but not necessarily the operator's click.
///
///
///
/// Returns null unless the condition is active AND acknowledged — matching
/// OpcUaAlarmMapper.DeriveAckTime, so both native protocols agree on "null while
/// unacked, cleared on re-raise". This matters especially here because the gateway maps
/// INACTIVE to acked = true: without the active check, every return-to-normal
/// would report an ack it never observed.
///
///
/// The mirrored condition state for the transition.
/// The transition's timestamp — the DCL's observation instant for the ack.
/// The observed ack timestamp, or null when the condition is not an outstanding acknowledged alarm.
public static DateTimeOffset? DeriveAckTime(AlarmConditionState condition, DateTimeOffset transitionTime) =>
condition is { Active: true, Acknowledged: true } ? transitionTime : null;
///
/// Converts an union to a display-only string using
/// and invariant culture formatting,
/// so numeric values always use '.' as the decimal separator. Null or unset
/// values produce an empty string.
///
/// The gateway value union to convert, or null.
/// The value formatted as an invariant-culture string, or an empty string if null or unset.
internal static string MxValueToString(MxValue? mxVal)
{
if (mxVal is null) return "";
var clr = mxVal.ToClrValue();
return clr is null ? "" : Convert.ToString(clr, CultureInfo.InvariantCulture) ?? "";
}
/// Maps a live to a transition.
/// The gateway alarm transition event proto message to map.
/// The protocol-neutral .
public static NativeAlarmTransition MapTransition(OnAlarmTransitionEvent body)
{
var condition = ConditionFromKind(body.TransitionKind, body.Severity);
var transitionTime = body.TransitionTimestamp?.ToDateTimeOffset() ?? DateTimeOffset.UtcNow;
return new NativeAlarmTransition(
// Identify the condition by the object-relative reference (e.g.
// "Z28061.HeartbeatTimeoutAlarm") rather than the gateway's full provider
// reference ("Galaxy!.