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": // Only the four setpoints are exposed as flags. The codec also accepts // per-setpoint priorities/deadbands/messages — intentionally omitted here; // use raw --trigger-config for those (see the YAGNI scope guard in the plan). 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", }; }