feat(cli): typed setpoint flags for template alarm add (serializes trigger-config JSON)

This commit is contained in:
Joseph Doherty
2026-06-07 10:02:51 -04:00
parent 9d7e69056a
commit bbc3804d07
2 changed files with 122 additions and 2 deletions
@@ -0,0 +1,80 @@
using System.Text;
using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.CLI;
/// <summary>
/// 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).
/// </summary>
internal static class AlarmTriggerConfigJson
{
/// <summary>
/// Builds the trigger-config JSON for <paramref name="triggerType"/> 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.
/// </summary>
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",
};
}
@@ -226,6 +226,22 @@ public static class TemplateCommands
var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
// Typed setpoint flags (alternative to raw --trigger-config; raw wins when both supplied).
var attributeOption = new Option<string?>("--attribute") { Description = "Attribute name the trigger watches (all trigger types except Expression)" };
var matchValueOption = new Option<string?>("--match-value") { Description = "ValueMatch: value to compare against" };
var notEqualsOption = new Option<bool>("--not-equals") { Description = "ValueMatch: match when the value is NOT equal (emits !=)" };
notEqualsOption.DefaultValueFactory = _ => false;
var minOption = new Option<double?>("--min") { Description = "RangeViolation: minimum allowed value" };
var maxOption = new Option<double?>("--max") { Description = "RangeViolation: maximum allowed value" };
var thresholdOption = new Option<double?>("--threshold-per-second") { Description = "RateOfChange: rate threshold per second" };
var windowOption = new Option<double?>("--window-seconds") { Description = "RateOfChange: sliding window in seconds" };
var directionOption = new Option<string?>("--direction") { Description = "RateOfChange: direction (rising|falling|either)" };
var loLoOption = new Option<double?>("--lolo") { Description = "HiLo: low-low setpoint" };
var loOption = new Option<double?>("--lo") { Description = "HiLo: low setpoint" };
var hiOption = new Option<double?>("--hi") { Description = "HiLo: high setpoint" };
var hiHiOption = new Option<double?>("--hihi") { Description = "HiLo: high-high setpoint" };
var expressionOption = new Option<string?>("--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);