Quickstarts alarms: report one event per Enable instead of two

ConditionTypeHolder.OnEnableDisableAlarm runs as the OnEnableDisable hook, before
ConditionState.OnEnableCalled/OnDisableCalled change the EnabledState. It switched the state and called
ReportEvent itself, and the core then reported the same change again (AutoReportStateChanges is set by
every concrete holder), so each Enable produced two condition events with different EventIds. Disable
was not affected because the hook's ReportEvent skips disabled conditions.

Part 9 5.5.2: a change of EnabledState causes one ConditionType Event Notification. The hook now only
sets the Message and leaves the state change and its event to the core; it still changes the state and
reports for a condition without AutoReportStateChanges.

The CTT's A and C Enable Test_002 took the second notification as the answer to its next call
(worked around in the CTT scripts as C61).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roman Ettlinger
2026-10-14 06:41:03 +02:00
parent 6cfb8cf196
commit f6209beb33
2 changed files with 42 additions and 3 deletions
@@ -282,13 +282,21 @@ namespace Alarms
if (enabling != alarm.EnabledState!.Id!.Value)
{
alarm.SetEnableState(SystemContext, enabling);
alarm.Message!.Value = enabling ?
LocalizedText.From("Enabling") :
LocalizedText.From("Disabling alarm " + MapName);
// if disabled, it will not fire
ReportEvent();
// This runs before ConditionState.OnEnableCalled/OnDisableCalled change the EnabledState.
// A condition with AutoReportStateChanges reports that change itself; reporting here as well
// sent two Event Notifications for one Enable (Part 9 5.5.2: a change of EnabledState causes
// one ConditionType Event Notification).
if (!alarm.AutoReportStateChanges)
{
alarm.SetEnableState(SystemContext, enabling);
// if disabled, it will not fire
ReportEvent();
}
}
else if (enabling)
{
@@ -28,6 +28,7 @@
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
@@ -369,9 +370,39 @@ namespace Opc.Ua.History.Tests
AssertEventRetain(enableEvent, expected: true,
"Enable should restore Retain when the source changed to an active alarm while disabled.");
// One EnabledState change causes one Event Notification (Part 9 5.5.2). Both notifications of a
// duplicate report are generated during the call, so count the events of the call window.
await Task.Delay(500).ConfigureAwait(false);
int enableEvents = CountEventsInWindow(collector, alarmId, enableStart, enableEnd);
Assert.That(enableEvents, Is.EqualTo(1),
"Enable should report the EnabledState change with exactly one condition event.");
await NormalizeAlarmAsync(alarmId).ConfigureAwait(false);
}
private static int CountEventsInWindow(
AlarmEventCollector collector,
NodeId alarmId,
DateTime start,
DateTime end)
{
if (!collector.EventLog.TryGetValue(alarmId, out List<EventFieldList>? events))
{
return 0;
}
int count = 0;
foreach (EventFieldList e in events)
{
if (AlarmEventCollector.TryGetDateTime(e, AlarmEventCollector.FieldIndex.Time, out DateTime time) &&
time >= start.AddMilliseconds(-50) &&
time <= end.AddMilliseconds(50))
{
count++;
}
}
return count;
}
private static void AssertTransitionTimeInCallWindow(
EventFieldList eventFields,
AlarmEventCollector.FieldIndex fieldIndex,