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
}