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:
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user