using System.Security.Cryptography; using System.Text; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; /// /// The deterministic alarm-event id and numeric severity the SQL historian backends store, so the store-and-forward /// drain's at-least-once redelivery (and both pair nodes writing the same event) converge on one row. /// public static class AlarmEventIds { /// /// Lowercase-hex SHA-256 over length-prefixed UTF-8 fields (little-endian int32 byte length, -1 for null): /// EquipmentPath, AlarmId, EventKind, TimestampUtc.Ticks (decimal), User, Comment, Message. Length-prefixing keeps /// field boundaries unambiguous ("a|b"+"c" ≠ "a"+"b|c"). /// /// The event. /// 64 hex characters. public static string ComputeEventId(AlarmHistorianEvent e) { using var ms = new MemoryStream(); Field(ms, e.EquipmentPath); Field(ms, e.AlarmId); Field(ms, e.EventKind); Field(ms, e.TimestampUtc.Ticks.ToString(System.Globalization.CultureInfo.InvariantCulture)); Field(ms, e.User); Field(ms, e.Comment); Field(ms, e.Message); return Convert.ToHexStringLower(SHA256.HashData(ms.ToArray())); } /// The numeric Part 9 priority stored for a severity (same values the gateway path sends). /// Alarm severity. /// 200 / 500 / 700 / 900; 500 for anything else. public static int ToNumericSeverity(AlarmSeverity s) => s switch { AlarmSeverity.Low => 200, AlarmSeverity.Medium => 500, AlarmSeverity.High => 700, AlarmSeverity.Critical => 900, _ => 500, }; private static void Field(MemoryStream ms, string? value) { Span len = stackalloc byte[4]; if (value is null) { System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(len, -1); ms.Write(len); return; } var bytes = Encoding.UTF8.GetBytes(value); System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(len, bytes.Length); ms.Write(len); ms.Write(bytes); } }