alarms-over-gateway: full pipeline (wnwrap consumer + dispatcher + IPC + auto-subscribe + ack-by-name + live smoke) #118
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,7 @@ message MxCommand {
|
||||
UnsubscribeAlarmsCommand unsubscribe_alarms = 35;
|
||||
AcknowledgeAlarmCommand acknowledge_alarm_command = 36;
|
||||
QueryActiveAlarmsCommand query_active_alarms_command = 37;
|
||||
AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38;
|
||||
PingCommand ping = 100;
|
||||
GetSessionStateCommand get_session_state = 101;
|
||||
GetWorkerInfoCommand get_worker_info = 102;
|
||||
@@ -130,6 +131,7 @@ enum MxCommandKind {
|
||||
MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26;
|
||||
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27;
|
||||
MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28;
|
||||
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME = 29;
|
||||
MX_COMMAND_KIND_PING = 100;
|
||||
MX_COMMAND_KIND_GET_SESSION_STATE = 101;
|
||||
MX_COMMAND_KIND_GET_WORKER_INFO = 102;
|
||||
@@ -307,6 +309,27 @@ message QueryActiveAlarmsCommand {
|
||||
string alarm_filter_prefix = 1;
|
||||
}
|
||||
|
||||
// Acknowledge a single alarm by its (name, provider, group) tuple. Used
|
||||
// when the public RPC's AlarmFullReference (Provider!Group.Tag) cannot
|
||||
// be resolved to a GUID directly. The worker invokes
|
||||
// wwAlarmConsumerClass.AlarmAckByName which reaches the same alarm
|
||||
// history path as AlarmAckByGUID.
|
||||
message AcknowledgeAlarmByNameCommand {
|
||||
// Tag/alarm name (e.g. "TestMachine_001.TestAlarm001"). Tag itself
|
||||
// may contain dots; the gateway-side parser splits on the first dot
|
||||
// after the '!' separator.
|
||||
string alarm_name = 1;
|
||||
// AVEVA alarm-provider name (literal "Galaxy" for ArchestrA Galaxies).
|
||||
string provider_name = 2;
|
||||
// Area/group name (e.g. "TestArea").
|
||||
string group_name = 3;
|
||||
string comment = 4;
|
||||
string operator_user = 5;
|
||||
string operator_node = 6;
|
||||
string operator_domain = 7;
|
||||
string operator_full_name = 8;
|
||||
}
|
||||
|
||||
message UnsubscribeBulkCommand {
|
||||
int32 server_handle = 1;
|
||||
repeated int32 item_handles = 2;
|
||||
|
||||
@@ -45,6 +45,39 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a full alarm reference of the form <c>Provider!Group.Tag</c>
|
||||
/// into its components. Convention: the first <c>!</c> separates
|
||||
/// provider from <c>Group.Tag</c>; the first <c>.</c> after the
|
||||
/// <c>!</c> separates group from tag (the tag itself may contain
|
||||
/// more dots — e.g. <c>TestMachine_001.TestAlarm001</c>).
|
||||
/// </summary>
|
||||
/// <returns>true on a well-formed reference; false otherwise.</returns>
|
||||
public static bool TryParseAlarmReference(
|
||||
string? reference,
|
||||
out string providerName,
|
||||
out string groupName,
|
||||
out string alarmName)
|
||||
{
|
||||
providerName = string.Empty;
|
||||
groupName = string.Empty;
|
||||
alarmName = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(reference)) return false;
|
||||
|
||||
int bang = reference!.IndexOf('!');
|
||||
if (bang <= 0 || bang == reference.Length - 1) return false;
|
||||
|
||||
string left = reference[..bang];
|
||||
string right = reference[(bang + 1)..];
|
||||
int dot = right.IndexOf('.');
|
||||
if (dot <= 0 || dot == right.Length - 1) return false;
|
||||
|
||||
providerName = left;
|
||||
groupName = right[..dot];
|
||||
alarmName = right[(dot + 1)..];
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AcknowledgeAlarmReply> AcknowledgeAsync(
|
||||
AcknowledgeAlarmRequest request,
|
||||
@@ -64,11 +97,58 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
||||
};
|
||||
}
|
||||
|
||||
if (!System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid))
|
||||
WorkerCommand workerCommand;
|
||||
if (System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid))
|
||||
{
|
||||
workerCommand = new WorkerCommand
|
||||
{
|
||||
Command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarm,
|
||||
AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
|
||||
{
|
||||
AlarmGuid = guid.ToString(),
|
||||
Comment = request.Comment ?? string.Empty,
|
||||
OperatorUser = request.OperatorUser ?? string.Empty,
|
||||
// Operator node/domain/full-name are not on the public
|
||||
// RPC surface today; pass empty strings so the worker
|
||||
// honours the existing AcknowledgeAlarmCommand schema.
|
||||
OperatorNode = string.Empty,
|
||||
OperatorDomain = string.Empty,
|
||||
OperatorFullName = string.Empty,
|
||||
},
|
||||
},
|
||||
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
||||
};
|
||||
}
|
||||
else if (TryParseAlarmReference(
|
||||
request.AlarmFullReference,
|
||||
out string providerName,
|
||||
out string groupName,
|
||||
out string alarmName))
|
||||
{
|
||||
workerCommand = new WorkerCommand
|
||||
{
|
||||
Command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||
{
|
||||
AlarmName = alarmName,
|
||||
ProviderName = providerName,
|
||||
GroupName = groupName,
|
||||
Comment = request.Comment ?? string.Empty,
|
||||
OperatorUser = request.OperatorUser ?? string.Empty,
|
||||
OperatorNode = string.Empty,
|
||||
OperatorDomain = string.Empty,
|
||||
OperatorFullName = string.Empty,
|
||||
},
|
||||
},
|
||||
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reference→GUID lookup not yet implemented. Surface a clear
|
||||
// diagnostic so client teams can plumb the reference parser
|
||||
// when the worker AlarmAckByName command lands.
|
||||
return new AcknowledgeAlarmReply
|
||||
{
|
||||
SessionId = request.SessionId,
|
||||
@@ -76,33 +156,12 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
||||
ProtocolStatus = new ProtocolStatus
|
||||
{
|
||||
Code = ProtocolStatusCode.InvalidRequest,
|
||||
Message = "AlarmFullReference must currently be a canonical GUID; reference→GUID lookup is pending the AlarmAckByName worker command.",
|
||||
Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.",
|
||||
},
|
||||
DiagnosticMessage = $"AcknowledgeAlarm received non-GUID reference '{request.AlarmFullReference}'.",
|
||||
DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.",
|
||||
};
|
||||
}
|
||||
|
||||
WorkerCommand workerCommand = new WorkerCommand
|
||||
{
|
||||
Command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarm,
|
||||
AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
|
||||
{
|
||||
AlarmGuid = guid.ToString(),
|
||||
Comment = request.Comment ?? string.Empty,
|
||||
OperatorUser = request.OperatorUser ?? string.Empty,
|
||||
// Operator node/domain/full-name are not on the public
|
||||
// RPC surface today; pass empty strings so the worker
|
||||
// honours the existing AcknowledgeAlarmCommand schema.
|
||||
OperatorNode = string.Empty,
|
||||
OperatorDomain = string.Empty,
|
||||
OperatorFullName = string.Empty,
|
||||
},
|
||||
},
|
||||
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
||||
};
|
||||
|
||||
WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -36,33 +36,6 @@ public sealed class WorkerAlarmRpcDispatcherTests
|
||||
Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_returns_invalid_request_when_reference_is_not_a_guid()
|
||||
{
|
||||
SessionRegistry registry = new SessionRegistry();
|
||||
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient();
|
||||
GatewaySession session = NewSession("s1");
|
||||
session.AttachWorkerClient(worker);
|
||||
session.MarkReady();
|
||||
registry.TryAdd(session);
|
||||
|
||||
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
||||
|
||||
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "s1",
|
||||
ClientCorrelationId = "c1",
|
||||
AlarmFullReference = "Galaxy!Area.Tag", // not a GUID
|
||||
Comment = "x",
|
||||
OperatorUser = "u",
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(0, worker.InvokeCount); // dispatcher short-circuits before the IPC.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status()
|
||||
{
|
||||
@@ -148,6 +121,107 @@ public sealed class WorkerAlarmRpcDispatcherTests
|
||||
Assert.Contains("-123", reply.DiagnosticMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")]
|
||||
[InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")]
|
||||
[InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")]
|
||||
public void TryParseAlarmReference_decomposes_provider_group_tag(
|
||||
string reference, string expectedProvider, string expectedGroup, string expectedName)
|
||||
{
|
||||
Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
|
||||
reference, out string provider, out string group, out string name));
|
||||
Assert.Equal(expectedProvider, provider);
|
||||
Assert.Equal(expectedGroup, group);
|
||||
Assert.Equal(expectedName, name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
[InlineData("no-bang-here")]
|
||||
[InlineData("!Group.Tag")] // empty provider
|
||||
[InlineData("Galaxy!")] // bang at end
|
||||
[InlineData("Galaxy!Group")] // missing dot
|
||||
[InlineData("Galaxy!.Tag")] // empty group
|
||||
[InlineData("Galaxy!Group.")] // empty tag
|
||||
public void TryParseAlarmReference_rejects_malformed_references(string? reference)
|
||||
{
|
||||
Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
|
||||
reference, out _, out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_routes_provider_group_tag_via_AckByName()
|
||||
{
|
||||
SessionRegistry registry = new SessionRegistry();
|
||||
AcknowledgeAlarmByNameCommand? observed = null;
|
||||
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
|
||||
{
|
||||
ReplyFactory = command =>
|
||||
{
|
||||
Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind);
|
||||
observed = command.Command.AcknowledgeAlarmByNameCommand;
|
||||
return new MxCommandReply
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
|
||||
Hresult = 0,
|
||||
AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
|
||||
};
|
||||
},
|
||||
};
|
||||
GatewaySession session = NewSession("s1");
|
||||
session.AttachWorkerClient(worker);
|
||||
session.MarkReady();
|
||||
registry.TryAdd(session);
|
||||
|
||||
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
||||
|
||||
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "s1",
|
||||
ClientCorrelationId = "c1",
|
||||
AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
|
||||
Comment = "ack-by-name",
|
||||
OperatorUser = "bob",
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName);
|
||||
Assert.Equal("Galaxy", observed.ProviderName);
|
||||
Assert.Equal("TestArea", observed.GroupName);
|
||||
Assert.Equal("bob", observed.OperatorUser);
|
||||
Assert.Equal("ack-by-name", observed.Comment);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_returns_invalid_request_for_unparseable_reference()
|
||||
{
|
||||
SessionRegistry registry = new SessionRegistry();
|
||||
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient();
|
||||
GatewaySession session = NewSession("s1");
|
||||
session.AttachWorkerClient(worker);
|
||||
session.MarkReady();
|
||||
registry.TryAdd(session);
|
||||
|
||||
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
||||
|
||||
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "s1",
|
||||
AlarmFullReference = "no-bang-no-dot",
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(0, worker.InvokeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload()
|
||||
{
|
||||
|
||||
@@ -164,6 +164,62 @@ public sealed class AlarmCommandExecutorTests
|
||||
Assert.Contains("-123", reply.DiagnosticMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcknowledgeAlarmByName_routes_tuple_to_handler()
|
||||
{
|
||||
FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 };
|
||||
MxAccessCommandExecutor executor = NewExecutor(handler);
|
||||
|
||||
StaCommand command = new StaCommand(
|
||||
SessionId, CorrelationId,
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||
{
|
||||
AlarmName = "TestMachine_001.TestAlarm001",
|
||||
ProviderName = "Galaxy",
|
||||
GroupName = "TestArea",
|
||||
Comment = "ack",
|
||||
OperatorUser = "alice",
|
||||
},
|
||||
});
|
||||
|
||||
MxCommandReply reply = executor.Execute(command);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.NotNull(reply.AcknowledgeAlarm);
|
||||
Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus);
|
||||
Assert.NotNull(handler.LastAckByNameTuple);
|
||||
Assert.Equal("TestMachine_001.TestAlarm001", handler.LastAckByNameTuple!.Value.Name);
|
||||
Assert.Equal("Galaxy", handler.LastAckByNameTuple!.Value.Provider);
|
||||
Assert.Equal("TestArea", handler.LastAckByNameTuple!.Value.Group);
|
||||
Assert.Equal("alice", handler.LastAckOperatorName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcknowledgeAlarmByName_with_empty_name_returns_invalid_request()
|
||||
{
|
||||
MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler());
|
||||
|
||||
StaCommand command = new StaCommand(
|
||||
SessionId, CorrelationId,
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||
{
|
||||
AlarmName = " ",
|
||||
ProviderName = "Galaxy",
|
||||
GroupName = "TestArea",
|
||||
},
|
||||
});
|
||||
|
||||
MxCommandReply reply = executor.Execute(command);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryActiveAlarms_returns_payload_with_snapshots()
|
||||
{
|
||||
@@ -373,6 +429,18 @@ public sealed class AlarmCommandExecutorTests
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public int AcknowledgeByName(
|
||||
string alarmName, string providerName, string groupName,
|
||||
string comment, string operatorUser, string operatorNode,
|
||||
string operatorDomain, string operatorFullName)
|
||||
{
|
||||
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||
LastAckOperatorName = operatorUser;
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||
|
||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||
{
|
||||
LastFilterPrefix = alarmFilterPrefix;
|
||||
|
||||
@@ -222,6 +222,18 @@ public sealed class AlarmCommandHandlerTests
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public int AcknowledgeByName(
|
||||
string alarmName, string providerName, string groupName,
|
||||
string ackComment, string ackOperatorName, string ackOperatorNode,
|
||||
string ackOperatorDomain, string ackOperatorFullName)
|
||||
{
|
||||
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||
LastAckOperatorName = ackOperatorName;
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => SnapshotResult;
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -158,6 +158,32 @@ public sealed class AlarmDispatcherTests
|
||||
Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcknowledgeByName_forwards_to_consumer_with_full_tuple()
|
||||
{
|
||||
FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = 0 };
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||
consumer,
|
||||
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||
SessionId);
|
||||
|
||||
int rc = dispatcher.AcknowledgeByName(
|
||||
alarmName: "TestMachine_001.TestAlarm001",
|
||||
providerName: "Galaxy",
|
||||
groupName: "TestArea",
|
||||
ackComment: "ack",
|
||||
ackOperatorName: "alice",
|
||||
ackOperatorNode: "WS",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Alice Smith");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.NotNull(consumer.LastAckByNameTuple);
|
||||
Assert.Equal("TestMachine_001.TestAlarm001", consumer.LastAckByNameTuple!.Value.Name);
|
||||
Assert.Equal("Galaxy", consumer.LastAckByNameTuple!.Value.Provider);
|
||||
Assert.Equal("TestArea", consumer.LastAckByNameTuple!.Value.Group);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SnapshotActiveAlarms_maps_records_to_protos()
|
||||
{
|
||||
@@ -275,6 +301,18 @@ public sealed class AlarmDispatcherTests
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public int AcknowledgeByName(
|
||||
string alarmName, string providerName, string groupName,
|
||||
string ackComment, string ackOperatorName, string ackOperatorNode,
|
||||
string ackOperatorDomain, string ackOperatorFullName)
|
||||
{
|
||||
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||
LastAckOperatorName = ackOperatorName;
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
||||
{
|
||||
return SnapshotResult;
|
||||
|
||||
@@ -120,6 +120,29 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
||||
operatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string comment,
|
||||
string operatorUser,
|
||||
string operatorNode,
|
||||
string operatorDomain,
|
||||
string operatorFullName)
|
||||
{
|
||||
AlarmDispatcher? d = GetDispatcherOrThrow();
|
||||
return d.AcknowledgeByName(
|
||||
alarmName ?? string.Empty,
|
||||
providerName ?? string.Empty,
|
||||
groupName ?? string.Empty,
|
||||
comment ?? string.Empty,
|
||||
operatorUser ?? string.Empty,
|
||||
operatorNode ?? string.Empty,
|
||||
operatorDomain ?? string.Empty,
|
||||
operatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||
{
|
||||
@@ -184,6 +207,20 @@ public interface IAlarmCommandHandler : IDisposable
|
||||
string operatorDomain,
|
||||
string operatorFullName);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledge a single alarm by (name, provider, group) — used when
|
||||
/// the caller has the human-readable reference but not the GUID.
|
||||
/// </summary>
|
||||
int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string comment,
|
||||
string operatorUser,
|
||||
string operatorNode,
|
||||
string operatorDomain,
|
||||
string operatorFullName);
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot the currently-active alarm set, optionally scoped to a
|
||||
/// prefix matched against <c>AlarmFullReference</c>.
|
||||
|
||||
@@ -92,6 +92,33 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
ackOperatorFullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledge an alarm by its (name, provider, group) tuple.
|
||||
/// Routes to the consumer's <c>AcknowledgeByName</c> path which
|
||||
/// maps to <c>wwAlarmConsumerClass.AlarmAckByName</c>.
|
||||
/// </summary>
|
||||
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(AlarmDispatcher));
|
||||
return consumer.AcknowledgeByName(
|
||||
alarmName,
|
||||
providerName,
|
||||
groupName,
|
||||
ackComment,
|
||||
ackOperatorName,
|
||||
ackOperatorNode,
|
||||
ackOperatorDomain,
|
||||
ackOperatorFullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot the currently-active alarm set as
|
||||
/// <see cref="ActiveAlarmSnapshot"/> protos for the
|
||||
|
||||
@@ -61,6 +61,23 @@ public interface IMxAccessAlarmConsumer : IDisposable
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledge a single alarm by its (name, provider, group) tuple.
|
||||
/// Reaches AVEVA's <c>AlarmAckByName</c> on
|
||||
/// <c>wwAlarmConsumerClass</c>; same alarm-history outcome as
|
||||
/// <see cref="AcknowledgeByGuid"/>, used when the caller has the
|
||||
/// human-readable reference but not the canonical GUID.
|
||||
/// </summary>
|
||||
int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string ackComment,
|
||||
string ackOperatorName,
|
||||
string ackOperatorNode,
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the consumer's most recently parsed snapshot of currently
|
||||
/// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7)
|
||||
|
||||
@@ -83,6 +83,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command),
|
||||
MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command),
|
||||
MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command),
|
||||
MxCommandKind.AcknowledgeAlarmByName => ExecuteAcknowledgeAlarmByName(command),
|
||||
MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command),
|
||||
_ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."),
|
||||
};
|
||||
@@ -402,6 +403,54 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
}
|
||||
}
|
||||
|
||||
private MxCommandReply ExecuteAcknowledgeAlarmByName(StaCommand command)
|
||||
{
|
||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AcknowledgeAlarmByNameCommand)
|
||||
{
|
||||
return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName command payload is required.");
|
||||
}
|
||||
if (alarmCommandHandler is null)
|
||||
{
|
||||
return CreateInvalidRequestReply(
|
||||
command,
|
||||
"AcknowledgeAlarmByName requires an alarm command handler; the worker was constructed without one.");
|
||||
}
|
||||
|
||||
AcknowledgeAlarmByNameCommand payload = command.Command.AcknowledgeAlarmByNameCommand;
|
||||
if (string.IsNullOrWhiteSpace(payload.AlarmName))
|
||||
{
|
||||
return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName.alarm_name is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int rc = alarmCommandHandler.AcknowledgeByName(
|
||||
payload.AlarmName,
|
||||
payload.ProviderName,
|
||||
payload.GroupName,
|
||||
payload.Comment,
|
||||
payload.OperatorUser,
|
||||
payload.OperatorNode,
|
||||
payload.OperatorDomain,
|
||||
payload.OperatorFullName);
|
||||
MxCommandReply reply = CreateOkReply(command);
|
||||
reply.Hresult = rc;
|
||||
reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload
|
||||
{
|
||||
NativeStatus = rc,
|
||||
};
|
||||
if (rc != 0)
|
||||
{
|
||||
reply.DiagnosticMessage = $"AVEVA AlarmAckByName returned non-zero status {rc}.";
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateAlarmFailureReply(command, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command)
|
||||
{
|
||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand)
|
||||
|
||||
@@ -172,6 +172,33 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
szOprFullName: ackOperatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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(WnWrapAlarmConsumer));
|
||||
|
||||
wwAlarmConsumerClass com = client
|
||||
?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
|
||||
|
||||
return com.AlarmAckByName(
|
||||
szAlarmName: alarmName ?? string.Empty,
|
||||
szProviderName: providerName ?? string.Empty,
|
||||
szGroupName: groupName ?? string.Empty,
|
||||
szComment: ackComment ?? string.Empty,
|
||||
szOprName: ackOperatorName ?? string.Empty,
|
||||
szNode: ackOperatorNode ?? string.Empty,
|
||||
szDomainName: ackOperatorDomain ?? string.Empty,
|
||||
szOprFullName: ackOperatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user