worker(alarms): SubtagAlarmConsumer synthesizing degraded transitions; dispatcher propagates Degraded
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="SubtagAlarmConsumer"/>: prove that the subtag
|
||||
/// fallback advises the observable subtags, synthesizes degraded
|
||||
/// transitions with stable synthetic GUIDs, routes acknowledgments to the
|
||||
/// ack-comment subtag, and stamps snapshots. A <see cref="FakeSource"/>
|
||||
/// stands in for the live MXAccess subtag source so this needs no AVEVA
|
||||
/// install.
|
||||
/// </summary>
|
||||
public sealed class SubtagAlarmConsumerTests
|
||||
{
|
||||
private const string Reference = "Galaxy!TestArea.Tank01.Level.HiHi";
|
||||
private const string ActiveSubtag = "Tank01.Level.HiHi.InAlarm";
|
||||
private const string AckedSubtag = "Tank01.Level.HiHi.Acked";
|
||||
private const string AckCommentSubtag = "Tank01.Level.HiHi.AckComment";
|
||||
private const string PrioritySubtag = "Tank01.Level.HiHi.Priority";
|
||||
|
||||
private static AlarmSubtagTarget BuildTarget()
|
||||
{
|
||||
return new AlarmSubtagTarget
|
||||
{
|
||||
AlarmFullReference = Reference,
|
||||
SourceObjectReference = "Tank01",
|
||||
ActiveSubtag = ActiveSubtag,
|
||||
AckedSubtag = AckedSubtag,
|
||||
AckCommentSubtag = AckCommentSubtag,
|
||||
PrioritySubtag = PrioritySubtag,
|
||||
};
|
||||
}
|
||||
|
||||
private static SubtagAlarmConsumer BuildConsumer(FakeSource source)
|
||||
{
|
||||
return new SubtagAlarmConsumer(source, new[] { BuildTarget() });
|
||||
}
|
||||
|
||||
/// <summary>Verifies Subscribe advises the active, acked, and priority subtags but not the ack-comment subtag.</summary>
|
||||
[Fact]
|
||||
public void Subscribe_AdvisesActiveAndAckedSubtags()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
Assert.Contains(ActiveSubtag, source.Advised);
|
||||
Assert.Contains(AckedSubtag, source.Advised);
|
||||
Assert.Contains(PrioritySubtag, source.Advised);
|
||||
Assert.DoesNotContain(AckCommentSubtag, source.Advised);
|
||||
}
|
||||
|
||||
/// <summary>Verifies an active=true value change raises a degraded, GUID-stamped UNACK_ALM transition.</summary>
|
||||
[Fact]
|
||||
public void ValueChange_RaisesDegradedSynthesizedTransition()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
MxAlarmTransitionEvent? emitted = null;
|
||||
consumer.AlarmTransitionEmitted += (_, e) => emitted = e;
|
||||
|
||||
source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
Assert.NotNull(emitted);
|
||||
Assert.Equal(MxAlarmStateKind.UnackAlm, emitted!.Record.State);
|
||||
Assert.True(emitted.Record.Degraded);
|
||||
Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeByName writes the comment to the ack-comment subtag and returns success.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_WritesCommentToAckCommentSubtag()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
int rc = consumer.AcknowledgeByName(
|
||||
alarmName: "Tank01.Level.HiHi",
|
||||
providerName: "Galaxy",
|
||||
groupName: "TestArea",
|
||||
ackComment: "operator ack",
|
||||
ackOperatorName: "alice",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Alice Smith");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.NotNull(source.LastWrite);
|
||||
Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
|
||||
Assert.Equal("operator ack", source.LastWrite!.Value.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeByName returns non-zero when no target matches the supplied name.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_UnknownAlarm_ReturnsNonZero()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
int rc = consumer.AcknowledgeByName(
|
||||
alarmName: "DoesNotExist.NoSuchAlarm",
|
||||
providerName: "Galaxy",
|
||||
groupName: "TestArea",
|
||||
ackComment: "operator ack",
|
||||
ackOperatorName: "alice",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Alice Smith");
|
||||
|
||||
Assert.NotEqual(0, rc);
|
||||
Assert.Null(source.LastWrite);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a snapshot of an active alarm stamps Degraded and a non-empty synthetic GUID.</summary>
|
||||
[Fact]
|
||||
public void SnapshotActiveAlarms_StampsDegradedAndGuid()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
IReadOnlyList<MxAlarmSnapshotRecord> snapshot = consumer.SnapshotActiveAlarms();
|
||||
|
||||
Assert.Single(snapshot);
|
||||
Assert.True(snapshot[0].Degraded);
|
||||
Assert.NotEqual(Guid.Empty, snapshot[0].AlarmGuid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the synthetic GUID on the emitted transition equals the GUID in the snapshot for the same alarm.</summary>
|
||||
[Fact]
|
||||
public void SameReference_SyntheticGuidStableAcrossTransitionAndSnapshot()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
MxAlarmTransitionEvent? emitted = null;
|
||||
consumer.AlarmTransitionEmitted += (_, e) => emitted = e;
|
||||
|
||||
source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
IReadOnlyList<MxAlarmSnapshotRecord> snapshot = consumer.SnapshotActiveAlarms();
|
||||
|
||||
Assert.NotNull(emitted);
|
||||
Assert.Single(snapshot);
|
||||
Assert.Equal(emitted!.Record.AlarmGuid, snapshot[0].AlarmGuid);
|
||||
Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
|
||||
}
|
||||
|
||||
private sealed class FakeSource : ISubtagAlarmSource
|
||||
{
|
||||
/// <summary>Raised when an advised subtag reports a new value.</summary>
|
||||
public event EventHandler<SubtagValueChange>? ValueChanged;
|
||||
|
||||
/// <summary>Gets the subtag addresses passed to <see cref="Advise"/>.</summary>
|
||||
public List<string> Advised { get; } = new List<string>();
|
||||
|
||||
/// <summary>Gets the most recent (address, value) pair passed to <see cref="Write"/>.</summary>
|
||||
public (string Address, object? Value)? LastWrite { get; private set; }
|
||||
|
||||
/// <summary>Records the advised subtag addresses.</summary>
|
||||
/// <param name="itemAddresses">The subtag references to advise.</param>
|
||||
public void Advise(IReadOnlyCollection<string> itemAddresses)
|
||||
{
|
||||
Advised.AddRange(itemAddresses);
|
||||
}
|
||||
|
||||
/// <summary>Records the most recent write.</summary>
|
||||
/// <param name="itemAddress">The subtag reference to write.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void Write(string itemAddress, object? value)
|
||||
{
|
||||
LastWrite = (itemAddress, value);
|
||||
}
|
||||
|
||||
/// <summary>Raises a <see cref="SubtagValueChange"/> for the given subtag.</summary>
|
||||
/// <param name="address">The subtag address whose value changed.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <param name="timestampUtc">The UTC timestamp of the change.</param>
|
||||
public void Raise(string address, object? value, DateTime timestampUtc)
|
||||
{
|
||||
ValueChanged?.Invoke(this, new SubtagValueChange
|
||||
{
|
||||
ItemAddress = address,
|
||||
Value = value,
|
||||
TimestampUtc = timestampUtc,
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,8 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
operatorUser: record.OperatorName,
|
||||
operatorComment: record.AlarmComment,
|
||||
category: record.Group,
|
||||
description: string.Empty);
|
||||
description: string.Empty,
|
||||
degraded: record.Degraded);
|
||||
}
|
||||
|
||||
private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record)
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Subtag-fallback implementation of <see cref="IMxAccessAlarmConsumer"/>.
|
||||
/// Where <see cref="WnWrapAlarmConsumer"/> polls the native alarmmgr
|
||||
/// (wnwrap) COM stream, this consumer advises a configured set of MXAccess
|
||||
/// alarm subtags via an <see cref="ISubtagAlarmSource"/> and synthesizes
|
||||
/// alarm transitions through a <see cref="SubtagAlarmStateMachine"/>. Every
|
||||
/// emitted record is flagged <see cref="MxAlarmSnapshotRecord.Degraded"/>
|
||||
/// and assigned a deterministic <see cref="SyntheticAlarmGuid"/> derived
|
||||
/// from the alarm's full reference, since the subtag path has no
|
||||
/// alarmmgr-supplied GUID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Threading: like <see cref="WnWrapAlarmConsumer"/>, this consumer is
|
||||
/// driven on the worker's STA thread. <see cref="Subscribe"/> binds the
|
||||
/// source's <c>ValueChanged</c> event; the source raises that event on
|
||||
/// the same STA in production, so <see cref="AlarmTransitionEmitted"/>
|
||||
/// fires on the STA and subscribers must marshal off it themselves if
|
||||
/// they need another thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The subtag path is event-driven, so <see cref="PollOnce"/> is a
|
||||
/// no-op — value changes arrive through the source's advise stream
|
||||
/// rather than an explicit poll. Acknowledgment in subtag mode writes
|
||||
/// the comment to the target's writable ack-comment subtag rather than
|
||||
/// calling a native <c>AlarmAckByGUID</c> / <c>AlarmAckByName</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
|
||||
{
|
||||
private readonly ISubtagAlarmSource source;
|
||||
private readonly SubtagAlarmStateMachine stateMachine;
|
||||
private readonly Dictionary<string, AlarmSubtagTarget> targetsByReference;
|
||||
private readonly Dictionary<Guid, string> referencesBySyntheticGuid;
|
||||
private readonly EventHandler<SubtagValueChange> valueChangedHandler;
|
||||
private bool subscribed;
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>Fires once per synthesized alarm-state transition.</summary>
|
||||
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the consumer over a subtag source and a watch list of
|
||||
/// alarm targets.
|
||||
/// </summary>
|
||||
/// <param name="source">The subtag value source to advise and write through.</param>
|
||||
/// <param name="watchList">The alarm subtag targets to observe.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="source"/> or <paramref name="watchList"/>
|
||||
/// is <see langword="null"/>.
|
||||
/// </exception>
|
||||
public SubtagAlarmConsumer(ISubtagAlarmSource source, IReadOnlyList<AlarmSubtagTarget> watchList)
|
||||
{
|
||||
this.source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
if (watchList is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(watchList));
|
||||
}
|
||||
|
||||
this.stateMachine = new SubtagAlarmStateMachine(watchList);
|
||||
this.targetsByReference = new Dictionary<string, AlarmSubtagTarget>(StringComparer.OrdinalIgnoreCase);
|
||||
this.referencesBySyntheticGuid = new Dictionary<Guid, string>();
|
||||
|
||||
foreach (AlarmSubtagTarget target in watchList)
|
||||
{
|
||||
string reference = target.AlarmFullReference ?? string.Empty;
|
||||
this.targetsByReference[reference] = target;
|
||||
this.referencesBySyntheticGuid[SyntheticAlarmGuid.ForReference(reference)] = reference;
|
||||
}
|
||||
|
||||
this.valueChangedHandler = OnValueChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advises every observable alarm subtag (active / acked / priority —
|
||||
/// the ack-comment subtag is a write-only target and is not advised)
|
||||
/// and begins listening for value changes. The <paramref name="subscription"/>
|
||||
/// expression is ignored: the subtag set is fixed by the watch list.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription expression (unused in subtag mode).</param>
|
||||
public void Subscribe(string subscription)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(SubtagAlarmConsumer));
|
||||
}
|
||||
|
||||
List<string> addresses = new List<string>();
|
||||
foreach (AlarmSubtagTarget target in targetsByReference.Values)
|
||||
{
|
||||
AddIfNotEmpty(addresses, target.ActiveSubtag);
|
||||
AddIfNotEmpty(addresses, target.AckedSubtag);
|
||||
AddIfNotEmpty(addresses, target.PrioritySubtag);
|
||||
}
|
||||
|
||||
source.Advise(addresses);
|
||||
|
||||
if (!subscribed)
|
||||
{
|
||||
source.ValueChanged += valueChangedHandler;
|
||||
subscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges an alarm by its synthetic GUID. Resolves the GUID back
|
||||
/// to its alarm full reference and delegates to the by-name write path.
|
||||
/// </summary>
|
||||
/// <param name="alarmGuid">The synthetic alarm GUID.</param>
|
||||
/// <param name="ackComment">The acknowledgment comment.</param>
|
||||
/// <param name="ackOperatorName">The operator name (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorNode">The operator node (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorDomain">The operator domain (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorFullName">The operator full name (unused in subtag mode).</param>
|
||||
/// <returns>0 on success; non-zero when the GUID or ack-comment subtag is unknown.</returns>
|
||||
public int AcknowledgeByGuid(
|
||||
Guid alarmGuid,
|
||||
string ackComment,
|
||||
string ackOperatorName,
|
||||
string ackOperatorNode,
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(SubtagAlarmConsumer));
|
||||
}
|
||||
|
||||
if (!referencesBySyntheticGuid.TryGetValue(alarmGuid, out string reference) ||
|
||||
!targetsByReference.TryGetValue(reference, out AlarmSubtagTarget target))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return WriteAckComment(target, ackComment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges an alarm by its (name, provider, group) tuple. In subtag
|
||||
/// mode the comment is written to the target's writable ack-comment
|
||||
/// subtag; the operator-identity arguments are not surfaced through the
|
||||
/// subtag write.
|
||||
/// </summary>
|
||||
/// <param name="alarmName">The alarm name (object-rooted tag name or any reference fragment).</param>
|
||||
/// <param name="providerName">The provider name (unused for matching).</param>
|
||||
/// <param name="groupName">The group name (unused for matching).</param>
|
||||
/// <param name="ackComment">The acknowledgment comment.</param>
|
||||
/// <param name="ackOperatorName">The operator name (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorNode">The operator node (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorDomain">The operator domain (unused in subtag mode).</param>
|
||||
/// <param name="ackOperatorFullName">The operator full name (unused in subtag mode).</param>
|
||||
/// <returns>0 on success; non-zero when no target matches or it lacks an ack-comment subtag.</returns>
|
||||
public int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string ackComment,
|
||||
string ackOperatorName,
|
||||
string ackOperatorNode,
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(SubtagAlarmConsumer));
|
||||
}
|
||||
|
||||
AlarmSubtagTarget? target = ResolveTargetByName(alarmName);
|
||||
if (target is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return WriteAckComment(target, ackComment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state machine's currently-active alarm snapshot, with
|
||||
/// each record stamped <see cref="MxAlarmSnapshotRecord.Degraded"/> and
|
||||
/// assigned its synthetic GUID.
|
||||
/// </summary>
|
||||
/// <returns>The active alarm snapshot records.</returns>
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
||||
{
|
||||
IReadOnlyList<MxAlarmSnapshotRecord> records = stateMachine.SnapshotActive();
|
||||
foreach (MxAlarmSnapshotRecord record in records)
|
||||
{
|
||||
StampSynthetic(record);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op: the subtag path is event-driven and owns no poll cadence.
|
||||
/// </summary>
|
||||
public void PollOnce()
|
||||
{
|
||||
// Subtag mode is event-driven; value changes arrive via the source's
|
||||
// advise stream, so there is nothing to poll.
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
|
||||
if (subscribed)
|
||||
{
|
||||
try
|
||||
{
|
||||
source.ValueChanged -= valueChangedHandler;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow — best-effort detach during dispose
|
||||
}
|
||||
|
||||
subscribed = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
source.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow — best-effort source dispose
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValueChanged(object? sender, SubtagValueChange change)
|
||||
{
|
||||
if (disposed || change is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<MxAlarmTransitionEvent> transitions =
|
||||
stateMachine.Apply(change.ItemAddress, change.Value, change.TimestampUtc);
|
||||
if (transitions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EventHandler<MxAlarmTransitionEvent>? handler = AlarmTransitionEmitted;
|
||||
foreach (MxAlarmTransitionEvent transition in transitions)
|
||||
{
|
||||
StampSynthetic(transition.Record);
|
||||
handler?.Invoke(this, transition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stamps the degraded flag and synthetic GUID onto a synthesized
|
||||
/// record. The state machine sets only the provider / group / tag-name
|
||||
/// parts, so the alarm reference is recomposed exactly as
|
||||
/// <see cref="AlarmDispatcher"/> composes it — keying the synthetic GUID
|
||||
/// off that recomposed reference keeps it stable across transitions and
|
||||
/// snapshots for the same alarm.
|
||||
/// </summary>
|
||||
private static void StampSynthetic(MxAlarmSnapshotRecord record)
|
||||
{
|
||||
record.Degraded = true;
|
||||
string reference = AlarmRecordTransitionMapper.ComposeFullReference(
|
||||
record.ProviderName, record.Group, record.TagName);
|
||||
record.AlarmGuid = SyntheticAlarmGuid.ForReference(reference);
|
||||
}
|
||||
|
||||
private AlarmSubtagTarget? ResolveTargetByName(string? alarmName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(alarmName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Match a target whose full reference contains the supplied name. The
|
||||
// dispatcher derives (name, provider, group) from the composed
|
||||
// reference, so the object-rooted tag name is always a substring of the
|
||||
// target's AlarmFullReference.
|
||||
foreach (AlarmSubtagTarget target in targetsByReference.Values)
|
||||
{
|
||||
string reference = target.AlarmFullReference ?? string.Empty;
|
||||
if (reference.IndexOf(alarmName!, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int WriteAckComment(AlarmSubtagTarget target, string ackComment)
|
||||
{
|
||||
string ackCommentSubtag = target.AckCommentSubtag ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(ackCommentSubtag))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
source.Write(ackCommentSubtag, ackComment);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void AddIfNotEmpty(List<string> addresses, string? address)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
{
|
||||
addresses.Add(address!);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user