alarms: compose subtag reference from object's real Galaxy area for exact alarmmgr parity
This commit is contained in:
@@ -818,6 +818,16 @@ subtags (confirmed AVEVA `AlarmExtension` field names, verified against the live
|
|||||||
ZB Galaxy `attribute_definition` rows). The gateway re-runs discovery on its
|
ZB Galaxy `attribute_definition` rows). The gateway re-runs discovery on its
|
||||||
reconcile cadence and pushes an updated watch-list when the model changes.
|
reconcile cadence and pushes an updated watch-list when the model changes.
|
||||||
|
|
||||||
|
Each target's canonical `AlarmFullReference` is composed as
|
||||||
|
`Galaxy!{area}.{reference}` (literal `Galaxy` provider). The `{area}` is the
|
||||||
|
alarm object's **real Galaxy area** — discovered per object via
|
||||||
|
`gobject.area_gobject_id` (`GetAlarmAttributesAsync` projects it as `area_name`)
|
||||||
|
— so the synthesized reference's group matches exactly the area the native
|
||||||
|
alarmmgr (wnwrap) emits for the same alarm (e.g. `TestMachine_001` in `TestArea`
|
||||||
|
yields `Galaxy!TestArea.TestMachine_001.TestAlarm001`). The configured
|
||||||
|
`Discovery.Area` / `DefaultArea` is **only** the fallback for explicit
|
||||||
|
`IncludeAttributes` entries, which carry no discovered area.
|
||||||
|
|
||||||
### Subtag advise and `LmxSubtagAlarmSource`
|
### Subtag advise and `LmxSubtagAlarmSource`
|
||||||
|
|
||||||
`LmxSubtagAlarmSource` (implements `ISubtagAlarmSource`) owns a separate
|
`LmxSubtagAlarmSource` (implements `ISubtagAlarmSource`) owns a separate
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ native wnwrap alarm-manager provider and the subtag-monitoring fallback.
|
|||||||
| `MxGateway:Alarms:Fallback:FailbackProbeIntervalSeconds` | `30` | While in subtag mode, how often (in seconds) the monitor probes the wnwrap provider to detect recovery. Floored at 1. |
|
| `MxGateway:Alarms:Fallback:FailbackProbeIntervalSeconds` | `30` | While in subtag mode, how often (in seconds) the monitor probes the wnwrap provider to detect recovery. Floored at 1. |
|
||||||
| `MxGateway:Alarms:Fallback:FailbackStableProbes` | `3` | Number of consecutive clean wnwrap probes required before the monitor switches back to the alarm manager. Floored at 1. |
|
| `MxGateway:Alarms:Fallback:FailbackStableProbes` | `3` | Number of consecutive clean wnwrap probes required before the monitor switches back to the alarm manager. Floored at 1. |
|
||||||
| `MxGateway:Alarms:Fallback:Discovery:UseGalaxyRepository` | `true` | When `true`, the monitor queries the Galaxy Repository SQL database to build the subtag watch-list for the configured area. |
|
| `MxGateway:Alarms:Fallback:Discovery:UseGalaxyRepository` | `true` | When `true`, the monitor queries the Galaxy Repository SQL database to build the subtag watch-list for the configured area. |
|
||||||
| `MxGateway:Alarms:Fallback:Discovery:Area` | _(empty)_ | Galaxy area to scope the Repository query to. Falls back to `MxGateway:Alarms:DefaultArea` when empty. Ignored when `UseGalaxyRepository` is `false`. |
|
| `MxGateway:Alarms:Fallback:Discovery:Area` | _(empty)_ | Galaxy area to scope the Repository query to. Falls back to `MxGateway:Alarms:DefaultArea` when empty. Ignored when `UseGalaxyRepository` is `false`. This area is **not** used to compose a Repository-derived alarm's canonical `Galaxy!{area}.{reference}`: each discovered alarm uses its object's real Galaxy area (discovered via `gobject.area_gobject_id`), so the reference's group matches what the native alarmmgr emits. `Discovery:Area` / `DefaultArea` is used as the composition area only for explicit `IncludeAttributes` entries, which carry no discovered area. |
|
||||||
| `MxGateway:Alarms:Fallback:Discovery:IncludeAttributes` | _(empty)_ | Explicit MXAccess attribute paths to add to the subtag watch-list, supplementing (or replacing, when `UseGalaxyRepository` is `false`) the Repository-derived list. |
|
| `MxGateway:Alarms:Fallback:Discovery:IncludeAttributes` | _(empty)_ | Explicit MXAccess attribute paths to add to the subtag watch-list, supplementing (or replacing, when `UseGalaxyRepository` is `false`) the Repository-derived list. |
|
||||||
| `MxGateway:Alarms:Fallback:Discovery:ExcludeAttributes` | _(empty)_ | Attribute paths to remove from the Repository-derived watch-list. Ignored when `UseGalaxyRepository` is `false`. |
|
| `MxGateway:Alarms:Fallback:Discovery:ExcludeAttributes` | _(empty)_ | Attribute paths to remove from the Repository-derived watch-list. Ignored when `UseGalaxyRepository` is `false`. |
|
||||||
| `MxGateway:Alarms:Fallback:Subtags:Active` | `InAlarm` | Subtag name for the in-alarm boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
| `MxGateway:Alarms:Fallback:Subtags:Active` | `InAlarm` | Subtag name for the in-alarm boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ namespace ZB.MOM.WW.MxGateway.Server.Alarms;
|
|||||||
// NOTE: The exact subtag names and the canonical AlarmFullReference shape
|
// NOTE: The exact subtag names and the canonical AlarmFullReference shape
|
||||||
// ("Galaxy!{area}.{reference}") are validated against a live Galaxy in the
|
// ("Galaxy!{area}.{reference}") are validated against a live Galaxy in the
|
||||||
// Task 17 live smoke test. The config Subtags block exists precisely so these
|
// Task 17 live smoke test. The config Subtags block exists precisely so these
|
||||||
// names are not hard-coded here.
|
// names are not hard-coded here. The {area} is the alarm object's REAL Galaxy
|
||||||
|
// area discovered via gobject.area_gobject_id (the alarm group the native
|
||||||
|
// alarmmgr emits), giving exact reference parity with wnwrap. The configured
|
||||||
|
// Discovery.Area/DefaultArea is only the fallback for explicit IncludeAttributes
|
||||||
|
// entries, which carry no discovered area.
|
||||||
public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
||||||
{
|
{
|
||||||
private const string ProviderLiteral = "Galaxy";
|
private const string ProviderLiteral = "Galaxy";
|
||||||
@@ -43,9 +47,16 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|||||||
|
|
||||||
AlarmDiscoveryOptions discovery = options.Fallback.Discovery;
|
AlarmDiscoveryOptions discovery = options.Fallback.Discovery;
|
||||||
|
|
||||||
|
// Config fallback area used only for explicit IncludeAttributes entries (which
|
||||||
|
// carry no discovered area): discovery area, else the default area (may be empty).
|
||||||
|
string configFallbackArea = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area;
|
||||||
|
|
||||||
// 1. Build the ordered, de-duplicated attribute reference set.
|
// 1. Build the ordered, de-duplicated attribute reference set.
|
||||||
// Each entry carries the reference plus the source-object reference.
|
// Each entry carries the reference, the source-object reference, and the
|
||||||
List<(string Reference, string SourceObject)> ordered = [];
|
// per-entry area used to compose the canonical reference. GR rows contribute
|
||||||
|
// the object's real Galaxy area; config includes contribute the config
|
||||||
|
// fallback area (Discovery.Area else DefaultArea).
|
||||||
|
List<(string Reference, string SourceObject, string Area)> ordered = [];
|
||||||
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (discovery.UseGalaxyRepository)
|
if (discovery.UseGalaxyRepository)
|
||||||
@@ -73,7 +84,7 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.Add((row.FullTagReference, row.SourceObjectReference));
|
ordered.Add((row.FullTagReference, row.SourceObjectReference, row.Area));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +95,7 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.Add((include, DeriveSourceObject(include)));
|
ordered.Add((include, DeriveSourceObject(include), configFallbackArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove excluded references (case-insensitive), but only when GR discovery
|
// Remove excluded references (case-insensitive), but only when GR discovery
|
||||||
@@ -112,12 +123,11 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|||||||
string priority = options.Fallback.Subtags.Priority;
|
string priority = options.Fallback.Subtags.Priority;
|
||||||
string ackComment = options.Fallback.Subtags.AckComment;
|
string ackComment = options.Fallback.Subtags.AckComment;
|
||||||
|
|
||||||
// 3. Resolve the area: discovery area, else the default area (may be empty).
|
// 3. Compose one target per reference, using the PER-ENTRY area: the GR row's
|
||||||
string area = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area;
|
// real Galaxy area (matching the alarmmgr group), or the config fallback for
|
||||||
|
// explicit includes.
|
||||||
// 4. Compose one target per reference.
|
|
||||||
List<AlarmSubtagTarget> targets = new(ordered.Count);
|
List<AlarmSubtagTarget> targets = new(ordered.Count);
|
||||||
foreach ((string reference, string sourceObject) in ordered)
|
foreach ((string reference, string sourceObject, string area) in ordered)
|
||||||
{
|
{
|
||||||
targets.Add(new AlarmSubtagTarget
|
targets.Add(new AlarmSubtagTarget
|
||||||
{
|
{
|
||||||
@@ -130,7 +140,7 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Report the resolved count; warn when subtag mode was expected to cover
|
// 4. Report the resolved count; warn when subtag mode was expected to cover
|
||||||
// something (GR enabled, or explicit includes were configured) but resolved
|
// something (GR enabled, or explicit includes were configured) but resolved
|
||||||
// to nothing. Only emit the Debug line when there is at least one target,
|
// to nothing. Only emit the Debug line when there is at least one target,
|
||||||
// to avoid a confusing "0 target(s)" noise line.
|
// to avoid a confusing "0 target(s)" noise line.
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ public sealed class GalaxyAlarmAttributeRow
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SourceObjectReference { get; init; } = string.Empty;
|
public string SourceObjectReference { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the owning object's Galaxy area (e.g. <c>TestArea</c>) — the alarm group.
|
||||||
|
/// <para>
|
||||||
|
/// Resolved via <c>gobject.area_gobject_id</c> in <c>AlarmAttributesSql</c>. The
|
||||||
|
/// watch-list resolver composes the canonical <c>Galaxy!{area}.{reference}</c> from
|
||||||
|
/// this so the synthesized reference's group matches the native alarmmgr (wnwrap)
|
||||||
|
/// for reference parity. May be <see cref="string.Empty"/> when the object has no
|
||||||
|
/// area; the resolver then falls back to the configured area.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public string Area { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the writable ack-comment attribute address.
|
/// Gets the writable ack-comment attribute address.
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
|
|||||||
{
|
{
|
||||||
rows.Add(MapAlarmRow(
|
rows.Add(MapAlarmRow(
|
||||||
fullTagReference: reader.GetString(0),
|
fullTagReference: reader.GetString(0),
|
||||||
sourceObjectReference: reader.GetString(1)));
|
sourceObjectReference: reader.GetString(1),
|
||||||
|
area: reader.GetString(2)));
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -151,16 +152,23 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
|
|||||||
/// schema does not expose an ack-comment address and the watch-list resolver
|
/// schema does not expose an ack-comment address and the watch-list resolver
|
||||||
/// composes it later.
|
/// composes it later.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <paramref name="area"/> is the owning object's real Galaxy area (its alarm
|
||||||
|
/// group), resolved via <c>gobject.area_gobject_id</c>; the watch-list resolver
|
||||||
|
/// composes the canonical reference from it so the synthesized reference's group
|
||||||
|
/// matches what the native alarmmgr (wnwrap) emits.
|
||||||
/// Exposed internally so the derivation can be unit-tested without a database.
|
/// Exposed internally so the derivation can be unit-tested without a database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fullTagReference">The alarm-bearing attribute reference.</param>
|
/// <param name="fullTagReference">The alarm-bearing attribute reference.</param>
|
||||||
/// <param name="sourceObjectReference">The owning object reference (tag name).</param>
|
/// <param name="sourceObjectReference">The owning object reference (tag name).</param>
|
||||||
|
/// <param name="area">The owning object's Galaxy area (the alarm group).</param>
|
||||||
internal static GalaxyAlarmAttributeRow MapAlarmRow(
|
internal static GalaxyAlarmAttributeRow MapAlarmRow(
|
||||||
string fullTagReference,
|
string fullTagReference,
|
||||||
string sourceObjectReference) => new()
|
string sourceObjectReference,
|
||||||
|
string area) => new()
|
||||||
{
|
{
|
||||||
FullTagReference = fullTagReference,
|
FullTagReference = fullTagReference,
|
||||||
SourceObjectReference = sourceObjectReference,
|
SourceObjectReference = sourceObjectReference,
|
||||||
|
Area = area,
|
||||||
AckCommentSubtag = string.Empty,
|
AckCommentSubtag = string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -307,7 +315,9 @@ ORDER BY r.tag_name, r.attribute_name";
|
|||||||
// object. It projects just what the watch-list needs — full_tag_reference (tag_name +
|
// object. It projects just what the watch-list needs — full_tag_reference (tag_name +
|
||||||
// '.' + attribute_name, matching AttributesSql) and the owning object's tag_name as
|
// '.' + attribute_name, matching AttributesSql) and the owning object's tag_name as
|
||||||
// source_object_reference. The array `[]` suffix is intentionally omitted: an
|
// source_object_reference. The array `[]` suffix is intentionally omitted: an
|
||||||
// alarm-bearing attribute is a scalar anchor, not an array body.
|
// alarm-bearing attribute is a scalar anchor, not an array body. It also projects the
|
||||||
|
// owning object's real Galaxy area (via gobject.area_gobject_id) as area_name so the
|
||||||
|
// watch-list resolver composes a reference whose group matches the native alarmmgr.
|
||||||
private const string AlarmAttributesSql = @"
|
private const string AlarmAttributesSql = @"
|
||||||
;WITH deployed_package_chain AS (
|
;WITH deployed_package_chain AS (
|
||||||
SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
|
SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
|
||||||
@@ -354,8 +364,11 @@ ranked AS (
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
r.tag_name + '.' + r.attribute_name AS full_tag_reference,
|
r.tag_name + '.' + r.attribute_name AS full_tag_reference,
|
||||||
r.tag_name AS source_object_reference
|
r.tag_name AS source_object_reference,
|
||||||
|
ISNULL(area.tag_name, '') AS area_name
|
||||||
FROM ranked r
|
FROM ranked r
|
||||||
|
INNER JOIN gobject g ON g.gobject_id = r.gobject_id
|
||||||
|
LEFT JOIN gobject area ON area.gobject_id = g.area_gobject_id
|
||||||
WHERE r.rn = 1
|
WHERE r.rn = 1
|
||||||
AND r.src_pri = 0
|
AND r.src_pri = 0
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02", Area = "TestArea" },
|
||||||
// Duplicate of an include below (case-insensitive) — should appear once.
|
// Duplicate of an include below (case-insensitive) — should appear once.
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Pump01.Fault", SourceObjectReference = "Pump01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Pump01.Fault", SourceObjectReference = "Pump01", Area = "TestArea" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
@@ -68,7 +68,7 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
@@ -94,7 +94,7 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
@@ -111,27 +111,37 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResolveAsync_ComposesCanonicalFullReference_WithArea()
|
public async Task ResolveAsync_ComposesCanonicalFullReference_FromRealGalaxyArea_NotConfigArea()
|
||||||
{
|
{
|
||||||
|
// The GR row carries the object's real Galaxy area (the alarmmgr group). The
|
||||||
|
// composed reference must use that area, NOT the configured Discovery.Area.
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow
|
||||||
|
{
|
||||||
|
FullTagReference = "TestMachine_001.TestAlarm001",
|
||||||
|
SourceObjectReference = "TestMachine_001",
|
||||||
|
Area = "TestArea",
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
|
|
||||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(area: "Site_A"));
|
// Config area "DEV" must be ignored for a GR row that has a discovered area.
|
||||||
|
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(
|
||||||
|
Options(area: "DEV", defaultArea: "DEV"));
|
||||||
|
|
||||||
AlarmSubtagTarget target = Assert.Single(result);
|
AlarmSubtagTarget target = Assert.Single(result);
|
||||||
Assert.Equal("Galaxy!Site_A.Tank01.Level.HiHi", target.AlarmFullReference);
|
Assert.Equal("Galaxy!TestArea.TestMachine_001.TestAlarm001", target.AlarmFullReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResolveAsync_ComposesCanonicalFullReference_WithoutArea()
|
public async Task ResolveAsync_ComposesCanonicalFullReference_WithoutArea()
|
||||||
{
|
{
|
||||||
|
// GR row with no discovered area and no config area -> bare Galaxy!{reference}.
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
@@ -144,16 +154,34 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResolveAsync_FallsBackToDefaultArea_WhenDiscoveryAreaEmpty()
|
public async Task ResolveAsync_ConfigInclude_UsesDiscoveryAreaFallback()
|
||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
// A config IncludeAttributes entry has no discovered area, so it uses the
|
||||||
[
|
// config fallback: Discovery.Area when set.
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
StubGalaxyRepository repo = new([]);
|
||||||
]);
|
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
|
|
||||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(area: "", defaultArea: "Plant"));
|
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||||
|
area: "Site_A",
|
||||||
|
include: ["Tank01.Level.HiHi"]));
|
||||||
|
|
||||||
|
AlarmSubtagTarget target = Assert.Single(result);
|
||||||
|
Assert.Equal("Galaxy!Site_A.Tank01.Level.HiHi", target.AlarmFullReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveAsync_ConfigInclude_FallsBackToDefaultArea_WhenDiscoveryAreaEmpty()
|
||||||
|
{
|
||||||
|
// A config IncludeAttributes entry with no Discovery.Area uses DefaultArea.
|
||||||
|
StubGalaxyRepository repo = new([]);
|
||||||
|
|
||||||
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
|
|
||||||
|
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||||
|
area: "",
|
||||||
|
defaultArea: "Plant",
|
||||||
|
include: ["Tank01.Level.HiHi"]));
|
||||||
|
|
||||||
AlarmSubtagTarget target = Assert.Single(result);
|
AlarmSubtagTarget target = Assert.Single(result);
|
||||||
Assert.Equal("Galaxy!Plant.Tank01.Level.HiHi", target.AlarmFullReference);
|
Assert.Equal("Galaxy!Plant.Tank01.Level.HiHi", target.AlarmFullReference);
|
||||||
@@ -239,8 +267,8 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02", Area = "TestArea" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
@@ -263,7 +291,7 @@ public sealed class AlarmWatchListResolverTests
|
|||||||
{
|
{
|
||||||
StubGalaxyRepository repo = new(
|
StubGalaxyRepository repo = new(
|
||||||
[
|
[
|
||||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||||
|
|||||||
@@ -11,16 +11,18 @@ namespace ZB.MOM.WW.MxGateway.Tests.Galaxy;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GalaxyAlarmAttributeMappingTests
|
public sealed class GalaxyAlarmAttributeMappingTests
|
||||||
{
|
{
|
||||||
/// <summary>Verifies the mapper copies both projected columns onto the row.</summary>
|
/// <summary>Verifies the mapper copies all projected columns onto the row.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MapAlarmRow_CopiesProjectedColumns()
|
public void MapAlarmRow_CopiesProjectedColumns()
|
||||||
{
|
{
|
||||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
||||||
fullTagReference: "Tank01.Level.HiHi",
|
fullTagReference: "Tank01.Level.HiHi",
|
||||||
sourceObjectReference: "Tank01");
|
sourceObjectReference: "Tank01",
|
||||||
|
area: "TestArea");
|
||||||
|
|
||||||
Assert.Equal("Tank01.Level.HiHi", row.FullTagReference);
|
Assert.Equal("Tank01.Level.HiHi", row.FullTagReference);
|
||||||
Assert.Equal("Tank01", row.SourceObjectReference);
|
Assert.Equal("Tank01", row.SourceObjectReference);
|
||||||
|
Assert.Equal("TestArea", row.Area);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -33,7 +35,8 @@ public sealed class GalaxyAlarmAttributeMappingTests
|
|||||||
{
|
{
|
||||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
||||||
fullTagReference: "Tank01.Level.HiHi",
|
fullTagReference: "Tank01.Level.HiHi",
|
||||||
sourceObjectReference: "Tank01");
|
sourceObjectReference: "Tank01",
|
||||||
|
area: "TestArea");
|
||||||
|
|
||||||
Assert.Equal(string.Empty, row.AckCommentSubtag);
|
Assert.Equal(string.Empty, row.AckCommentSubtag);
|
||||||
}
|
}
|
||||||
@@ -55,10 +58,11 @@ public sealed class GalaxyAlarmAttributeMappingTests
|
|||||||
// Mirror the AlarmAttributesSql projection: full_tag_reference = tag_name + '.' + attribute_name.
|
// Mirror the AlarmAttributesSql projection: full_tag_reference = tag_name + '.' + attribute_name.
|
||||||
string fullTagReference = tagName + "." + attributeName;
|
string fullTagReference = tagName + "." + attributeName;
|
||||||
|
|
||||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(fullTagReference, tagName);
|
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(fullTagReference, tagName, area: "TestArea");
|
||||||
|
|
||||||
Assert.Equal(expectedFullReference, row.FullTagReference);
|
Assert.Equal(expectedFullReference, row.FullTagReference);
|
||||||
Assert.Equal(tagName, row.SourceObjectReference);
|
Assert.Equal(tagName, row.SourceObjectReference);
|
||||||
|
Assert.Equal("TestArea", row.Area);
|
||||||
Assert.Equal(row.FullTagReference, row.SourceObjectReference + "." + attributeName);
|
Assert.Equal(row.FullTagReference, row.SourceObjectReference + "." + attributeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,28 @@ public sealed class SubtagAlarmStateMachineTests
|
|||||||
Assert.Equal("Area", e.Record.Group);
|
Assert.Equal("Area", e.Record.Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveFalseToTrue_AlarmMgrShape_EmitsNativeProviderGroupTagName()
|
||||||
|
{
|
||||||
|
// Reference parity: a subtag target composed from the object's real Galaxy
|
||||||
|
// area must round-trip to exactly the native alarmmgr (wnwrap) record fields:
|
||||||
|
// Provider "Galaxy", Group = the real area "TestArea", and the object-rooted
|
||||||
|
// TagName "TestMachine_001.TestAlarm001".
|
||||||
|
var target = new AlarmSubtagTarget
|
||||||
|
{
|
||||||
|
AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
|
||||||
|
SourceObjectReference = "TestMachine_001",
|
||||||
|
ActiveSubtag = "TestMachine_001.TestAlarm001.InAlarm",
|
||||||
|
};
|
||||||
|
var sm = new SubtagAlarmStateMachine(new[] { target });
|
||||||
|
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||||
|
var events = sm.Apply("TestMachine_001.TestAlarm001.InAlarm", true, ts);
|
||||||
|
var e = Assert.Single(events);
|
||||||
|
Assert.Equal("Galaxy", e.Record.ProviderName);
|
||||||
|
Assert.Equal("TestArea", e.Record.Group);
|
||||||
|
Assert.Equal("TestMachine_001.TestAlarm001", e.Record.TagName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActiveFalseToTrue_NoProviderBang_UsesWholeReferenceAsTagName()
|
public void ActiveFalseToTrue_NoProviderBang_UsesWholeReferenceAsTagName()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user