diff --git a/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs b/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs index 60b305d..d281bd1 100644 --- a/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs +++ b/src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs @@ -70,12 +70,29 @@ public static class OpcUaEndpointConfigSerializer ["KeepAliveCount"] = config.KeepAliveCount.ToString(), ["LifetimeCount"] = config.LifetimeCount.ToString(), ["MaxNotificationsPerPublish"] = config.MaxNotificationsPerPublish.ToString(), + ["DiscardOldest"] = config.DiscardOldest.ToString(), + ["SubscriptionPriority"] = config.SubscriptionPriority.ToString(), + ["SubscriptionDisplayName"] = config.SubscriptionDisplayName, + ["TimestampsToReturn"] = config.TimestampsToReturn.ToString(), }; if (config.Heartbeat is { } hb) { dict["HeartbeatTagPath"] = hb.TagPath; dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString(); } + if (config.UserIdentity is { } ui) + { + dict["UserIdentity.TokenType"] = ui.TokenType.ToString(); + dict["UserIdentity.Username"] = ui.Username; + dict["UserIdentity.Password"] = ui.Password; + dict["UserIdentity.CertificatePath"] = ui.CertificatePath; + dict["UserIdentity.CertificatePassword"] = ui.CertificatePassword; + } + if (config.Deadband is { } db) + { + dict["Deadband.Type"] = db.Type.ToString(); + dict["Deadband.Value"] = db.Value.ToString(System.Globalization.CultureInfo.InvariantCulture); + } return dict; } @@ -105,6 +122,16 @@ public static class OpcUaEndpointConfigSerializer TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v); TryAssignInt(dict, "MaxNotificationsPerPublish", v => c.MaxNotificationsPerPublish = v); + if (dict.TryGetValue("DiscardOldest", out var doStr) && bool.TryParse(doStr, out var doVal)) + c.DiscardOldest = doVal; + if (dict.TryGetValue("SubscriptionPriority", out var spStr) && byte.TryParse(spStr, out var spVal)) + c.SubscriptionPriority = spVal; + if (dict.TryGetValue("SubscriptionDisplayName", out var sdnStr) && !string.IsNullOrWhiteSpace(sdnStr)) + c.SubscriptionDisplayName = sdnStr; + if (dict.TryGetValue("TimestampsToReturn", out var ttrStr) + && Enum.TryParse(ttrStr, ignoreCase: true, out var ttr)) + c.TimestampsToReturn = ttr; + if (dict.TryGetValue("HeartbeatTagPath", out var hbPath) && !string.IsNullOrWhiteSpace(hbPath)) { @@ -113,6 +140,28 @@ public static class OpcUaEndpointConfigSerializer c.Heartbeat = hb; } + if (dict.TryGetValue("UserIdentity.TokenType", out var uiTt) + && Enum.TryParse(uiTt, ignoreCase: true, out var tokenType)) + { + var ui = new OpcUaUserIdentityConfig { TokenType = tokenType }; + if (dict.TryGetValue("UserIdentity.Username", out var u)) ui.Username = u; + if (dict.TryGetValue("UserIdentity.Password", out var p)) ui.Password = p; + if (dict.TryGetValue("UserIdentity.CertificatePath", out var cp)) ui.CertificatePath = cp; + if (dict.TryGetValue("UserIdentity.CertificatePassword", out var cpw)) ui.CertificatePassword = cpw; + c.UserIdentity = ui; + } + + if (dict.TryGetValue("Deadband.Type", out var dbT) + && Enum.TryParse(dbT, ignoreCase: true, out var dbType)) + { + var db = new OpcUaDeadbandConfig { Type = dbType }; + if (dict.TryGetValue("Deadband.Value", out var dbV) + && double.TryParse(dbV, System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var dbVal)) + db.Value = dbVal; + c.Deadband = db; + } + return c; } diff --git a/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs b/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs index d59777e..fc0e494 100644 --- a/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs +++ b/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs @@ -40,6 +40,10 @@ public static class OpcUaEndpointConfigValidator if (config.MaxNotificationsPerPublish < 1) errors.Add(Err("MaxNotificationsPerPublish", "Must be ≥ 1.")); + if (string.IsNullOrWhiteSpace(config.SubscriptionDisplayName)) + errors.Add(Err("SubscriptionDisplayName", + "Subscription display name is required.")); + if (config.Heartbeat is { } hb) { if (string.IsNullOrWhiteSpace(hb.TagPath)) @@ -49,6 +53,21 @@ public static class OpcUaEndpointConfigValidator errors.Add(Err("Heartbeat.MaxSilenceSeconds", "Must be > 0.")); } + if (config.UserIdentity is { } ui) + { + if (ui.TokenType == OpcUaUserTokenType.UsernamePassword + && string.IsNullOrWhiteSpace(ui.Username)) + errors.Add(Err("UserIdentity.Username", + "Username is required when token type is UsernamePassword.")); + if (ui.TokenType == OpcUaUserTokenType.X509Certificate + && string.IsNullOrWhiteSpace(ui.CertificatePath)) + errors.Add(Err("UserIdentity.CertificatePath", + "Certificate path is required when token type is X509Certificate.")); + } + + if (config.Deadband is { } db && db.Value <= 0) + errors.Add(Err("Deadband.Value", "Must be > 0.")); + return errors.Count == 0 ? ValidationResult.Success() : ValidationResult.FromErrors(errors.ToArray());