Files
Joseph Doherty 29665c1872 feat(opcua): HistoryReadEvents projects EventType, ConditionName, SourceNode; honours the where-clause (lmxopcua#746)
HistoryReadEvents projected only EventId / SourceName / Time / ReceiveTime /
Message / Severity: a select clause asking for EventType, ConditionName or
SourceNode (what most generic alarm viewers ask for) got nulls, and the
event filter's where-clause (an OfType) was ignored.

- HistoricalEvent gains optional EventTypeName, ConditionName and AlarmId.
  SqlServer, QuestDB and Gateway event reads fill them from what their
  alarm writers already store (AlarmTypeName / AlarmName / AlarmId; the
  gateway event Type and its AlarmName / AlarmId properties).
- The node manager maps the type name to its Part 9 event type (unknown or
  non-event => AlarmConditionType), projects ConditionName, and resolves
  SourceNode to the materialised condition the alarm id names (the live
  events' SourceNode), else null.
- The where-clause is evaluated per event with the SDK content filter
  through an IFilterTarget (OfType via the type tree; other operators
  against the projected fields), after the page's cursor is taken, so
  paging is unchanged.

Tested in the node manager, over a real client session (the Client.CLI has
no `historyread --events` mode yet; filed as a follow-up), and by alarm
writer -> event read round trips on SqlServer and QuestDB.

Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22
in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:40:47 -05:00

61 lines
2.9 KiB
C#

using Google.Protobuf.WellKnownTypes;
using Xunit;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
public sealed class EventMapperTests
{
[Fact]
public void Carries_the_alarm_type_condition_name_and_alarm_id_the_writer_sent()
{
// The round trip of AlarmEventMapper's shape: Type = AlarmTypeName, Properties AlarmName / AlarmId.
var sent = AlarmEventMapper.ToHistorianEvent(new ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.AlarmHistorianEvent(
"Plant/Modbus/dev1/temp_hi", "Area/Line/Eq", "HiTemp", "LimitAlarm", ZB.MOM.WW.OtOpcUa.Core.Abstractions.AlarmSeverity.High,
"Activated", "high", "u", null, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
var h = EventMapper.ToHistoricalEvent(sent);
Assert.Equal("LimitAlarm", h.EventTypeName);
Assert.Equal("HiTemp", h.ConditionName);
Assert.Equal("Plant/Modbus/dev1/temp_hi", h.AlarmId);
// A gateway row without them (e.g. an event another writer produced) maps to nulls, never throws.
var bare = new HistorianEvent { Id = "E", EventTime = Ts(2026, 1, 1, 0, 0, 0), ReceivedTime = Ts(2026, 1, 1, 0, 0, 0) };
var b = EventMapper.ToHistoricalEvent(bare);
Assert.Null(b.EventTypeName);
Assert.Null(b.ConditionName);
Assert.Null(b.AlarmId);
}
[Fact]
public void Maps_core_fields_and_times()
{
var e = new HistorianEvent { Id = "E1", SourceName = "Pump1",
EventTime = Ts(2026, 1, 1, 0, 0, 0), ReceivedTime = Ts(2026, 1, 1, 0, 0, 5) };
e.Properties["Message"] = "High temp";
e.Properties["Severity"] = "700";
var h = EventMapper.ToHistoricalEvent(e);
Assert.Equal("E1", h.EventId);
Assert.Equal("Pump1", h.SourceName);
Assert.Equal("High temp", h.Message);
Assert.Equal((ushort)700, h.Severity);
Assert.Equal(DateTimeKind.Utc, h.EventTimeUtc.Kind);
}
[Theory]
[InlineData("Priority", "999", 999)]
[InlineData("Severity", "0", 1)] // clamp to OPC UA min 1
[InlineData("Severity", "5000", 1000)] // clamp to OPC UA max 1000
[InlineData(null, null, 1)] // missing → default min severity
public void Severity_parsed_and_clamped(string? key, string? val, int expected)
{
var e = new HistorianEvent { Id = "E", EventTime = Ts(2026, 1, 1, 0, 0, 0), ReceivedTime = Ts(2026, 1, 1, 0, 0, 0) };
if (key is not null) e.Properties[key] = val!;
Assert.Equal((ushort)expected, EventMapper.ToHistoricalEvent(e).Severity);
}
// Ts(...) builds a Google.Protobuf.WellKnownTypes.Timestamp from UTC parts.
private static Timestamp Ts(int y, int mo, int d, int h, int mi, int s)
=> Timestamp.FromDateTime(new DateTime(y, mo, d, h, mi, s, DateTimeKind.Utc));
}