feat(scripted-alarms): materialise real Part 9 AlarmConditionState nodes (T14)
This commit is contained in:
@@ -285,6 +285,36 @@ public sealed class Phase7Applier
|
||||
composition.EquipmentVirtualTags.Select(v => v.EquipmentId).Distinct(StringComparer.Ordinal).Count());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T14 — materialise real OPC UA Part 9 <c>AlarmConditionState</c> nodes from a composition
|
||||
/// snapshot. For each <b>enabled</b> <see cref="EquipmentScriptedAlarmPlan"/>, register a
|
||||
/// condition node (keyed by its <see cref="EquipmentScriptedAlarmPlan.ScriptedAlarmId"/>, which
|
||||
/// is the same id <c>OpcUaPublishActor.AlarmStateUpdate</c> targets) under its equipment folder.
|
||||
/// Disabled alarms are skipped — they expose no node. Must run AFTER
|
||||
/// <see cref="MaterialiseHierarchy"/> so the equipment folders exist. Idempotent (the sink's
|
||||
/// <c>MaterialiseAlarmCondition</c> re-creates cleanly on re-apply).
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the scripted alarms to materialise.</param>
|
||||
public void MaterialiseScriptedAlarms(Phase7CompositionResult composition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(composition);
|
||||
if (composition.EquipmentScriptedAlarms.Count == 0) return;
|
||||
|
||||
var materialised = 0;
|
||||
foreach (var alarm in composition.EquipmentScriptedAlarms)
|
||||
{
|
||||
if (!alarm.Enabled) continue;
|
||||
SafeMaterialiseAlarmCondition(alarm.ScriptedAlarmId, alarm.EquipmentId, alarm.Name, alarm.AlarmType, alarm.Severity);
|
||||
materialised++;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
|
||||
materialised,
|
||||
composition.EquipmentScriptedAlarms.Where(a => a.Enabled)
|
||||
.Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count());
|
||||
}
|
||||
|
||||
/// <summary>Deterministic NodeId for a tag's FolderPath sub-folder, scoped under its equipment
|
||||
/// folder so two equipments' identically-named sub-folders never collide.</summary>
|
||||
private static string EquipmentSubFolderNodeId(string equipmentId, string folderPath) =>
|
||||
@@ -307,6 +337,12 @@ public sealed class Phase7Applier
|
||||
try { _sink.WriteAlarmState(nodeId, active, acknowledged, ts); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmState threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity)
|
||||
{
|
||||
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary>
|
||||
|
||||
Reference in New Issue
Block a user