diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/AlarmTriggerConfigJson.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/AlarmTriggerConfigJson.cs
new file mode 100644
index 00000000..f451a42f
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CLI/AlarmTriggerConfigJson.cs
@@ -0,0 +1,80 @@
+using System.Text;
+using System.Text.Json;
+
+namespace ZB.MOM.WW.ScadaBridge.CLI;
+
+///
+/// Serializes typed alarm-setpoint CLI flags into the trigger-config JSON the
+/// server expects. Key names MUST stay in lockstep with the canonical codec at
+/// src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs
+/// (that codec is internal to CentralUI, so this is a deliberate CLI-side mirror;
+/// the round-trip test verifies the JSON against the live server — the real contract).
+///
+internal static class AlarmTriggerConfigJson
+{
+ ///
+ /// Builds the trigger-config JSON for from the typed
+ /// flags, or returns null when none are supplied (so the alarm is created without a
+ /// trigger config). Unknown/blank trigger types yield null.
+ ///
+ internal static string? Build(
+ string triggerType, string? attribute,
+ string? matchValue, bool notEquals,
+ double? min, double? max,
+ double? thresholdPerSecond, double? windowSeconds, string? direction,
+ double? loLo, double? lo, double? hi, double? hiHi,
+ string? expression)
+ {
+ var type = triggerType?.Trim();
+ var anyTyped = attribute is not null || matchValue is not null || notEquals
+ || min.HasValue || max.HasValue || thresholdPerSecond.HasValue || windowSeconds.HasValue
+ || direction is not null || loLo.HasValue || lo.HasValue || hi.HasValue || hiHi.HasValue
+ || expression is not null;
+ if (!anyTyped) return null;
+
+ using var stream = new MemoryStream();
+ using (var w = new Utf8JsonWriter(stream))
+ {
+ w.WriteStartObject();
+ if (!string.Equals(type, "Expression", StringComparison.OrdinalIgnoreCase))
+ w.WriteString("attributeName", attribute ?? "");
+
+ switch (type?.ToLowerInvariant())
+ {
+ case "valuematch":
+ var mv = matchValue ?? "";
+ if (notEquals) mv = "!=" + mv;
+ w.WriteString("matchValue", mv);
+ break;
+ case "rangeviolation":
+ if (min.HasValue) w.WriteNumber("min", min.Value);
+ if (max.HasValue) w.WriteNumber("max", max.Value);
+ break;
+ case "rateofchange":
+ if (thresholdPerSecond.HasValue) w.WriteNumber("thresholdPerSecond", thresholdPerSecond.Value);
+ if (windowSeconds.HasValue) w.WriteNumber("windowSeconds", windowSeconds.Value);
+ w.WriteString("direction", NormalizeDirection(direction));
+ break;
+ case "hilo":
+ if (loLo.HasValue) w.WriteNumber("loLo", loLo.Value);
+ if (lo.HasValue) w.WriteNumber("lo", lo.Value);
+ if (hi.HasValue) w.WriteNumber("hi", hi.Value);
+ if (hiHi.HasValue) w.WriteNumber("hiHi", hiHi.Value);
+ break;
+ case "expression":
+ w.WriteString("expression", expression ?? "");
+ break;
+ }
+ w.WriteEndObject();
+ }
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ // Mirrors AlarmTriggerConfigCodec.NormalizeDirection.
+ private static string NormalizeDirection(string? raw) => raw?.ToLowerInvariant() switch
+ {
+ "rising" or "up" or "positive" => "rising",
+ "falling" or "down" or "negative" => "falling",
+ _ => "either",
+ };
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs
index 9d88a4fa..5201e225 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs
@@ -226,6 +226,22 @@ public static class TemplateCommands
var lockedOption = new Option("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
+ // Typed setpoint flags (alternative to raw --trigger-config; raw wins when both supplied).
+ var attributeOption = new Option("--attribute") { Description = "Attribute name the trigger watches (all trigger types except Expression)" };
+ var matchValueOption = new Option("--match-value") { Description = "ValueMatch: value to compare against" };
+ var notEqualsOption = new Option("--not-equals") { Description = "ValueMatch: match when the value is NOT equal (emits !=)" };
+ notEqualsOption.DefaultValueFactory = _ => false;
+ var minOption = new Option("--min") { Description = "RangeViolation: minimum allowed value" };
+ var maxOption = new Option("--max") { Description = "RangeViolation: maximum allowed value" };
+ var thresholdOption = new Option("--threshold-per-second") { Description = "RateOfChange: rate threshold per second" };
+ var windowOption = new Option("--window-seconds") { Description = "RateOfChange: sliding window in seconds" };
+ var directionOption = new Option("--direction") { Description = "RateOfChange: direction (rising|falling|either)" };
+ var loLoOption = new Option("--lolo") { Description = "HiLo: low-low setpoint" };
+ var loOption = new Option("--lo") { Description = "HiLo: low setpoint" };
+ var hiOption = new Option("--hi") { Description = "HiLo: high setpoint" };
+ var hiHiOption = new Option("--hihi") { Description = "HiLo: high-high setpoint" };
+ var expressionOption = new Option("--expression") { Description = "Expression: boolean trigger expression" };
+
var addCmd = new Command("add") { Description = "Add an alarm to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(nameOption);
@@ -234,17 +250,41 @@ public static class TemplateCommands
addCmd.Add(descOption);
addCmd.Add(triggerConfigOption);
addCmd.Add(lockedOption);
+ addCmd.Add(attributeOption);
+ addCmd.Add(matchValueOption);
+ addCmd.Add(notEqualsOption);
+ addCmd.Add(minOption);
+ addCmd.Add(maxOption);
+ addCmd.Add(thresholdOption);
+ addCmd.Add(windowOption);
+ addCmd.Add(directionOption);
+ addCmd.Add(loLoOption);
+ addCmd.Add(loOption);
+ addCmd.Add(hiOption);
+ addCmd.Add(hiHiOption);
+ addCmd.Add(expressionOption);
addCmd.SetAction(async (ParseResult result) =>
{
+ var triggerType = result.GetValue(triggerTypeOption)!;
+ var rawConfig = result.GetValue(triggerConfigOption);
+ var triggerConfig = rawConfig ?? AlarmTriggerConfigJson.Build(
+ triggerType,
+ result.GetValue(attributeOption),
+ result.GetValue(matchValueOption), result.GetValue(notEqualsOption),
+ result.GetValue(minOption), result.GetValue(maxOption),
+ result.GetValue(thresholdOption), result.GetValue(windowOption), result.GetValue(directionOption),
+ result.GetValue(loLoOption), result.GetValue(loOption), result.GetValue(hiOption), result.GetValue(hiHiOption),
+ result.GetValue(expressionOption));
+
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateAlarmCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
- result.GetValue(triggerTypeOption)!,
+ triggerType,
result.GetValue(priorityOption)!,
result.GetValue(descOption),
- result.GetValue(triggerConfigOption),
+ triggerConfig,
result.GetValue(lockedOption)));
});
group.Add(addCmd);