refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Round-trip codec for the alarm trigger configuration JSON used by both
|
||||
/// <see cref="AlarmTriggerEditor"/> (UI editing) and AlarmActor (runtime
|
||||
/// evaluation). The serialized shape per trigger type:
|
||||
/// ValueMatch { attributeName, matchValue } ("!=X" prefix = not equals)
|
||||
/// RangeViolation { attributeName, min, max }
|
||||
/// RateOfChange { attributeName, thresholdPerSecond, windowSeconds, direction }
|
||||
/// HiLo { attributeName, loLo, lo, hi, hiHi,
|
||||
/// loLoPriority, loPriority, hiPriority, hiHiPriority }
|
||||
/// Expression { expression }
|
||||
///
|
||||
/// All HiLo setpoints and per-setpoint priorities are optional — any subset
|
||||
/// is valid (e.g., only Hi/HiHi configured for over-temperature protection).
|
||||
///
|
||||
/// Parsing also accepts legacy aliases the runtime used to consume
|
||||
/// (<c>attribute</c>, <c>value</c>, <c>low</c>, <c>high</c>) so older configs
|
||||
/// survive a round-trip through the editor.
|
||||
/// </summary>
|
||||
internal static class AlarmTriggerConfigCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a trigger configuration JSON in the context of the given trigger
|
||||
/// type. Returns a model with default values on null/empty/malformed input
|
||||
/// or for missing keys — never throws.
|
||||
/// </summary>
|
||||
/// <param name="json">The trigger configuration JSON string, or null/empty for defaults.</param>
|
||||
/// <param name="type">The alarm trigger type that determines which properties to extract.</param>
|
||||
/// <returns>A populated AlarmTriggerModel with default values for missing fields.</returns>
|
||||
internal static AlarmTriggerModel Parse(string? json, AlarmTriggerType type)
|
||||
{
|
||||
var model = new AlarmTriggerModel();
|
||||
if (string.IsNullOrWhiteSpace(json)) return model;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
model.AttributeName =
|
||||
root.TryGetProperty("attributeName", out var a) ? a.GetString()
|
||||
: root.TryGetProperty("attribute", out var a2) ? a2.GetString()
|
||||
: null;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case AlarmTriggerType.ValueMatch:
|
||||
{
|
||||
var raw = root.TryGetProperty("matchValue", out var mv) ? mv.GetString()
|
||||
: root.TryGetProperty("value", out var mv2) ? mv2.GetString()
|
||||
: null;
|
||||
if (raw != null && raw.StartsWith("!=", StringComparison.Ordinal))
|
||||
{
|
||||
model.NotEquals = true;
|
||||
model.MatchValue = raw[2..];
|
||||
}
|
||||
else
|
||||
{
|
||||
model.MatchValue = raw;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AlarmTriggerType.RangeViolation:
|
||||
model.Min = TryReadDouble(root, "min") ?? TryReadDouble(root, "low");
|
||||
model.Max = TryReadDouble(root, "max") ?? TryReadDouble(root, "high");
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.RateOfChange:
|
||||
model.ThresholdPerSecond = TryReadDouble(root, "thresholdPerSecond");
|
||||
model.WindowSeconds = TryReadDouble(root, "windowSeconds");
|
||||
var dir = root.TryGetProperty("direction", out var d) ? d.GetString() : null;
|
||||
model.Direction = NormalizeDirection(dir);
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.HiLo:
|
||||
model.LoLo = TryReadDouble(root, "loLo");
|
||||
model.Lo = TryReadDouble(root, "lo");
|
||||
model.Hi = TryReadDouble(root, "hi");
|
||||
model.HiHi = TryReadDouble(root, "hiHi");
|
||||
model.LoLoPriority = TryReadInt(root, "loLoPriority");
|
||||
model.LoPriority = TryReadInt(root, "loPriority");
|
||||
model.HiPriority = TryReadInt(root, "hiPriority");
|
||||
model.HiHiPriority = TryReadInt(root, "hiHiPriority");
|
||||
model.LoLoDeadband = TryReadDouble(root, "loLoDeadband");
|
||||
model.LoDeadband = TryReadDouble(root, "loDeadband");
|
||||
model.HiDeadband = TryReadDouble(root, "hiDeadband");
|
||||
model.HiHiDeadband = TryReadDouble(root, "hiHiDeadband");
|
||||
model.LoLoMessage = TryReadString(root, "loLoMessage");
|
||||
model.LoMessage = TryReadString(root, "loMessage");
|
||||
model.HiMessage = TryReadString(root, "hiMessage");
|
||||
model.HiHiMessage = TryReadString(root, "hiHiMessage");
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.Expression:
|
||||
model.Expression = TryReadString(root, "expression");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Malformed JSON — fall through with default model.
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the model to the JSON shape AlarmActor.ParseEvalConfig
|
||||
/// expects. Writes <c>attributeName</c> (canonical key) for the
|
||||
/// attribute-bound trigger types and only the keys relevant to the
|
||||
/// current trigger type. <c>Expression</c> is not bound to a single
|
||||
/// attribute, so <c>attributeName</c> is omitted for it.
|
||||
/// </summary>
|
||||
/// <param name="model">The AlarmTriggerModel to serialize.</param>
|
||||
/// <param name="type">The alarm trigger type determining which properties to serialize.</param>
|
||||
/// <returns>The serialized JSON representation of the model.</returns>
|
||||
internal static string Serialize(AlarmTriggerModel model, AlarmTriggerType type)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var w = new Utf8JsonWriter(stream))
|
||||
{
|
||||
w.WriteStartObject();
|
||||
if (type != AlarmTriggerType.Expression)
|
||||
w.WriteString("attributeName", model.AttributeName ?? "");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case AlarmTriggerType.ValueMatch:
|
||||
var mv = model.MatchValue ?? "";
|
||||
if (model.NotEquals) mv = "!=" + mv;
|
||||
w.WriteString("matchValue", mv);
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.RangeViolation:
|
||||
if (model.Min.HasValue) w.WriteNumber("min", model.Min.Value);
|
||||
if (model.Max.HasValue) w.WriteNumber("max", model.Max.Value);
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.RateOfChange:
|
||||
if (model.ThresholdPerSecond.HasValue)
|
||||
w.WriteNumber("thresholdPerSecond", model.ThresholdPerSecond.Value);
|
||||
if (model.WindowSeconds.HasValue)
|
||||
w.WriteNumber("windowSeconds", model.WindowSeconds.Value);
|
||||
w.WriteString("direction", model.Direction);
|
||||
break;
|
||||
|
||||
case AlarmTriggerType.HiLo:
|
||||
if (model.LoLo.HasValue) w.WriteNumber("loLo", model.LoLo.Value);
|
||||
if (model.Lo.HasValue) w.WriteNumber("lo", model.Lo.Value);
|
||||
if (model.Hi.HasValue) w.WriteNumber("hi", model.Hi.Value);
|
||||
if (model.HiHi.HasValue) w.WriteNumber("hiHi", model.HiHi.Value);
|
||||
if (model.LoLoPriority.HasValue) w.WriteNumber("loLoPriority", model.LoLoPriority.Value);
|
||||
if (model.LoPriority.HasValue) w.WriteNumber("loPriority", model.LoPriority.Value);
|
||||
if (model.HiPriority.HasValue) w.WriteNumber("hiPriority", model.HiPriority.Value);
|
||||
if (model.HiHiPriority.HasValue) w.WriteNumber("hiHiPriority", model.HiHiPriority.Value);
|
||||
if (model.LoLoDeadband.HasValue) w.WriteNumber("loLoDeadband", model.LoLoDeadband.Value);
|
||||
if (model.LoDeadband.HasValue) w.WriteNumber("loDeadband", model.LoDeadband.Value);
|
||||
if (model.HiDeadband.HasValue) w.WriteNumber("hiDeadband", model.HiDeadband.Value);
|
||||
if (model.HiHiDeadband.HasValue) w.WriteNumber("hiHiDeadband", model.HiHiDeadband.Value);
|
||||
if (!string.IsNullOrEmpty(model.LoLoMessage)) w.WriteString("loLoMessage", model.LoLoMessage);
|
||||
if (!string.IsNullOrEmpty(model.LoMessage)) w.WriteString("loMessage", model.LoMessage);
|
||||
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();
|
||||
}
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a direction string to one of: rising, falling, or either.
|
||||
/// </summary>
|
||||
/// <param name="raw">The raw direction string to normalize.</param>
|
||||
/// <returns>Normalized direction: "rising", "falling", or "either".</returns>
|
||||
internal static string NormalizeDirection(string? raw) => raw?.ToLowerInvariant() switch
|
||||
{
|
||||
"rising" or "up" or "positive" => "rising",
|
||||
"falling" or "down" or "negative" => "falling",
|
||||
_ => "either"
|
||||
};
|
||||
|
||||
private static double? TryReadDouble(JsonElement el, string name)
|
||||
{
|
||||
if (!el.TryGetProperty(name, out var p)) return null;
|
||||
return p.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number => p.GetDouble(),
|
||||
JsonValueKind.String when double.TryParse(p.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v) => v,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static int? TryReadInt(JsonElement el, string name)
|
||||
{
|
||||
if (!el.TryGetProperty(name, out var p)) return null;
|
||||
return p.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when p.TryGetInt32(out var i) => i,
|
||||
JsonValueKind.Number => (int)p.GetDouble(),
|
||||
JsonValueKind.String when int.TryParse(p.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) => v,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string? TryReadString(JsonElement el, string name)
|
||||
{
|
||||
if (!el.TryGetProperty(name, out var p)) return null;
|
||||
return p.ValueKind == JsonValueKind.String ? p.GetString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AlarmTriggerModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name bound to this trigger.
|
||||
/// </summary>
|
||||
public string? AttributeName { get; set; }
|
||||
|
||||
// ValueMatch
|
||||
/// <summary>
|
||||
/// The value to match against the attribute for ValueMatch triggers.
|
||||
/// </summary>
|
||||
public string? MatchValue { get; set; }
|
||||
/// <summary>
|
||||
/// Indicates whether the match should be inverted (not equal) for ValueMatch triggers.
|
||||
/// </summary>
|
||||
public bool NotEquals { get; set; }
|
||||
|
||||
// RangeViolation
|
||||
/// <summary>
|
||||
/// The minimum threshold for RangeViolation triggers.
|
||||
/// </summary>
|
||||
public double? Min { get; set; }
|
||||
/// <summary>
|
||||
/// The maximum threshold for RangeViolation triggers.
|
||||
/// </summary>
|
||||
public double? Max { get; set; }
|
||||
|
||||
// RateOfChange
|
||||
/// <summary>
|
||||
/// The threshold per second for RateOfChange triggers.
|
||||
/// </summary>
|
||||
public double? ThresholdPerSecond { get; set; }
|
||||
/// <summary>
|
||||
/// The time window in seconds for RateOfChange rate calculation.
|
||||
/// </summary>
|
||||
public double? WindowSeconds { get; set; }
|
||||
/// <summary>
|
||||
/// The direction of change: "rising", "falling", or "either" for RateOfChange triggers.
|
||||
/// </summary>
|
||||
public string Direction { get; set; } = "either";
|
||||
|
||||
// HiLo — any subset of setpoints may be set; per-setpoint priorities
|
||||
// override the alarm-level priority for that band.
|
||||
/// <summary>
|
||||
/// The low-low setpoint for HiLo triggers.
|
||||
/// </summary>
|
||||
public double? LoLo { get; set; }
|
||||
/// <summary>
|
||||
/// The low setpoint for HiLo triggers.
|
||||
/// </summary>
|
||||
public double? Lo { get; set; }
|
||||
/// <summary>
|
||||
/// The high setpoint for HiLo triggers.
|
||||
/// </summary>
|
||||
public double? Hi { get; set; }
|
||||
/// <summary>
|
||||
/// The high-high setpoint for HiLo triggers.
|
||||
/// </summary>
|
||||
public double? HiHi { get; set; }
|
||||
/// <summary>
|
||||
/// The priority for low-low alarm state.
|
||||
/// </summary>
|
||||
public int? LoLoPriority { get; set; }
|
||||
/// <summary>
|
||||
/// The priority for low alarm state.
|
||||
/// </summary>
|
||||
public int? LoPriority { get; set; }
|
||||
/// <summary>
|
||||
/// The priority for high alarm state.
|
||||
/// </summary>
|
||||
public int? HiPriority { get; set; }
|
||||
/// <summary>
|
||||
/// The priority for high-high alarm state.
|
||||
/// </summary>
|
||||
public int? HiHiPriority { get; set; }
|
||||
|
||||
// Hysteresis: optional deactivation deadband per setpoint. Once at the
|
||||
// band, the setpoint threshold is relaxed by this amount before the alarm
|
||||
// de-escalates. Prevents flapping when the value hovers at the boundary.
|
||||
/// <summary>
|
||||
/// The deadband for low-low alarm de-escalation.
|
||||
/// </summary>
|
||||
public double? LoLoDeadband { get; set; }
|
||||
/// <summary>
|
||||
/// The deadband for low alarm de-escalation.
|
||||
/// </summary>
|
||||
public double? LoDeadband { get; set; }
|
||||
/// <summary>
|
||||
/// The deadband for high alarm de-escalation.
|
||||
/// </summary>
|
||||
public double? HiDeadband { get; set; }
|
||||
/// <summary>
|
||||
/// The deadband for high-high alarm de-escalation.
|
||||
/// </summary>
|
||||
public double? HiHiDeadband { get; set; }
|
||||
|
||||
// Per-band operator message. Optional; surfaces on AlarmStateChanged.Message
|
||||
// and may be used by notification routing or operator displays.
|
||||
/// <summary>
|
||||
/// The operator message for low-low alarm state.
|
||||
/// </summary>
|
||||
public string? LoLoMessage { get; set; }
|
||||
/// <summary>
|
||||
/// The operator message for low alarm state.
|
||||
/// </summary>
|
||||
public string? LoMessage { get; set; }
|
||||
/// <summary>
|
||||
/// The operator message for high alarm state.
|
||||
/// </summary>
|
||||
public string? HiMessage { get; set; }
|
||||
/// <summary>
|
||||
/// The operator message for high-high alarm state.
|
||||
/// </summary>
|
||||
public string? HiHiMessage { get; set; }
|
||||
|
||||
// Expression — boolean C# expression evaluated on attribute updates.
|
||||
/// <summary>
|
||||
/// The boolean C# expression to evaluate for Expression triggers.
|
||||
/// </summary>
|
||||
public string? Expression { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user