From 199cdbe7982c107e7a17de979d6a1a061d55fd89 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 16 May 2026 05:27:33 -0400 Subject: [PATCH] feat(triggers): add Expression to the script & alarm trigger codecs --- .../Shared/AlarmTriggerConfigCodec.cs | 20 ++++++++++++++++--- .../Shared/ScriptTriggerConfigCodec.cs | 15 +++++++++++++- .../Types/Enums/AlarmTriggerType.cs | 8 +++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs index fca75ef..48ac61d 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs +++ b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs @@ -93,6 +93,10 @@ internal static class AlarmTriggerConfigCodec model.HiMessage = TryReadString(root, "hiMessage"); model.HiHiMessage = TryReadString(root, "hiHiMessage"); break; + + case AlarmTriggerType.Expression: + model.Expression = TryReadString(root, "expression"); + break; } } catch (JsonException) @@ -105,8 +109,10 @@ internal static class AlarmTriggerConfigCodec /// /// Serializes the model to the JSON shape AlarmActor.ParseEvalConfig - /// expects. Always writes attributeName (canonical key) and only - /// the keys relevant to the current trigger type. + /// expects. Writes attributeName (canonical key) for the + /// attribute-bound trigger types and only the keys relevant to the + /// current trigger type. Expression is not bound to a single + /// attribute, so attributeName is omitted for it. /// internal static string Serialize(AlarmTriggerModel model, AlarmTriggerType type) { @@ -114,7 +120,8 @@ internal static class AlarmTriggerConfigCodec using (var w = new Utf8JsonWriter(stream)) { w.WriteStartObject(); - w.WriteString("attributeName", model.AttributeName ?? ""); + if (type != AlarmTriggerType.Expression) + w.WriteString("attributeName", model.AttributeName ?? ""); switch (type) { @@ -155,6 +162,10 @@ internal static class AlarmTriggerConfigCodec if (!string.IsNullOrEmpty(model.HiMessage)) w.WriteString("hiMessage", model.HiMessage); if (!string.IsNullOrEmpty(model.HiHiMessage)) w.WriteString("hiHiMessage", model.HiHiMessage); break; + + case AlarmTriggerType.Expression: + w.WriteString("expression", model.Expression ?? ""); + break; } w.WriteEndObject(); @@ -241,4 +252,7 @@ internal sealed class AlarmTriggerModel public string? LoMessage { get; set; } public string? HiMessage { get; set; } public string? HiHiMessage { get; set; } + + // Expression — boolean C# expression evaluated on attribute updates. + public string? Expression { get; set; } } diff --git a/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs b/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs index 43cb0fd..fa4d4c9 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs +++ b/src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs @@ -10,7 +10,7 @@ namespace ScadaLink.CentralUI.Components.Shared; /// trigger; is a stored trigger-type string the runtime /// does not recognize (preserved as-is by the editor). /// -internal enum ScriptTriggerKind { None, Interval, ValueChange, Conditional, Call, Unknown } +internal enum ScriptTriggerKind { None, Interval, ValueChange, Conditional, Call, Expression, Unknown } /// A script's trigger as the editor emits it: a type string + config JSON. public sealed record ScriptTriggerValue(string? TriggerType, string? Config); @@ -29,6 +29,9 @@ internal sealed class ScriptTriggerModel /// Comparison threshold (Conditional). public double? Threshold { get; set; } + + /// Boolean C# expression (Expression). + public string? Expression { get; set; } } /// @@ -59,6 +62,7 @@ internal static class ScriptTriggerConfigCodec "valuechange" => ScriptTriggerKind.ValueChange, "conditional" => ScriptTriggerKind.Conditional, "call" => ScriptTriggerKind.Call, + "expression" => ScriptTriggerKind.Expression, _ => ScriptTriggerKind.Unknown }; } @@ -70,6 +74,7 @@ internal static class ScriptTriggerConfigCodec ScriptTriggerKind.ValueChange => "ValueChange", ScriptTriggerKind.Conditional => "Conditional", ScriptTriggerKind.Call => "Call", + ScriptTriggerKind.Expression => "Expression", _ => null }; @@ -104,6 +109,10 @@ internal static class ScriptTriggerConfigCodec model.Operator = NormalizeOperator(op); model.Threshold = TryReadDouble(root, "threshold") ?? TryReadDouble(root, "value"); break; + + case ScriptTriggerKind.Expression: + model.Expression = root.TryGetProperty("expression", out var e) ? e.GetString() : null; + break; } } catch (JsonException) @@ -144,6 +153,10 @@ internal static class ScriptTriggerConfigCodec w.WriteNumber("threshold", model.Threshold.Value); break; + case ScriptTriggerKind.Expression: + w.WriteString("expression", model.Expression ?? ""); + break; + // Call → empty object. } w.WriteEndObject(); diff --git a/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs index d497c09..31c3aa6 100644 --- a/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs +++ b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs @@ -11,5 +11,11 @@ public enum AlarmTriggerType /// may carry its own priority; transitions between levels emit a fresh /// AlarmStateChanged with the corresponding . /// - HiLo + HiLo, + + /// + /// Read-only boolean C# expression evaluated on attribute updates. The + /// trigger fires when the expression evaluates to true. + /// + Expression }