diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs
index 24ed133b..6128c6dd 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs
@@ -20,6 +20,12 @@ public static class EquipmentScriptPaths
private static readonly Regex GetTagRefRegex =
new(@"ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)", RegexOptions.Compiled);
+ // {TagPath} message-template token — a single-brace {…} run. The negative look-behind/-ahead
+ // reject the reserved {{equip}} double-brace form (and any other doubled brace), so "{A.X}"
+ // matches but "{{equip}}" does not yield "{equip}".
+ private static readonly Regex MessageTemplateTokenRegex =
+ new(@"(?
+ /// Merge a scripted alarm's dependency graph: the predicate script's distinct
+ /// ctx.GetTag("…") read refs (via , first-seen order)
+ /// UNION the distinct {TagPath} token paths referenced in the message template (first-seen
+ /// order, appended after the predicate reads, trimmed + non-empty). The reserved
+ /// {{equip}} double-brace form is excluded by the token regex. Deterministic so the live
+ /// composer (Phase7Composer) and the artifact-decode mirror (DeploymentArtifact)
+ /// produce the exact same ordered list — the byte-parity contract EquipmentScriptedAlarmPlan
+ /// equality depends on. Scripted alarms do NOT use {{equip}} substitution (only virtual
+ /// tags do) — pass the predicate source as-is.
+ ///
+ /// The resolved predicate script source.
+ /// The alarm message template carrying {TagPath} tokens.
+ /// The merged, distinct, deterministically-ordered dependency refs.
+ public static IReadOnlyList ExtractAlarmDependencyRefs(string? predicateSource, string? messageTemplate)
+ {
+ var seen = new HashSet(StringComparer.Ordinal);
+ var result = new List();
+
+ // Predicate reads first — same regex extraction the VirtualTag projection uses.
+ foreach (var r in ExtractDependencyRefs(predicateSource))
+ {
+ if (seen.Add(r)) result.Add(r);
+ }
+
+ // Then message-template {TagPath} tokens (first-seen), trimmed, non-empty.
+ if (!string.IsNullOrEmpty(messageTemplate))
+ {
+ foreach (Match m in MessageTemplateTokenRegex.Matches(messageTemplate))
+ {
+ var token = m.Groups[1].Value.Trim();
+ if (token.Length > 0 && seen.Add(token)) result.Add(token);
+ }
+ }
+
+ return result;
+ }
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
index 23a628dd..dd527993 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Text.Json;
-using System.Text.RegularExpressions;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
@@ -208,10 +207,10 @@ public sealed record EquipmentScriptedAlarmPlan(
/// diffs empty (mirrors ).
///
/// DependencyRefs equality is order-sensitive (SequenceEqual).
- /// is the canonical, deterministic
+ /// is the canonical, deterministic
/// producer of that order (predicate ctx.GetTag reads first, then first-seen message
/// template tokens). Downstream byte-parity between the live composer and the artifact-decode
- /// mirror depends on both sides calling MergeAlarmDependencyRefs with identical inputs.
+ /// mirror depends on both sides calling ExtractAlarmDependencyRefs with identical inputs.
///
public bool Equals(EquipmentScriptedAlarmPlan? other) =>
other is not null &&
@@ -452,7 +451,10 @@ public static class Phase7Composer
MessageTemplate: a.MessageTemplate,
PredicateScriptId: a.PredicateScriptId,
PredicateSource: source,
- DependencyRefs: MergeAlarmDependencyRefs(source, a.MessageTemplate),
+ // Scripted alarms do NOT use {{equip}} substitution (only virtual tags do) — pass the
+ // predicate source as-is. The merge (predicate reads first, then template tokens) lives
+ // in the shared EquipmentScriptPaths helper so the artifact-decode mirror agrees.
+ DependencyRefs: EquipmentScriptPaths.ExtractAlarmDependencyRefs(source, a.MessageTemplate),
HistorizeToAveva: a.HistorizeToAveva,
Retain: a.Retain,
Enabled: a.Enabled));
@@ -466,47 +468,6 @@ public static class Phase7Composer
};
}
- // {TagPath} message-template token — a single-brace {…} run. Reuses no existing helper because
- // EquipmentScriptPaths only handles {{equip}} + ctx.GetTag literals; this is the alarm-message
- // surface. The negative look-behind/-ahead reject the reserved {{equip}} double-brace form
- // (and any other doubled brace), so "{A.X}" matches but "{{equip}}" does not yield "{equip}".
- private static readonly Regex MessageTemplateTokenRegex =
- new(@"(?
- /// Merge the alarm's dependency graph: the predicate script's distinct ctx.GetTag("…")
- /// read refs (first-seen order) UNION the distinct {TagPath} token paths referenced in
- /// the message template (first-seen order, appended after the predicate reads). The reserved
- /// {{equip}} double-brace form is excluded by the token regex. Deterministic so the
- /// artifact-decode mirror (sibling task) can reproduce the exact same ordered list.
- ///
- /// The resolved predicate script source.
- /// The alarm message template carrying {TagPath} tokens.
- /// The merged, distinct, deterministically-ordered dependency refs.
- private static IReadOnlyList MergeAlarmDependencyRefs(string predicateSource, string? messageTemplate)
- {
- var seen = new HashSet(StringComparer.Ordinal);
- var result = new List();
-
- // Predicate reads first — same regex extraction the VirtualTag projection uses.
- foreach (var r in EquipmentScriptPaths.ExtractDependencyRefs(predicateSource))
- {
- if (seen.Add(r)) result.Add(r);
- }
-
- // Then message-template {TagPath} tokens (first-seen), trimmed, non-empty.
- if (!string.IsNullOrEmpty(messageTemplate))
- {
- foreach (Match m in MessageTemplateTokenRegex.Matches(messageTemplate))
- {
- var token = m.Groups[1].Value.Trim();
- if (token.Length > 0 && seen.Add(token)) result.Add(token);
- }
- }
-
- return result;
- }
-
///
/// Extract the driver-side full reference from a JSON blob: the
/// CK_Tag_TagConfig_IsJson constraint guarantees a JSON object, and every shipped
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
index 07137975..3f590452 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
@@ -195,11 +195,13 @@ public static class DeploymentArtifact
var galaxyTags = BuildGalaxyTagPlans(root, drivers);
var equipmentTags = BuildEquipmentTagPlans(root);
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
+ var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms, galaxyTags)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
+ EquipmentScriptedAlarms = equipmentScriptedAlarms,
};
}
catch (JsonException)
@@ -261,6 +263,7 @@ public static class DeploymentArtifact
{
EquipmentTags = full.EquipmentTags.Where(t => sets.DriverIds.Contains(t.DriverInstanceId)).ToArray(),
EquipmentVirtualTags = full.EquipmentVirtualTags.Where(v => sets.EquipmentIds.Contains(v.EquipmentId)).ToArray(),
+ EquipmentScriptedAlarms = full.EquipmentScriptedAlarms.Where(a => sets.EquipmentIds.Contains(a.EquipmentId)).ToArray(),
};
}
@@ -606,6 +609,87 @@ public static class DeploymentArtifact
return result;
}
+ ///
+ /// Join the artifact's ScriptedAlarms array to its Scripts array (by PredicateScriptId) to emit
+ /// one per alarm. The artifact-decode mirror of
+ /// Phase7Composer.Compose's scripted-alarm producer — so the compose-side + artifact-decode
+ /// plans agree byte-for-byte. An alarm whose PredicateScriptId has no matching Script row is
+ /// SKIPPED (matching the composer's skip behaviour) to preserve parity. PredicateSource = the
+ /// joined script source ("" when missing — but such alarms are skipped above); DependencyRefs
+ /// = the shared merge of the predicate's
+ /// distinct ctx.GetTag("…") reads UNION the message template's {TagPath} tokens. Scripted
+ /// alarms do NOT use {{equip}} substitution (only virtual tags do) — the predicate source is
+ /// used as-is. Ordered by EquipmentId then ScriptedAlarmId to match the composer's deterministic order.
+ ///
+ private static IReadOnlyList BuildEquipmentScriptedAlarmPlans(JsonElement root)
+ {
+ if (!root.TryGetProperty("ScriptedAlarms", out var alarmsArr) || alarmsArr.ValueKind != JsonValueKind.Array)
+ return Array.Empty();
+
+ // scriptId → SourceCode (the predicate source the alarm host evaluates). Same join the
+ // VirtualTag builder uses; an alarm whose PredicateScriptId is absent here is skipped below.
+ var scriptSourceById = new Dictionary(StringComparer.Ordinal);
+ if (root.TryGetProperty("Scripts", out var scriptsArr) && scriptsArr.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var el in scriptsArr.EnumerateArray())
+ {
+ if (el.ValueKind != JsonValueKind.Object) continue;
+ var sid = el.TryGetProperty("ScriptId", out var sidEl) ? sidEl.GetString() : null;
+ if (string.IsNullOrWhiteSpace(sid)) continue;
+ var src = el.TryGetProperty("SourceCode", out var srcEl) && srcEl.ValueKind == JsonValueKind.String
+ ? srcEl.GetString() : null;
+ scriptSourceById[sid!] = src ?? string.Empty;
+ }
+ }
+
+ var result = new List(alarmsArr.GetArrayLength());
+ foreach (var el in alarmsArr.EnumerateArray())
+ {
+ if (el.ValueKind != JsonValueKind.Object) continue;
+ var scriptedAlarmId = el.TryGetProperty("ScriptedAlarmId", out var idEl) ? idEl.GetString() : null;
+ var equipmentId = el.TryGetProperty("EquipmentId", out var eqEl) ? eqEl.GetString() : null;
+ var name = el.TryGetProperty("Name", out var nmEl) ? nmEl.GetString() : null;
+ var alarmType = el.TryGetProperty("AlarmType", out var atEl) ? atEl.GetString() : null;
+ var severity = el.TryGetProperty("Severity", out var svEl) && svEl.TryGetInt32(out var sv) ? sv : 0;
+ var messageTemplate = el.TryGetProperty("MessageTemplate", out var mtEl) ? mtEl.GetString() : null;
+ var predicateScriptId = el.TryGetProperty("PredicateScriptId", out var psEl) ? psEl.GetString() : null;
+ var historize = el.TryGetProperty("HistorizeToAveva", out var hEl) && hEl.ValueKind != JsonValueKind.Null
+ ? hEl.GetBoolean() : false;
+ var retain = el.TryGetProperty("Retain", out var rEl) && rEl.ValueKind != JsonValueKind.Null
+ ? rEl.GetBoolean() : false;
+ var enabled = el.TryGetProperty("Enabled", out var enEl) && enEl.ValueKind != JsonValueKind.Null
+ ? enEl.GetBoolean() : false;
+
+ if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue;
+
+ // Skip alarms whose predicate script is missing — matching Phase7Composer's skip behaviour
+ // so both sides emit the same set (byte-parity).
+ if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source))
+ continue;
+
+ result.Add(new EquipmentScriptedAlarmPlan(
+ ScriptedAlarmId: scriptedAlarmId!,
+ EquipmentId: equipmentId ?? string.Empty,
+ Name: name ?? string.Empty,
+ AlarmType: alarmType ?? string.Empty,
+ Severity: severity,
+ MessageTemplate: messageTemplate ?? string.Empty,
+ PredicateScriptId: predicateScriptId,
+ PredicateSource: source,
+ DependencyRefs: EquipmentScriptPaths.ExtractAlarmDependencyRefs(source, messageTemplate),
+ HistorizeToAveva: historize,
+ Retain: retain,
+ Enabled: enabled));
+ }
+
+ result.Sort((a, b) =>
+ {
+ var byEquipment = string.CompareOrdinal(a.EquipmentId, b.EquipmentId);
+ return byEquipment != 0 ? byEquipment : string.CompareOrdinal(a.ScriptedAlarmId, b.ScriptedAlarmId);
+ });
+ return result;
+ }
+
///
/// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName"
/// field). The artifact-decode mirror of Phase7Composer.ExtractTagFullName /
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactScriptedAlarmParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactScriptedAlarmParityTests.cs
new file mode 100644
index 00000000..f81080e7
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactScriptedAlarmParityTests.cs
@@ -0,0 +1,242 @@
+using System.Linq;
+using System.Text.Json;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
+using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
+
+///
+/// Byte-parity tests for the scripted-alarm deployment-artifact decode path
+/// (DeploymentArtifact.BuildEquipmentScriptedAlarmPlans) against the live compose seam
+/// (Phase7Composer.Compose). Both sides derive EquipmentScriptedAlarmPlan from the
+/// same ScriptedAlarm + Script data via the shared EquipmentScriptPaths.ExtractAlarmDependencyRefs
+/// helper, so the decoded plans must equal the composer's element-wise (the record has value
+/// equality including DependencyRefs order). Mirrors the existing EquipmentVirtualTags parity
+/// style in DeploymentArtifactEquipTokenTests.
+///
+public sealed class DeploymentArtifactScriptedAlarmParityTests
+{
+ private static byte[] BlobOf(object snapshot) => JsonSerializer.SerializeToUtf8Bytes(snapshot);
+
+ /// The live composer's plan for the same scripted alarms + scripts the artifact carries
+ /// must equal the artifact-decode plan, field-for-field including DependencyRefs order.
+ [Fact]
+ public void ParseComposition_scripted_alarms_are_byte_parity_with_composer()
+ {
+ // Two equipments, each with a scripted alarm: predicate reads a tag via ctx.GetTag("X.Y"),
+ // and the message template carries a distinct {A.B} token — so DependencyRefs exercise the
+ // predicate-reads-then-template-tokens merge order on both sides.
+ var script1 = new Script
+ {
+ ScriptId = "s-1",
+ Name = "hot-1",
+ SourceCode = "return System.Convert.ToDouble(ctx.GetTag(\"Mach1.Temp\").Value) > 80;",
+ SourceHash = "h1",
+ };
+ var script2 = new Script
+ {
+ ScriptId = "s-2",
+ Name = "hot-2",
+ SourceCode = "return System.Convert.ToDouble(ctx.GetTag(\"Mach2.Temp\").Value) > 80;",
+ SourceHash = "h2",
+ };
+ var alarm1 = new ScriptedAlarm
+ {
+ ScriptedAlarmId = "al-1",
+ EquipmentId = "eq-1",
+ Name = "Overheat-1",
+ AlarmType = "LimitAlarm",
+ Severity = 700,
+ MessageTemplate = "Machine 1 at {Mach1.Pressure}",
+ PredicateScriptId = "s-1",
+ HistorizeToAveva = true,
+ Retain = true,
+ Enabled = true,
+ };
+ var alarm2 = new ScriptedAlarm
+ {
+ ScriptedAlarmId = "al-2",
+ EquipmentId = "eq-2",
+ Name = "Overheat-2",
+ AlarmType = "OffNormalAlarm",
+ Severity = 250,
+ MessageTemplate = "Machine 2 at {Mach2.Pressure}",
+ PredicateScriptId = "s-2",
+ HistorizeToAveva = false,
+ Retain = false,
+ Enabled = false,
+ };
+
+ var composed = Phase7Composer.Compose(
+ Array.Empty(), Array.Empty(), Array.Empty(),
+ Array.Empty(), new[] { alarm1, alarm2 },
+ Array.Empty(), Array.Empty(),
+ virtualTags: Array.Empty(),
+ scripts: new[] { script1, script2 });
+
+ // The artifact blob the write side (ConfigComposer) emits: the FULL ScriptedAlarm + Script
+ // entities serialised Pascal-case off the EF entity.
+ var blob = BlobOf(new
+ {
+ ScriptedAlarms = new[]
+ {
+ ToSnapshot(alarm1),
+ ToSnapshot(alarm2),
+ },
+ Scripts = new[]
+ {
+ new { ScriptId = script1.ScriptId, SourceCode = script1.SourceCode },
+ new { ScriptId = script2.ScriptId, SourceCode = script2.SourceCode },
+ },
+ });
+
+ var decoded = DeploymentArtifact.ParseComposition(blob);
+
+ // Byte-parity: element-wise value equality (record equality compares DependencyRefs order).
+ decoded.EquipmentScriptedAlarms.Count.ShouldBe(2);
+ decoded.EquipmentScriptedAlarms.SequenceEqual(composed.EquipmentScriptedAlarms).ShouldBeTrue();
+ // And spot-check the merged DependencyRefs (predicate read first, then template token).
+ var plan1 = decoded.EquipmentScriptedAlarms.Single(p => p.ScriptedAlarmId == "al-1");
+ plan1.DependencyRefs.ShouldBe(new[] { "Mach1.Temp", "Mach1.Pressure" });
+ plan1.Enabled.ShouldBeTrue();
+ var plan2 = decoded.EquipmentScriptedAlarms.Single(p => p.ScriptedAlarmId == "al-2");
+ plan2.DependencyRefs.ShouldBe(new[] { "Mach2.Temp", "Mach2.Pressure" });
+ plan2.Enabled.ShouldBeFalse();
+ plan2.HistorizeToAveva.ShouldBeFalse();
+ plan2.Retain.ShouldBeFalse();
+ }
+
+ /// The cluster-scoped overload keeps only the scripted alarms whose EquipmentId belongs to
+ /// an in-cluster driver (mirroring how EquipmentVirtualTags + EquipmentTags are filtered).
+ [Fact]
+ public void ParseComposition_scoped_keeps_only_my_clusters_scripted_alarms()
+ {
+ var blob = BlobOf(new
+ {
+ Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
+ Nodes = new[]
+ {
+ new { NodeId = "central-1:4053", ClusterId = "MAIN" },
+ new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
+ },
+ DriverInstances = new[]
+ {
+ new { DriverInstanceId = "main-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
+ new { DriverInstanceId = "sa-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
+ },
+ Equipment = new[]
+ {
+ new { EquipmentId = "eq-main", Name = "eqm", UnsLineId = "l1", DriverInstanceId = "main-modbus" },
+ new { EquipmentId = "eq-sa", Name = "eqs", UnsLineId = "l2", DriverInstanceId = "sa-modbus" },
+ },
+ Scripts = new[]
+ {
+ new { ScriptId = "scr", SourceCode = "return ctx.GetTag(\"A.X\").Value;" },
+ },
+ ScriptedAlarms = new[]
+ {
+ NewAlarmSnapshot("al-main", "eq-main", "scr"),
+ NewAlarmSnapshot("al-sa", "eq-sa", "scr"),
+ },
+ });
+
+ var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
+ main.EquipmentScriptedAlarms.Select(a => a.ScriptedAlarmId).ShouldBe(new[] { "al-main" });
+
+ var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
+ siteA.EquipmentScriptedAlarms.Select(a => a.ScriptedAlarmId).ShouldBe(new[] { "al-sa" });
+ }
+
+ /// An alarm whose PredicateScriptId matches no Script row is SKIPPED on BOTH the composer
+ /// and the artifact-decode side, so parity holds for the surviving alarm.
+ [Fact]
+ public void ParseComposition_skips_alarm_with_missing_predicate_script_matching_composer()
+ {
+ var goodScript = new Script
+ {
+ ScriptId = "s-ok",
+ Name = "ok",
+ SourceCode = "return ctx.GetTag(\"A.X\").Value;",
+ SourceHash = "h1",
+ };
+ var goodAlarm = new ScriptedAlarm
+ {
+ ScriptedAlarmId = "al-ok",
+ EquipmentId = "eq-1",
+ Name = "Ok",
+ AlarmType = "AlarmCondition",
+ Severity = 500,
+ MessageTemplate = "ok",
+ PredicateScriptId = "s-ok",
+ HistorizeToAveva = true,
+ Retain = true,
+ Enabled = true,
+ };
+ var orphanAlarm = new ScriptedAlarm
+ {
+ ScriptedAlarmId = "al-orphan",
+ EquipmentId = "eq-2",
+ Name = "Orphan",
+ AlarmType = "AlarmCondition",
+ Severity = 500,
+ MessageTemplate = "orphan",
+ PredicateScriptId = "s-does-not-exist",
+ HistorizeToAveva = true,
+ Retain = true,
+ Enabled = true,
+ };
+
+ var composed = Phase7Composer.Compose(
+ Array.Empty(), Array.Empty(), Array.Empty(),
+ Array.Empty(), new[] { goodAlarm, orphanAlarm },
+ Array.Empty(), Array.Empty(),
+ scripts: new[] { goodScript });
+
+ var blob = BlobOf(new
+ {
+ ScriptedAlarms = new[] { ToSnapshot(goodAlarm), ToSnapshot(orphanAlarm) },
+ Scripts = new[] { new { ScriptId = goodScript.ScriptId, SourceCode = goodScript.SourceCode } },
+ });
+
+ var decoded = DeploymentArtifact.ParseComposition(blob);
+
+ // The orphan is dropped on both sides; the survivor's plan is byte-identical.
+ var composedPlan = composed.EquipmentScriptedAlarms.ShouldHaveSingleItem();
+ composedPlan.ScriptedAlarmId.ShouldBe("al-ok");
+ var decodedPlan = decoded.EquipmentScriptedAlarms.ShouldHaveSingleItem();
+ decodedPlan.ScriptedAlarmId.ShouldBe("al-ok");
+ decodedPlan.ShouldBe(composedPlan);
+ }
+
+ // The full Pascal-case snapshot a ScriptedAlarm EF entity serialises to (matches ConfigComposer).
+ private static object ToSnapshot(ScriptedAlarm a) => new
+ {
+ a.ScriptedAlarmId,
+ a.EquipmentId,
+ a.Name,
+ a.AlarmType,
+ a.Severity,
+ a.MessageTemplate,
+ a.PredicateScriptId,
+ a.HistorizeToAveva,
+ a.Retain,
+ a.Enabled,
+ };
+
+ private static object NewAlarmSnapshot(string id, string equipmentId, string scriptId) => new
+ {
+ ScriptedAlarmId = id,
+ EquipmentId = equipmentId,
+ Name = id,
+ AlarmType = "AlarmCondition",
+ Severity = 500,
+ MessageTemplate = $"{id} alarm",
+ PredicateScriptId = scriptId,
+ HistorizeToAveva = true,
+ Retain = true,
+ Enabled = true,
+ };
+}