fix(alarms): Severity runs one direction, higher = more urgent; raw priority kept (#129)
AlarmConditionState.Severity mixed two conventions: OPC UA rows carried Part 9 Severity (higher = more severe) while MxGateway native alarms and computed alarms carried a raw AVEVA-style priority (1 = most urgent). Every consumer ranks higher as worse, so Worst Severity, the severity filter/sort and the site event-log Error threshold picked the LEAST urgent MxGateway/computed alarms. - AlarmPriorityBands: the OtOpcUa GalaxyAlarmPriority table (lmxopcua#887, b61f3e05), value for value: 1-250 -> 1000, 251-500 -> 750, 501-750 -> 500, 751-999 -> 250; <= 0 clamps to Critical, > 999 to Low. - MxGatewayAlarmMapper bands the gateway severity (= AVEVA priority); AlarmConditionStateFactory.ForComputed bands PriorityLevel. OPC UA unchanged. - AlarmConditionState gains trailing Priority (int?): the raw source number, exactly the pre-#129 Severity. AlarmStateChanged.Priority and the wire AlarmStateUpdate.priority keep that meaning (no proto change); central derives Severity by kind (SeverityFromPriority), so an older site decodes correctly. - ScriptAlarm gains trailing Priority. The native MES 900-999 band and the MES API Severity/MinSeverity/MaxSeverity key off Priority; rig seed scripts moved from a.Severity to a.Priority, so MES output is unchanged. - AlarmActor raise logs Error when the banded severity >= 700 (same threshold as NativeAlarmActor): priority 1-500 Error, 501+ Warning (was raw >= 700). - NativeAlarmActor upgrades pre-#129 native_alarm_state rows on rehydrate. - Debug snapshot decode restores Condition.Priority from the row's priority. - Docs: Commons, DCL, SiteRuntime, Communication, CentralUI, InboundAPI, HighLevelReqs, MES alarm-status plan, CLAUDE.md. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -345,7 +345,7 @@ scadabridge --url <url> template native-alarm-source add --template-id <int> --n
|
||||
| `--filter` | no | Condition filter (omit to mirror all conditions under the source) |
|
||||
| `--description` | no | Description |
|
||||
| `--locked` | no | Lock the binding in derived templates |
|
||||
| `--mes-reportable` | no | Every condition under this binding is MES-reportable (additive to the 900-999 severity band) |
|
||||
| `--mes-reportable` | no | Every condition under this binding is MES-reportable (additive to the 900-999 priority band on `ScriptAlarm.Priority`) |
|
||||
|
||||
#### `template native-alarm-source list`
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ public sealed record AlarmSummaryRow(
|
||||
/// Pure point-in-time roll-up over a set of <see cref="AlarmSummaryRow"/>s.
|
||||
/// </summary>
|
||||
/// <param name="TotalActive">Count of rows whose <see cref="AlarmStateChanged.State"/> is <see cref="AlarmState.Active"/>.</param>
|
||||
/// <param name="WorstSeverity">Highest <see cref="Commons.Types.Alarms.AlarmConditionState.Severity"/> among active rows; 0 when none active.</param>
|
||||
/// <param name="WorstSeverity">Highest <see cref="Commons.Types.Alarms.AlarmConditionState.Severity"/> among active rows — the most urgent, since Severity is higher = more urgent for every kind (Gitea #129); 0 when none active.</param>
|
||||
/// <param name="UnackedCount">Active, unacknowledged native conditions (<c>Kind != Computed</c>).</param>
|
||||
/// <param name="CountsByKind">Per-<see cref="AlarmKind"/> row counts (only kinds with at least one row appear).</param>
|
||||
public sealed record AlarmRollup(
|
||||
|
||||
@@ -26,7 +26,7 @@ public class TemplateNativeAlarmSource
|
||||
/// <summary>
|
||||
/// When true, every condition mirrored under this binding is MES-reportable
|
||||
/// (<c>AlarmStateChanged.IsMesReportable</c> / <c>ScriptAlarm.IsMesReportable</c>). Additive to the
|
||||
/// 900-999 severity-band convention, which still flags a condition on its own. Default false.
|
||||
/// 900-999 priority-band convention (on <c>ScriptAlarm.Priority</c>, Gitea #129), which still flags a condition on its own. Default false.
|
||||
/// Overridable per instance via <c>InstanceNativeAlarmSourceOverride.IsMesReportableOverride</c>.
|
||||
/// </summary>
|
||||
public bool IsMesReportable { get; set; }
|
||||
|
||||
@@ -3,6 +3,17 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
|
||||
/// <summary>An alarm's state change, as streamed site → central and kept in the Debug View snapshot.</summary>
|
||||
/// <param name="InstanceUniqueName">The instance the alarm belongs to.</param>
|
||||
/// <param name="AlarmName">The alarm's name (a native condition's source reference).</param>
|
||||
/// <param name="State">Active or Normal.</param>
|
||||
/// <param name="Priority">
|
||||
/// The source's raw number (Gitea #129): the computed alarm's <c>PriorityLevel</c>, the AVEVA priority of an
|
||||
/// MxAccess condition, the Part 9 Severity of an OPC UA condition — i.e. <c>Condition.Priority</c>. This is what
|
||||
/// the stream's <c>AlarmStateUpdate.priority</c> carries (meaning unchanged since before #129). For ranking use
|
||||
/// <see cref="Condition"/>.<see cref="AlarmConditionState.Severity"/>, which is higher = more urgent for every kind.
|
||||
/// </param>
|
||||
/// <param name="Timestamp">When the transition happened.</param>
|
||||
public record AlarmStateChanged(
|
||||
string InstanceUniqueName,
|
||||
string AlarmName,
|
||||
@@ -119,7 +130,7 @@ public record AlarmStateChanged(
|
||||
/// snapshot and on every event it relays to the site stream. For a native condition it is
|
||||
/// the binding-level flag (<c>ResolvedNativeAlarmSource.IsMesReportable</c>, Gitea #59), set by
|
||||
/// the owning <c>NativeAlarmActor</c> — there is still no per-condition config row; the
|
||||
/// 900-999 severity band remains an independent, additive signal. Carried to central on
|
||||
/// 900-999 priority band (on <see cref="Priority"/>, Gitea #129) remains an independent, additive signal. Carried to central on
|
||||
/// <c>AlarmStateUpdate</c> field 25 and <c>DebugAlarmStateDto</c> field 20 (Gitea #60);
|
||||
/// a pre-#60 site sends the proto3 default. Additive; default false.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,17 +8,35 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
/// onto them, so they are modeled as independent flags plus a 0–1000 severity.
|
||||
/// Computed alarms populate this from State + Priority; native alarms mirror it
|
||||
/// from the source.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>One direction (Gitea #129):</b> <see cref="Severity"/> is always OPC UA Part 9 direction,
|
||||
/// <b>higher = more urgent</b>, whatever the source. OPC UA rows carry the server's Part 9 Severity;
|
||||
/// MxAccess Gateway rows and computed alarms carry their AVEVA-style priority (1 = most urgent)
|
||||
/// mapped through <see cref="AlarmPriorityBands"/> (1–250 → 1000, 251–500 → 750, 501–750 → 500,
|
||||
/// 751–999 → 250). Sort, filter and threshold on <see cref="Severity"/>, never on <see cref="Priority"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="Active">Active vs Inactive.</param>
|
||||
/// <param name="Acknowledged">Acked vs Unacked.</param>
|
||||
/// <param name="Confirmed">Confirmed vs Unconfirmed; null when the condition is not confirmable.</param>
|
||||
/// <param name="Shelve">Shelving sub-state.</param>
|
||||
/// <param name="Suppressed">Whether the source has suppressed the condition.</param>
|
||||
/// <param name="Severity">Severity on the unified 0–1000 scale.</param>
|
||||
/// <param name="Severity">Severity on the unified 0–1000 scale, higher = more urgent for every source.</param>
|
||||
/// <param name="Priority">
|
||||
/// The source's own number, verbatim — exactly what <see cref="Severity"/> carried before Gitea #129:
|
||||
/// the AVEVA alarm priority for MxAccess Gateway rows (clamped 0–1000; 1 = most urgent), the configured
|
||||
/// <c>PriorityLevel</c> for computed alarms, and the Part 9 Severity again for OPC UA rows (OPC UA has
|
||||
/// no separate priority). Kept so no information is lost and so the native MES 900–999 band selects the
|
||||
/// same alarms it always did. Its direction depends on the source, so do not sort or threshold on it.
|
||||
/// <c>null</c> only on a condition persisted before #129 or built without one; readers fall back to
|
||||
/// <see cref="Severity"/>.
|
||||
/// </param>
|
||||
public record AlarmConditionState(
|
||||
bool Active,
|
||||
bool Acknowledged,
|
||||
bool? Confirmed,
|
||||
AlarmShelveState Shelve,
|
||||
bool Suppressed,
|
||||
int Severity);
|
||||
int Severity,
|
||||
int? Priority = null);
|
||||
|
||||
@@ -7,13 +7,32 @@ public static class AlarmConditionStateFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Computed alarms have no native ack/shelve/suppress lifecycle: they are
|
||||
/// auto-acked, never shelved or suppressed, not confirmable, and their
|
||||
/// severity is the configured priority. Active mirrors the alarm State.
|
||||
/// auto-acked, never shelved or suppressed, not confirmable. Their configured
|
||||
/// priority (lower = more urgent) is banded onto Severity through
|
||||
/// <see cref="AlarmPriorityBands"/> and kept verbatim on
|
||||
/// <see cref="AlarmConditionState.Priority"/> (Gitea #129). Active mirrors the alarm State.
|
||||
/// </summary>
|
||||
/// <param name="state">The current alarm state used to derive the Active flag.</param>
|
||||
/// <param name="priority">Configured priority mapped to the Severity field (0–1000).</param>
|
||||
/// <param name="priority">Configured priority (0–1000, lower = more urgent).</param>
|
||||
/// <returns>An <see cref="AlarmConditionState"/> reflecting the computed alarm's lifecycle (auto-acked, unshelved, not suppressed).</returns>
|
||||
public static AlarmConditionState ForComputed(AlarmState state, int priority) =>
|
||||
new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority);
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false,
|
||||
Severity: AlarmPriorityBands.ToSeverity(priority), Priority: priority);
|
||||
|
||||
/// <summary>
|
||||
/// Derives the unified Severity from the raw priority an <c>AlarmStateChanged</c> carries on the
|
||||
/// site stream (<c>AlarmStateUpdate.priority</c>), which is all central receives (Gitea #129).
|
||||
/// OPC UA rows carry their Part 9 Severity there already; MxAccess Gateway and computed rows carry
|
||||
/// an AVEVA-style priority that is banded. Because the wire field kept its pre-#129 meaning, this
|
||||
/// is also correct for a site still running an older build.
|
||||
/// </summary>
|
||||
/// <param name="kind">The alarm's kind.</param>
|
||||
/// <param name="priority">The raw priority from the wire.</param>
|
||||
/// <returns>The severity on the 0–1000 scale, higher = more urgent.</returns>
|
||||
public static int SeverityFromPriority(AlarmKind kind, int priority) => kind switch
|
||||
{
|
||||
AlarmKind.NativeOpcUa => Math.Clamp(priority, 0, 1000),
|
||||
_ => AlarmPriorityBands.ToSeverity(priority),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// Maps an alarm <b>priority</b> (AVEVA convention: 1 = most urgent … 999 = least) onto the unified
|
||||
/// <see cref="AlarmConditionState.Severity"/> scale (OPC UA Part 9 direction: higher = more urgent).
|
||||
/// Used for MxAccess Gateway native alarms and for computed alarms' <c>PriorityLevel</c> (Gitea #129).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the same table OtOpcUa's <c>GalaxyAlarmPriority.Map</c> uses since lmxopcua#887 (commit
|
||||
/// <c>b61f3e05</c>), value for value, so a Galaxy alarm reads the same Severity whether ScadaBridge
|
||||
/// gets it through OtOpcUa (OPC UA adapter) or straight from mxaccessgw (MxGateway adapter). AVEVA's
|
||||
/// default priority ranges, most urgent first:
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <listheader><term>Priority</term><description>AVEVA severity → Severity</description></listheader>
|
||||
/// <item><term>1–250</term><description>1 Critical → 1000</description></item>
|
||||
/// <item><term>251–500</term><description>2 High → 750</description></item>
|
||||
/// <item><term>501–750</term><description>3 Medium → 500</description></item>
|
||||
/// <item><term>751–999</term><description>4 Low → 250</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// A priority outside 1–999 is clamped into it first, exactly as OtOpcUa does: 0, negatives and
|
||||
/// <see cref="int.MinValue"/> read Critical (1000); anything above 999 reads Low (250). The mapping is
|
||||
/// monotonic: a lower (more urgent) priority never yields a lower Severity.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// OtOpcUa's table also has a history-only "not an alarm" row (AVEVA <i>severity</i> 0 → Severity 1, for
|
||||
/// System.*/User.* events in A&E history). It has no counterpart here: the MxGateway live feed carries
|
||||
/// alarms only, and only their priority, and a computed alarm is always an alarm.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The raw priority is never thrown away: it stays on <see cref="AlarmConditionState.Priority"/> and
|
||||
/// <c>AlarmStateChanged.Priority</c>, which is what the native MES 900–999 band keys off.
|
||||
/// Change the table here only, and keep it in step with OtOpcUa's.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class AlarmPriorityBands
|
||||
{
|
||||
/// <summary>The last priority of AVEVA's default Critical range (1–250).</summary>
|
||||
public const int CriticalMaxPriority = 250;
|
||||
|
||||
/// <summary>The last priority of AVEVA's default High range (251–500).</summary>
|
||||
public const int HighMaxPriority = 500;
|
||||
|
||||
/// <summary>The last priority of AVEVA's default Medium range (501–750); 751–999 is Low.</summary>
|
||||
public const int MediumMaxPriority = 750;
|
||||
|
||||
/// <summary>Severity of the Low band (priority 751–999).</summary>
|
||||
public const int SeverityLow = 250;
|
||||
|
||||
/// <summary>Severity of the Medium band (priority 501–750).</summary>
|
||||
public const int SeverityMedium = 500;
|
||||
|
||||
/// <summary>Severity of the High band (priority 251–500).</summary>
|
||||
public const int SeverityHigh = 750;
|
||||
|
||||
/// <summary>Severity of the Critical band (priority 1–250).</summary>
|
||||
public const int SeverityCritical = 1000;
|
||||
|
||||
/// <summary>Maps an AVEVA-style priority onto the unified severity.</summary>
|
||||
/// <param name="priority">The priority (1 = most urgent … 999 = least; clamped into 1–999).</param>
|
||||
/// <returns>The severity on the 0–1000 scale, 1000 = most urgent.</returns>
|
||||
public static int ToSeverity(int priority) => priority switch
|
||||
{
|
||||
// The first arm also takes 0 and negatives, the last anything above 999: the clamp into 1–999.
|
||||
<= CriticalMaxPriority => SeverityCritical,
|
||||
<= HighMaxPriority => SeverityHigh,
|
||||
<= MediumMaxPriority => SeverityMedium,
|
||||
_ => SeverityLow,
|
||||
};
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public sealed record ResolvedNativeAlarmSource
|
||||
public string Source { get; init; } = "Template";
|
||||
/// <summary>
|
||||
/// Gets whether every condition under this binding is MES-reportable (effective value after
|
||||
/// the instance override). Additive to the 900-999 severity band.
|
||||
/// the instance override). Additive to the 900-999 priority band (Gitea #129).
|
||||
/// </summary>
|
||||
public bool IsMesReportable { get; init; }
|
||||
/// <summary>
|
||||
|
||||
@@ -35,7 +35,11 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
/// <param name="Confirmed">Confirmed vs unconfirmed; <c>null</c> when the condition is not confirmable.</param>
|
||||
/// <param name="Shelved">Whether the source has shelved the condition (any shelve sub-state).</param>
|
||||
/// <param name="Suppressed">Whether the source has suppressed the condition.</param>
|
||||
/// <param name="Severity">Severity on the unified 0–1000 scale.</param>
|
||||
/// <param name="Severity">
|
||||
/// Severity on the unified 0–1000 scale, <b>higher = more urgent</b> for every kind (Gitea #129):
|
||||
/// OPC UA Part 9 Severity verbatim; MxAccess and computed priorities banded 1–250 → 1000,
|
||||
/// 251–500 → 750, 501–750 → 500, 751–999 → 250. Sort and threshold on this.
|
||||
/// </param>
|
||||
/// <param name="Kind">"Computed", "NativeOpcUa" or "NativeMxAccess".</param>
|
||||
/// <param name="Message">Event/per-band operator message; may be empty.</param>
|
||||
/// <param name="AlarmTypeName">Native alarm type (e.g. "AnalogLimitAlarm.HiHi"); empty for computed alarms.</param>
|
||||
@@ -59,6 +63,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
/// True when the deployed configuration flags the alarm as MES-reportable (template
|
||||
/// checkbox or instance override). For native conditions it is the source binding's flag (Gitea #59).
|
||||
/// </param>
|
||||
/// <param name="Priority">
|
||||
/// The source's raw number, verbatim — what <paramref name="Severity"/> held before Gitea #129: the
|
||||
/// AVEVA alarm priority for MxAccess (1 = most urgent), the configured <c>PriorityLevel</c> for computed
|
||||
/// alarms, the Part 9 Severity for OPC UA (no separate priority). The native MES 900–999 band and the MES
|
||||
/// API's <c>Severity</c>/<c>MinSeverity</c>/<c>MaxSeverity</c> (a 0–999 priority) key off this.
|
||||
/// Its direction differs by kind; do not rank alarms by it.
|
||||
/// </param>
|
||||
public sealed record ScriptAlarm(
|
||||
string Name,
|
||||
string SourceReference,
|
||||
@@ -81,4 +92,5 @@ public sealed record ScriptAlarm(
|
||||
string CurrentValue,
|
||||
string LimitValue,
|
||||
bool IsConfiguredPlaceholder,
|
||||
bool IsMesReportable = false);
|
||||
bool IsMesReportable = false,
|
||||
int Priority = 0);
|
||||
|
||||
@@ -1994,7 +1994,11 @@ public static class SiteCommandDtoMapper
|
||||
AckTime = DtoOrNull(dto.AckTime)
|
||||
};
|
||||
|
||||
return dto.Condition is null ? alarm : alarm with { Condition = FromProto(dto.Condition) };
|
||||
// AlarmConditionStateDto carries Severity only; the raw priority (Gitea #129) travels as the
|
||||
// row's own Priority, which the site always sets equal to Condition.Priority, so restore it here.
|
||||
return dto.Condition is null
|
||||
? alarm
|
||||
: alarm with { Condition = FromProto(dto.Condition) with { Priority = dto.Priority } };
|
||||
}
|
||||
|
||||
/// <summary>Projects an <see cref="AlarmConditionState"/> onto the wire.</summary>
|
||||
|
||||
@@ -575,43 +575,56 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
|
||||
// proto3 defaults, which map to null / false.
|
||||
SourceExecutionId: Guid.TryParse(evt.AttributeChanged.SourceExecutionId, out var traceSource) ? traceSource : null,
|
||||
Trace: evt.AttributeChanged.Trace),
|
||||
SiteStreamEvent.EventOneofCase.AlarmChanged => new AlarmStateChanged(
|
||||
evt.AlarmChanged.InstanceUniqueName,
|
||||
evt.AlarmChanged.AlarmName,
|
||||
MapAlarmState(evt.AlarmChanged.State),
|
||||
evt.AlarmChanged.Priority,
|
||||
evt.AlarmChanged.Timestamp.ToDateTimeOffset())
|
||||
{
|
||||
Level = MapAlarmLevel(evt.AlarmChanged.Level),
|
||||
Message = evt.AlarmChanged.Message ?? string.Empty,
|
||||
// Native alarm enrichment (additive — computed alarms carry their default condition).
|
||||
Kind = ParseAlarmKind(evt.AlarmChanged.Kind),
|
||||
Condition = new AlarmConditionState(
|
||||
Active: evt.AlarmChanged.Active,
|
||||
Acknowledged: evt.AlarmChanged.Acknowledged,
|
||||
Confirmed: evt.AlarmChanged.Confirmed,
|
||||
Shelve: AlarmShelveStateCodec.Parse(evt.AlarmChanged.ShelveState),
|
||||
Suppressed: evt.AlarmChanged.Suppressed,
|
||||
Severity: evt.AlarmChanged.Priority),
|
||||
SourceReference = evt.AlarmChanged.SourceReference ?? string.Empty,
|
||||
AlarmTypeName = evt.AlarmChanged.AlarmTypeName ?? string.Empty,
|
||||
Category = evt.AlarmChanged.Category ?? string.Empty,
|
||||
OperatorUser = evt.AlarmChanged.OperatorUser ?? string.Empty,
|
||||
OperatorComment = evt.AlarmChanged.OperatorComment ?? string.Empty,
|
||||
OriginalRaiseTime = evt.AlarmChanged.OriginalRaiseTime?.ToDateTimeOffset(),
|
||||
CurrentValue = evt.AlarmChanged.CurrentValue ?? string.Empty,
|
||||
LimitValue = evt.AlarmChanged.LimitValue ?? string.Empty,
|
||||
NativeSourceCanonicalName = evt.AlarmChanged.NativeSourceCanonicalName ?? string.Empty,
|
||||
IsConfiguredPlaceholder = evt.AlarmChanged.IsConfiguredPlaceholder,
|
||||
// MES alarm-status API §6.4: ack instant; an absent proto Timestamp (unacked
|
||||
// condition, computed alarm, or a pre-AckTime site) stays null.
|
||||
AckTime = evt.AlarmChanged.AckTime?.ToDateTimeOffset(),
|
||||
// Gitea #60: a pre-#60 site leaves the field at its proto3 default (false).
|
||||
IsMesReportable = evt.AlarmChanged.IsMesReportable
|
||||
},
|
||||
SiteStreamEvent.EventOneofCase.AlarmChanged => ConvertAlarmChanged(evt.AlarmChanged),
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps one <see cref="AlarmStateUpdate"/> to the domain record. The wire carries only the raw
|
||||
/// <c>priority</c> (its pre-#129 meaning, unchanged), so the unified, higher-is-more-urgent
|
||||
/// Severity is derived here from the kind (Gitea #129): banded for MxAccess and computed rows,
|
||||
/// verbatim for OPC UA. The raw value stays on <see cref="AlarmConditionState.Priority"/>.
|
||||
/// </summary>
|
||||
private static AlarmStateChanged ConvertAlarmChanged(AlarmStateUpdate update)
|
||||
{
|
||||
var kind = ParseAlarmKind(update.Kind);
|
||||
return new AlarmStateChanged(
|
||||
update.InstanceUniqueName,
|
||||
update.AlarmName,
|
||||
MapAlarmState(update.State),
|
||||
update.Priority,
|
||||
update.Timestamp.ToDateTimeOffset())
|
||||
{
|
||||
Level = MapAlarmLevel(update.Level),
|
||||
Message = update.Message ?? string.Empty,
|
||||
// Native alarm enrichment (additive — computed alarms carry their default condition).
|
||||
Kind = kind,
|
||||
Condition = new AlarmConditionState(
|
||||
Active: update.Active,
|
||||
Acknowledged: update.Acknowledged,
|
||||
Confirmed: update.Confirmed,
|
||||
Shelve: AlarmShelveStateCodec.Parse(update.ShelveState),
|
||||
Suppressed: update.Suppressed,
|
||||
Severity: AlarmConditionStateFactory.SeverityFromPriority(kind, update.Priority),
|
||||
Priority: update.Priority),
|
||||
SourceReference = update.SourceReference ?? string.Empty,
|
||||
AlarmTypeName = update.AlarmTypeName ?? string.Empty,
|
||||
Category = update.Category ?? string.Empty,
|
||||
OperatorUser = update.OperatorUser ?? string.Empty,
|
||||
OperatorComment = update.OperatorComment ?? string.Empty,
|
||||
OriginalRaiseTime = update.OriginalRaiseTime?.ToDateTimeOffset(),
|
||||
CurrentValue = update.CurrentValue ?? string.Empty,
|
||||
LimitValue = update.LimitValue ?? string.Empty,
|
||||
NativeSourceCanonicalName = update.NativeSourceCanonicalName ?? string.Empty,
|
||||
IsConfiguredPlaceholder = update.IsConfiguredPlaceholder,
|
||||
// MES alarm-status API §6.4: ack instant; an absent proto Timestamp (unacked
|
||||
// condition, computed alarm, or a pre-AckTime site) stays null.
|
||||
AckTime = update.AckTime?.ToDateTimeOffset(),
|
||||
// Gitea #60: a pre-#60 site leaves the field at its proto3 default (false).
|
||||
IsMesReportable = update.IsMesReportable
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a proto <see cref="SiteStreamEvent"/> to a domain <see cref="AlarmStateChanged"/>,
|
||||
/// returning <c>null</c> for any non-alarm event. Used by <see cref="SubscribeSiteAsync"/>
|
||||
|
||||
@@ -7,6 +7,7 @@ using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionK
|
||||
// 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 AlarmPriorityBands = ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms.AlarmPriorityBands;
|
||||
using AlarmTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
|
||||
using AlarmShelveState = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmShelveState;
|
||||
|
||||
@@ -20,14 +21,35 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
||||
/// </summary>
|
||||
public static class MxGatewayAlarmMapper
|
||||
{
|
||||
/// <summary>Clamps the gateway severity onto the unified 0–1000 scale.</summary>
|
||||
/// <param name="severity">Raw gateway severity value to clamp.</param>
|
||||
/// <returns>The severity clamped to the range [0, 1000].</returns>
|
||||
public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000);
|
||||
/// <summary>
|
||||
/// The raw priority kept on <see cref="AlarmConditionState.Priority"/>: the gateway's
|
||||
/// <c>severity</c> field — which mxaccessgw fills from the AVEVA alarm <b>priority</b>
|
||||
/// (1 = most urgent … 999 = least) — clamped to 0–1000, i.e. exactly the value that
|
||||
/// <see cref="AlarmConditionState.Severity"/> carried before Gitea #129. The native MES
|
||||
/// 900–999 band keys off this value, so it must keep selecting the same alarms.
|
||||
/// </summary>
|
||||
/// <param name="rawPriority">The gateway's raw <c>severity</c> field (the AVEVA priority).</param>
|
||||
/// <returns>The priority clamped to the range [0, 1000].</returns>
|
||||
public static int NormalizePriority(int rawPriority) => Math.Clamp(rawPriority, 0, 1000);
|
||||
|
||||
/// <summary>
|
||||
/// Maps the gateway's raw <c>severity</c> field (the AVEVA priority, lower = more urgent) onto the
|
||||
/// unified <see cref="AlarmConditionState.Severity"/> (higher = more urgent) through
|
||||
/// <see cref="AlarmPriorityBands"/> — the same table OtOpcUa applies to the same alarms
|
||||
/// (lmxopcua#887), so the MxGateway and OPC UA paths agree (Gitea #129).
|
||||
/// </summary>
|
||||
/// <param name="rawPriority">The gateway's raw <c>severity</c> field (the AVEVA priority).</param>
|
||||
/// <returns>The banded severity: 1000, 750, 500 or 250.</returns>
|
||||
public static int MapSeverity(int rawPriority) => AlarmPriorityBands.ToSeverity(rawPriority);
|
||||
|
||||
/// <summary>Builds the condition for one gateway alarm: banded Severity + verbatim Priority.</summary>
|
||||
private static AlarmConditionState BuildCondition(bool active, bool acked, int rawPriority) =>
|
||||
new(active, acked, Confirmed: null, Shelve: AlarmShelveState.Unshelved, Suppressed: false,
|
||||
Severity: MapSeverity(rawPriority), Priority: NormalizePriority(rawPriority));
|
||||
|
||||
/// <summary>Maps a gateway condition-state + severity to the unified condition.</summary>
|
||||
/// <param name="state">Gateway proto condition state to map.</param>
|
||||
/// <param name="severity">Raw gateway severity value; normalized to 0–1000.</param>
|
||||
/// <param name="severity">The gateway's raw <c>severity</c> field (the AVEVA priority); banded onto Severity, kept on Priority.</param>
|
||||
/// <returns>The protocol-neutral <see cref="AlarmConditionState"/> equivalent.</returns>
|
||||
public static AlarmConditionState MapConditionState(ProtoConditionState state, int severity)
|
||||
{
|
||||
@@ -38,8 +60,7 @@ public static class MxGatewayAlarmMapper
|
||||
ProtoConditionState.Inactive => (false, true),
|
||||
_ => (false, true)
|
||||
};
|
||||
return new AlarmConditionState(active, acked, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
|
||||
return BuildCondition(active, acked, severity);
|
||||
}
|
||||
|
||||
/// <summary>Maps a gateway transition kind to the unified transition kind.</summary>
|
||||
@@ -65,8 +86,7 @@ public static class MxGatewayAlarmMapper
|
||||
ProtoTransitionKind.Clear => (false, true),
|
||||
_ => (false, true)
|
||||
};
|
||||
return new AlarmConditionState(active, acked, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
|
||||
return BuildCondition(active, acked, severity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -63,11 +63,18 @@ public static class OpcUaAlarmMapper
|
||||
/// <param name="shelve">The current shelving state of the alarm.</param>
|
||||
/// <param name="suppressed">Whether the alarm is suppressed.</param>
|
||||
/// <param name="severity">The raw OPC UA severity (1–1000); clamped via <see cref="NormalizeSeverity"/>.</param>
|
||||
/// <returns>The composed <see cref="AlarmConditionState"/>.</returns>
|
||||
/// <returns>
|
||||
/// The composed <see cref="AlarmConditionState"/>. OPC UA Part 9 Severity is already the unified
|
||||
/// direction (higher = more urgent), so it is used as-is; OPC UA has no separate priority, so
|
||||
/// <see cref="AlarmConditionState.Priority"/> repeats it (Gitea #129).
|
||||
/// </returns>
|
||||
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));
|
||||
bool active, bool acked, bool? confirmed, AlarmShelveState shelve, bool suppressed, int severity)
|
||||
{
|
||||
var normalized = NormalizeSeverity(severity);
|
||||
return new(Active: active, Acknowledged: acked, Confirmed: confirmed,
|
||||
Shelve: shelve, Suppressed: suppressed, Severity: normalized, Priority: normalized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives the transition kind from the change in active/acked sub-states.
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
@@ -92,11 +93,15 @@ public class AlarmActor : ReceiveActor
|
||||
private readonly ISiteEventLogger? _siteEventLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Priority at or above which a computed-alarm raise is logged as
|
||||
/// Severity at or above which a computed-alarm raise is logged as
|
||||
/// <c>Error</c> to the site event log; below it, raises log as <c>Warning</c>.
|
||||
/// Mirrors the 0–1000 alarm-severity scale.
|
||||
/// Compared against the alarm's priority banded onto the unified severity
|
||||
/// (<see cref="AlarmPriorityBands"/>, higher = more urgent; Gitea #129) — the same
|
||||
/// threshold <c>NativeAlarmActor</c> applies — so priority 1–500 (Critical/High)
|
||||
/// logs as Error and 501+ (Medium/Low) as Warning. Before #129 the raw priority was
|
||||
/// compared, which ranked the least urgent alarms (700+) as errors.
|
||||
/// </summary>
|
||||
private const int ErrorPriorityThreshold = 700;
|
||||
private const int ErrorSeverityThreshold = 700;
|
||||
|
||||
private AlarmState _currentState = AlarmState.Normal;
|
||||
/// <summary>
|
||||
@@ -471,13 +476,13 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an alarm priority (0–1000) to a site-event severity for a
|
||||
/// <i>raise</i> transition — <c>Error</c> at or above
|
||||
/// <see cref="ErrorPriorityThreshold"/>, otherwise <c>Warning</c>. Clears and
|
||||
/// Maps an alarm priority (0–1000, lower = more urgent) to a site-event severity
|
||||
/// for a <i>raise</i> transition — <c>Error</c> when its banded severity is at or
|
||||
/// above <see cref="ErrorSeverityThreshold"/>, otherwise <c>Warning</c>. Clears and
|
||||
/// inter-band transitions always log as <c>Info</c>.
|
||||
/// </summary>
|
||||
private static string RaiseSeverity(int priority) =>
|
||||
priority >= ErrorPriorityThreshold ? "Error" : "Warning";
|
||||
internal static string RaiseSeverity(int priority) =>
|
||||
AlarmPriorityBands.ToSeverity(priority) >= ErrorSeverityThreshold ? "Error" : "Warning";
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget an <c>alarm</c> operational event to the optional
|
||||
|
||||
@@ -43,7 +43,9 @@ public class NativeAlarmActor : ReceiveActor
|
||||
/// <summary>
|
||||
/// Severity at or above which a native-alarm raise is logged as
|
||||
/// <c>Error</c> to the site event log; below it, raises log as <c>Warning</c>.
|
||||
/// Mirrors the 0–1000 condition-severity scale.
|
||||
/// On the unified 0–1000 condition-severity scale, higher = more urgent for every
|
||||
/// source (Gitea #129): an MxAccess alarm in AVEVA's Critical (priority 1–250 → 1000)
|
||||
/// or High (251–500 → 750) band logs as Error, Medium/Low as Warning.
|
||||
/// </summary>
|
||||
private const int ErrorSeverityThreshold = 700;
|
||||
|
||||
@@ -217,6 +219,8 @@ public class NativeAlarmActor : ReceiveActor
|
||||
continue;
|
||||
}
|
||||
|
||||
condition = UpgradeLegacyCondition(condition, _nativeKind);
|
||||
|
||||
// Restore the persisted display metadata (UA4) so the rehydrated condition renders
|
||||
// fully (type/category/message/values) until the first source snapshot replaces it.
|
||||
// Null/malformed metadata falls back to empty strings (pre-UA4 rows behave as before).
|
||||
@@ -481,7 +485,10 @@ public class NativeAlarmActor : ReceiveActor
|
||||
_instanceName,
|
||||
t.SourceReference,
|
||||
condition.Active ? AlarmState.Active : AlarmState.Normal,
|
||||
condition.Severity,
|
||||
// Gitea #129: the record's Priority (and the stream's AlarmStateUpdate.priority) is the
|
||||
// source's raw number — what Severity carried before #129 — so the MES 900-999 band and
|
||||
// an older central keep reading the same value. Severity rides on Condition.
|
||||
condition.Priority ?? condition.Severity,
|
||||
t.TransitionTime)
|
||||
{
|
||||
Kind = _nativeKind,
|
||||
@@ -497,7 +504,7 @@ public class NativeAlarmActor : ReceiveActor
|
||||
LimitValue = t.LimitValue,
|
||||
NativeSourceCanonicalName = _source.CanonicalName,
|
||||
// Gitea #59: the binding-level flag ("every condition under this source is
|
||||
// MES-reportable"). Additive to the 900-999 severity band the consumer applies.
|
||||
// MES-reportable"). Additive to the 900-999 priority band the consumer applies.
|
||||
IsMesReportable = _source.IsMesReportable,
|
||||
// MES alarm-status API §6.4: carried verbatim from the transition — the DCL
|
||||
// adapter already decided whether an ack instant applies (null while unacked,
|
||||
@@ -515,6 +522,32 @@ public class NativeAlarmActor : ReceiveActor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades a condition persisted in <c>native_alarm_state</c> by a pre-#129 build, whose
|
||||
/// <see cref="AlarmConditionState.Severity"/> held the raw source number and which has no
|
||||
/// <see cref="AlarmConditionState.Priority"/>. That number becomes the Priority; for an
|
||||
/// MxAccess source it is an AVEVA priority, so Severity is re-derived through
|
||||
/// <see cref="AlarmPriorityBands"/>. An OPC UA row's Severity was already Part 9 and is kept.
|
||||
/// A current row (Priority present) is returned unchanged.
|
||||
/// </summary>
|
||||
/// <param name="condition">The deserialized persisted condition.</param>
|
||||
/// <param name="kind">This actor's native alarm kind.</param>
|
||||
/// <returns>The condition in the post-#129 shape.</returns>
|
||||
internal static AlarmConditionState UpgradeLegacyCondition(AlarmConditionState condition, AlarmKind kind)
|
||||
{
|
||||
if (condition.Priority is not null)
|
||||
{
|
||||
return condition;
|
||||
}
|
||||
|
||||
var raw = condition.Severity;
|
||||
return condition with
|
||||
{
|
||||
Severity = AlarmConditionStateFactory.SeverityFromPriority(kind, raw),
|
||||
Priority = raw,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget an <c>alarm</c> operational event mirroring a native
|
||||
/// condition transition. An active condition is a raise (severity by the
|
||||
|
||||
@@ -1393,7 +1393,9 @@ public class ScriptRuntimeContext
|
||||
CurrentValue: a.CurrentValue,
|
||||
LimitValue: a.LimitValue,
|
||||
IsConfiguredPlaceholder: a.IsConfiguredPlaceholder,
|
||||
IsMesReportable: a.IsMesReportable);
|
||||
IsMesReportable: a.IsMesReportable,
|
||||
// Gitea #129: the raw source number (pre-#129 Severity) — the MES 900-999 band keys off it.
|
||||
Priority: a.Priority);
|
||||
|
||||
/// <summary>
|
||||
/// Site-local tracking-status API for cached operations.
|
||||
|
||||
Reference in New Issue
Block a user