diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs
index 9356abc2..61a89979 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ValidationResult.cs
@@ -80,5 +80,6 @@ public enum ValidationCategory
OnTriggerScriptNotFound,
CrossCallViolation,
MissingMetadata,
- ConnectionConfig
+ ConnectionConfig,
+ NativeAlarmSourceInvalid
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs
index cdb6db45..c65bff3e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs
@@ -27,7 +27,8 @@ public class SemanticValidator
/// Shared scripts available for CallShared references.
public ValidationResult Validate(
FlattenedConfiguration configuration,
- IReadOnlyList? sharedScripts = null)
+ IReadOnlyList? sharedScripts = null,
+ IReadOnlySet? alarmCapableConnectionNames = null)
{
var errors = new List();
var warnings = new List();
@@ -215,6 +216,33 @@ public class SemanticValidator
}
}
+ // Native alarm source bindings: connection + source reference must be
+ // present, and (when the alarm-capable connection set is supplied) the
+ // connection must resolve to an alarm-capable site data connection.
+ foreach (var nativeSource in configuration.NativeAlarmSources)
+ {
+ if (string.IsNullOrWhiteSpace(nativeSource.SourceReference))
+ {
+ errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid,
+ $"Native alarm source '{nativeSource.CanonicalName}' has an empty source reference.",
+ nativeSource.CanonicalName));
+ }
+
+ if (string.IsNullOrWhiteSpace(nativeSource.ConnectionName))
+ {
+ errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid,
+ $"Native alarm source '{nativeSource.CanonicalName}' has no data connection.",
+ nativeSource.CanonicalName));
+ }
+ else if (alarmCapableConnectionNames is not null &&
+ !alarmCapableConnectionNames.Contains(nativeSource.ConnectionName))
+ {
+ errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid,
+ $"Native alarm source '{nativeSource.CanonicalName}' references connection '{nativeSource.ConnectionName}' which is not an alarm-capable data connection on this site.",
+ nativeSource.CanonicalName));
+ }
+ }
+
return new ValidationResult { Errors = errors, Warnings = warnings };
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
index 14c9f4f0..0c82b9c3 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
@@ -7,6 +7,42 @@ public class SemanticValidatorTests
{
private readonly SemanticValidator _sut = new();
+ [Fact]
+ public void Validate_NativeAlarmSource_UnknownConnection_ReturnsError()
+ {
+ var cfg = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "Ghost", SourceReference = "x" }]
+ };
+ var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" });
+ Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
+ }
+
+ [Fact]
+ public void Validate_NativeAlarmSource_EmptySourceRef_ReturnsError()
+ {
+ var cfg = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "" }]
+ };
+ var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" });
+ Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
+ }
+
+ [Fact]
+ public void Validate_NativeAlarmSource_ValidBinding_NoError()
+ {
+ var cfg = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ NativeAlarmSources = [new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "ns=2;s=T1" }]
+ };
+ var result = _sut.Validate(cfg, alarmCapableConnectionNames: new HashSet { "RealConn" });
+ Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
+ }
+
[Fact]
public void Validate_CallScriptTargetNotFound_ReturnsError()
{