09e19db4e9
Bite-sized task plan with exact file paths, code, and TDD steps for native OPC UA A&C + MxAccess GW alarm ingestion. Phases: Commons contracts -> config/flatten/migration -> DCL ingestion (actor + 2 adapters) -> site runtime (NativeAlarmActor + SQLite) -> gRPC proto -> management/CLI -> UI -> seed/docs/integration. Includes .tasks.json for executing-plans resume.
1526 lines
74 KiB
Markdown
1526 lines
74 KiB
Markdown
# Native OPC UA & MxAccess Gateway Alarms — Implementation Plan
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** Mirror native alarms from OPC UA Alarms & Conditions servers and the MxAccess Gateway into ScadaBridge as a read-only, unified A&C-style alarm state model (severity + active/acked/shelved/suppressed), discovered at runtime from instance-bound sources, kept site-local and shown live in the DebugView.
|
||
|
||
**Architecture:** A new `NativeAlarmActor` (peer to the computed `AlarmActor` under `InstanceActor`) subscribes through a new DCL capability seam (`IAlarmSubscribableConnection`) implemented by the OPC UA and MxGateway adapters. The `DataConnectionActor` opens **one** alarm feed per connection and routes transitions to instances by source reference. State lives in site SQLite, streams to central over the (additively) enriched gRPC `AlarmStateUpdate`, and is seeded via the existing DebugView snapshot. No central tables; read-only (no ack-back). Design doc: `docs/plans/2026-05-29-native-alarms-design.md`.
|
||
|
||
**Tech Stack:** C#/.NET 10, Akka.NET (TestKit, Become/Stash), EF Core + MS SQL (central config), Microsoft.Data.Sqlite (site state), gRPC/protobuf, Blazor Server, System.CommandLine (CLI), xUnit + NSubstitute + Akka.TestKit.Xunit2 + bUnit.
|
||
|
||
**Conventions:** Messages/flattened types are `record`; entities are `class`. xUnit `[Fact]`, name `Method_Scenario_Expected`. Build with `dotnet build ZB.MOM.WW.ScadaBridge.slnx`. Commit after each task. TDD: failing test → verify fail → implement → verify pass → commit.
|
||
|
||
**Critical gotcha (read before Task 18):** `src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto` is **not** auto-compiled — the `<Protobuf>` include is commented out in the `.csproj` and generated `.cs` files are checked into `SiteStreamGrpc/`. Regen is a manual macOS-only step (toggle the include, `dotnet build`, copy generated files, re-comment). Task 18 documents it.
|
||
|
||
---
|
||
|
||
## Task dependency overview
|
||
|
||
```
|
||
T1 ─┬─ T2 ─┬─ T17 (computed AlarmActor enrich)
|
||
│ ├─ T18 (proto) ── T19 (grpc mapping) ── T23 (DebugView)
|
||
T3 ─┼─ T10 (DCL actor)
|
||
├─ T11 (OPC UA adapter)
|
||
└─ T12 (MxGateway adapter)
|
||
T4 ─┬─ T5 ── T6 ── T21 (mgmt handlers)
|
||
├─ T7 (migration)
|
||
├─ T8 ── T9 (validation)
|
||
└─ T20 ─┬─ T21 ── T26 (seed)
|
||
├─ T22 (CLI)
|
||
├─ T24 (template UI)
|
||
└─ T25 (instance UI)
|
||
T13, T14 ──┐
|
||
T1,T2,T3,T4(Resolved),T13,T14 ── T15 (NativeAlarmActor) ── T16 (InstanceActor wiring)
|
||
(everything) ── T27 (docs) , T28 (integration/manual verify)
|
||
```
|
||
|
||
---
|
||
|
||
## Task 1: Commons alarm core types
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 3, Task 4
|
||
|
||
**Files:**
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmKind.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmShelveState.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmTransitionKind.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionState.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/NativeAlarmTransition.cs`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Alarms/AlarmConditionStateTests.cs`
|
||
|
||
**Step 1: Write the failing test**
|
||
|
||
```csharp
|
||
// AlarmConditionStateTests.cs
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Alarms;
|
||
|
||
public class AlarmConditionStateTests
|
||
{
|
||
[Fact]
|
||
public void AlarmConditionState_DefaultsAreNormalUnshelved()
|
||
{
|
||
var s = new AlarmConditionState(
|
||
Active: false, Acknowledged: true, Confirmed: null,
|
||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 0);
|
||
|
||
Assert.False(s.Active);
|
||
Assert.Equal(AlarmShelveState.Unshelved, s.Shelve);
|
||
Assert.Equal(0, s.Severity);
|
||
}
|
||
|
||
[Fact]
|
||
public void NativeAlarmTransition_CarriesSourceAndCondition()
|
||
{
|
||
var t = new NativeAlarmTransition(
|
||
SourceReference: "Tank01.Level.HiHi", SourceObjectReference: "Tank01",
|
||
AlarmTypeName: "AnalogLimitAlarm.HiHi", Kind: AlarmTransitionKind.Raise,
|
||
Condition: new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800),
|
||
Category: "Process", Description: "High level", Message: "level high",
|
||
OperatorUser: "", OperatorComment: "",
|
||
OriginalRaiseTime: null, TransitionTime: DateTimeOffset.UnixEpoch,
|
||
CurrentValue: "92.1", LimitValue: "90");
|
||
|
||
Assert.Equal("Tank01", t.SourceObjectReference);
|
||
Assert.Equal(800, t.Condition.Severity);
|
||
Assert.Equal(AlarmTransitionKind.Raise, t.Kind);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test to verify it fails**
|
||
|
||
Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmConditionStateTests`
|
||
Expected: FAIL — types do not exist (compile error).
|
||
|
||
**Step 3: Write minimal implementation**
|
||
|
||
```csharp
|
||
// AlarmKind.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||
public enum AlarmKind { Computed, NativeOpcUa, NativeMxAccess }
|
||
|
||
// AlarmShelveState.cs — same namespace
|
||
public enum AlarmShelveState { Unshelved, OneShotShelved, TimedShelved, PermanentShelved }
|
||
|
||
// AlarmTransitionKind.cs — same namespace
|
||
public enum AlarmTransitionKind { Snapshot, SnapshotComplete, Raise, Acknowledge, Clear, Retrigger, StateChange }
|
||
```
|
||
|
||
```csharp
|
||
// AlarmConditionState.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
/// <summary>Orthogonal OPC UA Part 9 sub-conditions + severity. Read-only mirror of the source.</summary>
|
||
public record AlarmConditionState(
|
||
bool Active,
|
||
bool Acknowledged,
|
||
bool? Confirmed,
|
||
AlarmShelveState Shelve,
|
||
bool Suppressed,
|
||
int Severity);
|
||
```
|
||
|
||
```csharp
|
||
// NativeAlarmTransition.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
/// <summary>Protocol-neutral alarm transition emitted by an IAlarmSubscribableConnection adapter.</summary>
|
||
public record NativeAlarmTransition(
|
||
string SourceReference,
|
||
string SourceObjectReference,
|
||
string AlarmTypeName,
|
||
AlarmTransitionKind Kind,
|
||
AlarmConditionState Condition,
|
||
string Category,
|
||
string Description,
|
||
string Message,
|
||
string OperatorUser,
|
||
string OperatorComment,
|
||
DateTimeOffset? OriginalRaiseTime,
|
||
DateTimeOffset TransitionTime,
|
||
string CurrentValue,
|
||
string LimitValue);
|
||
```
|
||
|
||
**Step 4: Run test to verify it passes**
|
||
|
||
Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmConditionStateTests`
|
||
Expected: PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Types tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Alarms
|
||
git commit -m "feat(commons): native alarm core types (AlarmConditionState, NativeAlarmTransition, enums)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Extend AlarmStateChanged + computed-default mapping
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 3, Task 4
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs`
|
||
|
||
**Context:** `AlarmStateChanged` already uses init-properties (`Level`, `Message`) so all additions are additive — existing positional callers compile unchanged.
|
||
|
||
**Step 1: Write the failing test**
|
||
|
||
```csharp
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
public class AlarmStateChangedEnrichmentTests
|
||
{
|
||
[Fact]
|
||
public void Defaults_AreComputedKind_WithAutoAck()
|
||
{
|
||
var m = new AlarmStateChanged("inst", "HiAlarm", AlarmState.Active, 700, DateTimeOffset.UnixEpoch);
|
||
Assert.Equal(AlarmKind.Computed, m.Kind);
|
||
Assert.True(m.Condition.Acknowledged); // computed = auto-acked
|
||
Assert.Equal(700, m.Condition.Severity); // severity defaults to Priority
|
||
Assert.True(m.Condition.Active); // derived from State
|
||
Assert.Equal("", m.SourceReference);
|
||
}
|
||
|
||
[Fact]
|
||
public void Factory_ForComputed_MapsPriorityAndState()
|
||
{
|
||
var c = AlarmConditionStateFactory.ForComputed(AlarmState.Normal, priority: 250);
|
||
Assert.False(c.Active);
|
||
Assert.True(c.Acknowledged);
|
||
Assert.Equal(250, c.Severity);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test to verify it fails**
|
||
|
||
Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmStateChangedEnrichmentTests`
|
||
Expected: FAIL — `Kind`/`Condition` not defined.
|
||
|
||
**Step 3: Write minimal implementation**
|
||
|
||
Add `AlarmConditionStateFactory.cs` (namespace `...Types.Alarms`):
|
||
|
||
```csharp
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
public static class AlarmConditionStateFactory
|
||
{
|
||
/// <summary>Computed alarms are auto-acked, never shelved/suppressed; severity = priority.</summary>
|
||
public static AlarmConditionState ForComputed(AlarmState state, int priority) =>
|
||
new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null,
|
||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority);
|
||
}
|
||
```
|
||
|
||
In `AlarmStateChanged.cs`, add these init-properties to the record body (keep existing `Level`, `Message`):
|
||
|
||
```csharp
|
||
public AlarmKind Kind { get; init; } = AlarmKind.Computed;
|
||
|
||
private AlarmConditionState? _condition;
|
||
/// <summary>Unified A&C-style condition. Defaults to a computed mapping of State+Priority.</summary>
|
||
public AlarmConditionState Condition
|
||
{
|
||
get => _condition ?? AlarmConditionStateFactory.ForComputed(State, Priority);
|
||
init => _condition = value;
|
||
}
|
||
|
||
public string SourceReference { get; init; } = string.Empty;
|
||
public string AlarmTypeName { get; init; } = string.Empty;
|
||
public string Category { get; init; } = string.Empty;
|
||
public string OperatorUser { get; init; } = string.Empty;
|
||
public string OperatorComment { get; init; } = string.Empty;
|
||
public DateTimeOffset? OriginalRaiseTime { get; init; }
|
||
public string CurrentValue { get; init; } = string.Empty;
|
||
public string LimitValue { get; init; } = string.Empty;
|
||
```
|
||
|
||
Add `using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;` to the file.
|
||
|
||
**Step 4: Run test to verify it passes**
|
||
|
||
Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmStateChangedEnrichmentTests`
|
||
Expected: PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs
|
||
git commit -m "feat(commons): enrich AlarmStateChanged with unified condition state (additive)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: IAlarmSubscribableConnection seam + DCL alarm messages
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 2, Task 4
|
||
|
||
**Files:**
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IAlarmSubscribableConnection.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/SubscribeAlarmsRequest.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/NativeAlarmMessages.cs`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/NativeAlarmMessagesTests.cs`
|
||
|
||
**Step 1: Write the failing test**
|
||
|
||
```csharp
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||
|
||
public class NativeAlarmMessagesTests
|
||
{
|
||
[Fact]
|
||
public void SubscribeAlarmsRequest_CarriesSourceAndFilter()
|
||
{
|
||
var r = new SubscribeAlarmsRequest("c1", "inst", "PlantOpcUa", "ns=2;s=Tank01", null, DateTimeOffset.UnixEpoch);
|
||
Assert.Equal("ns=2;s=Tank01", r.SourceReference);
|
||
Assert.Null(r.ConditionFilter);
|
||
}
|
||
|
||
[Fact]
|
||
public void NativeAlarmTransitionUpdate_WrapsTransition()
|
||
{
|
||
var t = new NativeAlarmTransition("Tank01.Hi", "Tank01", "x", AlarmTransitionKind.Raise,
|
||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 500),
|
||
"", "", "", "", "", null, DateTimeOffset.UnixEpoch, "", "");
|
||
var u = new NativeAlarmTransitionUpdate("PlantOpcUa", t);
|
||
Assert.Equal("PlantOpcUa", u.ConnectionName);
|
||
Assert.Equal("Tank01", u.Transition.SourceObjectReference);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** — Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter NativeAlarmMessagesTests` → FAIL (types missing).
|
||
|
||
**Step 3: Implement**
|
||
|
||
```csharp
|
||
// IAlarmSubscribableConnection.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||
|
||
/// <summary>Callback invoked when a native alarm transition (incl. snapshot replay) arrives.</summary>
|
||
public delegate void AlarmTransitionCallback(NativeAlarmTransition transition);
|
||
|
||
/// <summary>Optional capability: an IDataConnection that can mirror a source's native alarms.</summary>
|
||
public interface IAlarmSubscribableConnection
|
||
{
|
||
Task<string> SubscribeAlarmsAsync(string sourceReference, string? conditionFilter,
|
||
AlarmTransitionCallback callback, CancellationToken cancellationToken = default);
|
||
Task UnsubscribeAlarmsAsync(string subscriptionId, CancellationToken cancellationToken = default);
|
||
}
|
||
```
|
||
|
||
```csharp
|
||
// SubscribeAlarmsRequest.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection
|
||
public record SubscribeAlarmsRequest(
|
||
string CorrelationId, string InstanceUniqueName, string ConnectionName,
|
||
string SourceReference, string? ConditionFilter, DateTimeOffset Timestamp);
|
||
|
||
public record SubscribeAlarmsResponse(
|
||
string CorrelationId, string InstanceUniqueName, bool Success, string? ErrorMessage, DateTimeOffset Timestamp);
|
||
|
||
public record UnsubscribeAlarmsRequest(
|
||
string CorrelationId, string InstanceUniqueName, string ConnectionName, string SourceReference, DateTimeOffset Timestamp);
|
||
```
|
||
|
||
```csharp
|
||
// NativeAlarmMessages.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||
|
||
/// <summary>DCL → instance: a native alarm transition routed by source reference.</summary>
|
||
public record NativeAlarmTransitionUpdate(string ConnectionName, NativeAlarmTransition Transition);
|
||
|
||
/// <summary>DCL → instance: the alarm feed for a source is unavailable (connection lost); mark uncertain.</summary>
|
||
public record NativeAlarmSourceUnavailable(string ConnectionName, string SourceReference, DateTimeOffset Timestamp);
|
||
```
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IAlarmSubscribableConnection.cs src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/NativeAlarmMessagesTests.cs
|
||
git commit -m "feat(commons): IAlarmSubscribableConnection seam + DCL native alarm messages"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Entities + flattened type + Template navigation
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 1, Task 2, Task 3
|
||
|
||
**Files:**
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs`
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs`
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/Template.cs` (add `NativeAlarmSources` collection)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/Instance.cs` (add `NativeAlarmSourceOverrides` collection)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs` (add `ResolvedNativeAlarmSource` record + `NativeAlarmSources` list)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/NativeAlarmSourceEntityTests.cs`
|
||
|
||
**Step 1: Write the failing test**
|
||
|
||
```csharp
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||
|
||
public class NativeAlarmSourceEntityTests
|
||
{
|
||
[Fact]
|
||
public void TemplateNativeAlarmSource_RequiresName()
|
||
{
|
||
var s = new TemplateNativeAlarmSource("PressureMon") { ConnectionName = "Plant", SourceReference = "ns=2;s=P1" };
|
||
Assert.Equal("PressureMon", s.Name);
|
||
Assert.False(s.IsLocked);
|
||
}
|
||
|
||
[Fact]
|
||
public void FlattenedConfiguration_HasNativeAlarmSourcesDefaultEmpty()
|
||
{
|
||
var f = new FlattenedConfiguration();
|
||
Assert.Empty(f.NativeAlarmSources);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolvedNativeAlarmSource_DefaultsSourceTemplate()
|
||
{
|
||
var r = new ResolvedNativeAlarmSource { CanonicalName = "PressureMon", ConnectionName = "Plant", SourceReference = "ns=2;s=P1" };
|
||
Assert.Equal("Template", r.Source);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL (types/members missing).
|
||
|
||
**Step 3: Implement**
|
||
|
||
`TemplateNativeAlarmSource.cs` — mirror `TemplateAlarm` (class, ctor takes name):
|
||
|
||
```csharp
|
||
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||
|
||
public class TemplateNativeAlarmSource
|
||
{
|
||
public int Id { get; set; }
|
||
public int TemplateId { get; set; }
|
||
public string Name { get; set; }
|
||
public string? Description { get; set; }
|
||
public string ConnectionName { get; set; } = string.Empty;
|
||
public string SourceReference { get; set; } = string.Empty;
|
||
public string? ConditionFilter { get; set; } // null = mirror all conditions under the source
|
||
public bool IsLocked { get; set; }
|
||
public bool IsInherited { get; set; }
|
||
public bool LockedInDerived { get; set; }
|
||
|
||
public TemplateNativeAlarmSource(string name) =>
|
||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||
}
|
||
```
|
||
|
||
`InstanceNativeAlarmSourceOverride.cs` — mirror `InstanceAlarmOverride`:
|
||
|
||
```csharp
|
||
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||
|
||
public class InstanceNativeAlarmSourceOverride
|
||
{
|
||
public int Id { get; set; }
|
||
public int InstanceId { get; set; }
|
||
public string SourceCanonicalName { get; set; }
|
||
public string? ConnectionNameOverride { get; set; } // null = keep inherited
|
||
public string? SourceReferenceOverride { get; set; }
|
||
public string? ConditionFilterOverride { get; set; }
|
||
|
||
public InstanceNativeAlarmSourceOverride(string sourceCanonicalName) =>
|
||
SourceCanonicalName = sourceCanonicalName ?? throw new ArgumentNullException(nameof(sourceCanonicalName));
|
||
}
|
||
```
|
||
|
||
In `Template.cs` add (mirror the `Alarms` collection): `public ICollection<TemplateNativeAlarmSource> NativeAlarmSources { get; set; } = new List<TemplateNativeAlarmSource>();`
|
||
|
||
In `Instance.cs` add (mirror `AlarmOverrides`): `public ICollection<InstanceNativeAlarmSourceOverride> NativeAlarmSourceOverrides { get; set; } = new List<InstanceNativeAlarmSourceOverride>();`
|
||
|
||
In `FlattenedConfiguration.cs` add to the record: `public IReadOnlyList<ResolvedNativeAlarmSource> NativeAlarmSources { get; init; } = [];` and a new record (mirror `ResolvedAlarm`):
|
||
|
||
```csharp
|
||
/// <summary>A fully resolved native alarm source binding (connection + source reference).</summary>
|
||
public sealed record ResolvedNativeAlarmSource
|
||
{
|
||
public string CanonicalName { get; init; } = string.Empty;
|
||
public string ConnectionName { get; init; } = string.Empty;
|
||
public string SourceReference { get; init; } = string.Empty;
|
||
public string? ConditionFilter { get; init; }
|
||
public string Source { get; init; } = "Template"; // Template|Inherited|Composed|Override
|
||
}
|
||
```
|
||
|
||
**Step 4: Run test** → PASS. Also run `dotnet build ZB.MOM.WW.ScadaBridge.slnx` to confirm nothing referencing `Template`/`Instance` broke.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Entities src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/NativeAlarmSourceEntityTests.cs
|
||
git commit -m "feat(commons): native alarm source entities + ResolvedNativeAlarmSource"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: EF configurations + DbSets
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 8, Task 20
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/TemplateConfiguration.cs` (add `TemplateNativeAlarmSourceConfiguration` class + relationship on `TemplateConfiguration`)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs` (add `InstanceNativeAlarmSourceOverrideConfiguration` + relationship)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs` (add two `DbSet`s after line ~65)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/NativeAlarmSourceSchemaTests.cs`
|
||
|
||
**Step 1: Write the failing test** (in-memory SQLite, `EnsureCreated`, mirrors existing repo-test setup):
|
||
|
||
```csharp
|
||
using Microsoft.EntityFrameworkCore;
|
||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||
|
||
public class NativeAlarmSourceSchemaTests
|
||
{
|
||
[Fact]
|
||
public async Task TemplateNativeAlarmSource_PersistsAndEnforcesUniqueName()
|
||
{
|
||
var opts = new DbContextOptionsBuilder<ScadaBridgeDbContext>().UseSqlite("DataSource=:memory:").Options;
|
||
using var ctx = new ScadaBridgeDbContext(opts);
|
||
ctx.Database.OpenConnection();
|
||
ctx.Database.EnsureCreated();
|
||
|
||
var t = new Template("T1");
|
||
t.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src1") { ConnectionName = "C", SourceReference = "r" });
|
||
ctx.Templates.Add(t);
|
||
await ctx.SaveChangesAsync();
|
||
|
||
Assert.Single(ctx.TemplateNativeAlarmSources);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL (`TemplateNativeAlarmSources` DbSet missing).
|
||
|
||
**Step 3: Implement**
|
||
|
||
In `ScadaBridgeDbContext.cs` after the template/instance DbSets:
|
||
|
||
```csharp
|
||
public DbSet<TemplateNativeAlarmSource> TemplateNativeAlarmSources => Set<TemplateNativeAlarmSource>();
|
||
public DbSet<InstanceNativeAlarmSourceOverride> InstanceNativeAlarmSourceOverrides => Set<InstanceNativeAlarmSourceOverride>();
|
||
```
|
||
|
||
(`OnModelCreating` already calls `ApplyConfigurationsFromAssembly` — new `IEntityTypeConfiguration` classes auto-register.)
|
||
|
||
In `TemplateConfiguration.cs`, in `TemplateConfiguration.Configure` add the relationship next to `HasMany(t => t.Alarms)`:
|
||
|
||
```csharp
|
||
builder.HasMany(t => t.NativeAlarmSources).WithOne().HasForeignKey(s => s.TemplateId).OnDelete(DeleteBehavior.Cascade);
|
||
```
|
||
|
||
Add a sibling config class (mirror `TemplateAlarmConfiguration`):
|
||
|
||
```csharp
|
||
public class TemplateNativeAlarmSourceConfiguration : IEntityTypeConfiguration<TemplateNativeAlarmSource>
|
||
{
|
||
public void Configure(EntityTypeBuilder<TemplateNativeAlarmSource> builder)
|
||
{
|
||
builder.HasKey(s => s.Id);
|
||
builder.Property(s => s.Name).IsRequired().HasMaxLength(200);
|
||
builder.Property(s => s.Description).HasMaxLength(2000);
|
||
builder.Property(s => s.ConnectionName).IsRequired().HasMaxLength(200);
|
||
builder.Property(s => s.SourceReference).IsRequired().HasMaxLength(1000);
|
||
builder.Property(s => s.ConditionFilter).HasMaxLength(1000);
|
||
builder.HasIndex(s => new { s.TemplateId, s.Name }).IsUnique();
|
||
}
|
||
}
|
||
```
|
||
|
||
In `InstanceConfiguration.cs`, add relationship next to `HasMany(i => i.AlarmOverrides)`:
|
||
|
||
```csharp
|
||
builder.HasMany(i => i.NativeAlarmSourceOverrides).WithOne().HasForeignKey(o => o.InstanceId).OnDelete(DeleteBehavior.Cascade);
|
||
```
|
||
|
||
And a sibling config (mirror `InstanceAlarmOverrideConfiguration`):
|
||
|
||
```csharp
|
||
public class InstanceNativeAlarmSourceOverrideConfiguration : IEntityTypeConfiguration<InstanceNativeAlarmSourceOverride>
|
||
{
|
||
public void Configure(EntityTypeBuilder<InstanceNativeAlarmSourceOverride> builder)
|
||
{
|
||
builder.HasKey(o => o.Id);
|
||
builder.Property(o => o.SourceCanonicalName).IsRequired().HasMaxLength(400);
|
||
builder.Property(o => o.ConnectionNameOverride).HasMaxLength(200);
|
||
builder.Property(o => o.SourceReferenceOverride).HasMaxLength(1000);
|
||
builder.Property(o => o.ConditionFilterOverride).HasMaxLength(1000);
|
||
builder.HasIndex(o => new { o.InstanceId, o.SourceCanonicalName }).IsUnique();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/NativeAlarmSourceSchemaTests.cs
|
||
git commit -m "feat(configdb): EF mappings + DbSets for native alarm source entities"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Repository interface + implementation
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 7, Task 8
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs` (add 10 methods)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs` (implement; mirror alarm-override methods)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs` — ensure `GetTemplateWithChildrenAsync` `.Include(t => t.NativeAlarmSources)` and the instance loader `.Include(i => i.NativeAlarmSourceOverrides)`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs` (add CRUD facts)
|
||
|
||
**Step 1: Write failing tests** — add facts:
|
||
|
||
```csharp
|
||
[Fact]
|
||
public async Task AddAndGetNativeAlarmSourcesByTemplateId_RoundTrips()
|
||
{
|
||
var t = new Template("T"); _context.Templates.Add(t); await _context.SaveChangesAsync();
|
||
await _repository.AddTemplateNativeAlarmSourceAsync(
|
||
new TemplateNativeAlarmSource("S") { TemplateId = t.Id, ConnectionName = "C", SourceReference = "r" });
|
||
await _context.SaveChangesAsync();
|
||
var list = await _repository.GetNativeAlarmSourcesByTemplateIdAsync(t.Id);
|
||
Assert.Single(list);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetTemplateWithChildren_IncludesNativeAlarmSources()
|
||
{
|
||
var t = new Template("T");
|
||
t.NativeAlarmSources.Add(new TemplateNativeAlarmSource("S") { ConnectionName = "C", SourceReference = "r" });
|
||
_context.Templates.Add(t); await _context.SaveChangesAsync();
|
||
var loaded = await _repository.GetTemplateWithChildrenAsync(t.Id);
|
||
Assert.Single(loaded!.NativeAlarmSources);
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** — add the 10 method signatures to `ITemplateEngineRepository` and implement them in `TemplateEngineRepository` exactly mirroring the existing alarm-override CRUD (`Get...ByTemplateIdAsync`, `Get...ByIdAsync`, `Add...Async`, `Update...Async`, `Delete...Async` for template; `Get...OverridesByInstanceIdAsync`, `Get...OverrideAsync`, `Add/Update/Delete...OverrideAsync` for instance). Add `.Include(t => t.NativeAlarmSources)` to `GetTemplateWithChildrenAsync` and `.Include(i => i.NativeAlarmSourceOverrides)` to the instance-with-children loader.
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs
|
||
git commit -m "feat(configdb): native alarm source repository CRUD + eager-load includes"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: EF migration
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 6, Task 8
|
||
|
||
**Files:**
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/<timestamp>_AddNativeAlarmSources.cs` (generated)
|
||
- Modify: `.../Migrations/ScadaBridgeDbContextModelSnapshot.cs` (generated)
|
||
|
||
**Step 1: Generate the migration**
|
||
|
||
Run from repo root:
|
||
|
||
```bash
|
||
dotnet ef migrations add AddNativeAlarmSources \
|
||
--project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase \
|
||
--startup-project src/ZB.MOM.WW.ScadaBridge.Host
|
||
```
|
||
|
||
**Step 2: Verify** the generated `Up()` creates `Templates.NativeAlarmSources`-equivalent table `NativeAlarmSources` (FK `TemplateId` → `Templates`, cascade; unique index `(TemplateId, Name)`) and `InstanceNativeAlarmSourceOverrides` (FK `InstanceId` → `Instances`, cascade; unique `(InstanceId, SourceCanonicalName)`). Compare shape against `20260513055537_AddInstanceAlarmOverrides.cs`.
|
||
|
||
**Step 3: Apply against a throwaway DB to confirm it runs**
|
||
|
||
```bash
|
||
dotnet build ZB.MOM.WW.ScadaBridge.slnx
|
||
# auto-applies in dev on Host startup; or:
|
||
dotnet ef database update --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase --startup-project src/ZB.MOM.WW.ScadaBridge.Host
|
||
```
|
||
|
||
Expected: applies with no error; `dotnet ef migrations list` shows `AddNativeAlarmSources` pending→applied.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations
|
||
git commit -m "feat(configdb): migration AddNativeAlarmSources"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Flattening — ResolveNativeAlarmSources
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 5, Task 6, Task 7
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs` (new `ResolveNativeAlarmSources` + `ResolveComposedNativeAlarmSources` + `ApplyInstanceNativeAlarmSourceOverrides`; call in `Flatten()` Step 5 region; attach to `FlattenedConfiguration`)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs` (add facts)
|
||
|
||
**Step 1: Write failing tests** (mirror `Flatten_SingleTemplate_ResolvesAttributes`):
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void Flatten_ResolvesNativeAlarmSources_FromTemplate()
|
||
{
|
||
var template = CreateTemplate(1, "Base");
|
||
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
|
||
{ ConnectionName = "Opc", SourceReference = "ns=2;s=P1" });
|
||
var result = _sut.Flatten(CreateInstance(), [template],
|
||
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||
new Dictionary<int, IReadOnlyList<Template>>(),
|
||
new Dictionary<int, DataConnection>());
|
||
Assert.True(result.IsSuccess);
|
||
Assert.Single(result.Value.NativeAlarmSources);
|
||
Assert.Equal("Pressure", result.Value.NativeAlarmSources[0].CanonicalName);
|
||
Assert.Equal("ns=2;s=P1", result.Value.NativeAlarmSources[0].SourceReference);
|
||
}
|
||
|
||
[Fact]
|
||
public void Flatten_InstanceOverride_ReplacesSourceReference()
|
||
{
|
||
var template = CreateTemplate(1, "Base");
|
||
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
|
||
{ ConnectionName = "Opc", SourceReference = "ns=2;s=DEFAULT" });
|
||
var instance = CreateInstance();
|
||
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
|
||
{ SourceReferenceOverride = "ns=2;s=Tank07" });
|
||
var result = _sut.Flatten(instance, [template], new(), new(), new());
|
||
Assert.Equal("ns=2;s=Tank07", result.Value.NativeAlarmSources[0].SourceReference);
|
||
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** — In `Flatten()`'s "Step 5" region (after alarms, ~line 97), add:
|
||
|
||
```csharp
|
||
var nativeAlarmSources = ResolveInheritedNativeAlarmSources(templateChain, prefix: null);
|
||
ResolveComposedNativeAlarmSources(templateChain, compositionMap, composedTemplateChains, nativeAlarmSources);
|
||
ApplyInstanceNativeAlarmSourceOverrides(instance.NativeAlarmSourceOverrides, nativeAlarmSources);
|
||
```
|
||
|
||
Attach to the returned config: `NativeAlarmSources = nativeAlarmSources.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList()`.
|
||
|
||
Implement the three helpers mirroring `ResolveInheritedAlarms` / `ResolveComposedAlarms(Recursive)` / `ApplyInstanceAlarmOverrides`:
|
||
- **Inherited:** walk chain base→derived; derived wins unless `IsLocked`; key by bare `Name`; produce `ResolvedNativeAlarmSource { CanonicalName = prefix-qualified Name, ConnectionName, SourceReference, ConditionFilter, Source = inherited?"Inherited":"Template" }`.
|
||
- **Composed:** for each composition module, recurse into the composed chain, path-qualify `CanonicalName` to `[ModuleInstanceName].[Name]`, collision-check, `Source = "Composed"`. (No attribute-reference rewriting needed — `SourceReference` is a raw connection address, not an attribute path.)
|
||
- **Overrides:** for each `InstanceNativeAlarmSourceOverride` matching `SourceCanonicalName`, apply non-null `ConnectionNameOverride`/`SourceReferenceOverride`/`ConditionFilterOverride`, set `Source = "Override"`.
|
||
|
||
**Step 4: Run test** → PASS. Run full TemplateEngine.Tests.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs
|
||
git commit -m "feat(templateengine): flatten native alarm sources (inherit/compose/override)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Semantic validation
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 10, Task 11, Task 12
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs` (validation block after line ~215; add `ValidationCategory.NativeAlarmSourceInvalid`)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/ValidationCategory.cs` (add enum member — confirm location during impl)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs`
|
||
|
||
**Step 1: Write failing tests:**
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void Validate_NativeAlarmSource_UnknownConnection_Errors()
|
||
{
|
||
var cfg = ConfigWith(new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "Ghost", SourceReference = "x" });
|
||
var errors = _sut.Validate(cfg, knownConnections: new HashSet<string> { "RealConn" });
|
||
Assert.Contains(errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
|
||
}
|
||
|
||
[Fact]
|
||
public void Validate_NativeAlarmSource_EmptySourceRef_Errors()
|
||
{
|
||
var cfg = ConfigWith(new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "" });
|
||
var errors = _sut.Validate(cfg, knownConnections: new HashSet<string> { "RealConn" });
|
||
Assert.Contains(errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
|
||
}
|
||
```
|
||
|
||
(Confirm `Validate`'s real signature for connection scope during impl; the existing alarm/connection validation already receives connection info — reuse that source rather than adding a new param if available.)
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** — add `NativeAlarmSourceInvalid` to `ValidationCategory`; after the on-trigger-script validation loop, iterate `config.NativeAlarmSources` and `errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid, "...", source.CanonicalName))` when: `ConnectionName` not in the known/alarm-capable connection set, or `SourceReference` is empty. (Protocol alarm-capability check: connection protocol ∈ {OpcUa, MxGateway}.)
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/ValidationCategory.cs tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
|
||
git commit -m "feat(templateengine): validate native alarm source connection + source reference"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: DataConnectionActor — alarm subscribe/route/unavailable
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 11, Task 12
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/DataConnectionActorAlarmTests.cs`
|
||
|
||
**Design:** add `_alarmSubscribers: Dictionary<string /*sourceObjectRef*/, HashSet<IActorRef>>` and `_alarmSubByInstance: Dictionary<string, (IActorRef, string sourceRef)>`. Handle `SubscribeAlarmsRequest` in **Connected** (stash in Connecting/Reconnecting); capability-check `_adapter is IAlarmSubscribableConnection` (else reply `SubscribeAlarmsResponse(false, "not alarm-capable")`); call `SubscribeAlarmsAsync` with a callback closure capturing `Self` + `_adapterGeneration` that `Self.Tell(new AlarmTransitionReceived(transition, generation))`. On `AlarmTransitionReceived`, drop if stale generation, else route: `Self.Tell` fan-out to `_alarmSubscribers` whose key prefixes `transition.SourceObjectReference`, wrapping as `NativeAlarmTransitionUpdate(_connectionName, transition)`. On entering **Reconnecting** (existing `PushBadQualityForAllTags`), also send `NativeAlarmSourceUnavailable` to alarm subscribers; on reconnect, re-subscribe alarms (the adapter re-emits a snapshot). Handle `UnsubscribeAlarmsRequest` in all states (mirror `HandleUnsubscribe`).
|
||
|
||
**Step 1: Write failing test** (NSubstitute adapter implementing both `IDataConnection` and `IAlarmSubscribableConnection`):
|
||
|
||
```csharp
|
||
public class DataConnectionActorAlarmTests : TestKit
|
||
{
|
||
[Fact]
|
||
public void SubscribeAlarms_RoutesTransitionToInstanceSubscriber()
|
||
{
|
||
AlarmTransitionCallback? cb = null;
|
||
var adapter = Substitute.For<IDataConnection, IAlarmSubscribableConnection>();
|
||
((IAlarmSubscribableConnection)adapter)
|
||
.SubscribeAlarmsAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Do<AlarmTransitionCallback>(c => cb = c), Arg.Any<CancellationToken>())
|
||
.Returns(Task.FromResult("alarm-sub-1"));
|
||
adapter.ConnectAsync(Arg.Any<IDictionary<string,string>>(), Arg.Any<CancellationToken>()).Returns(Task.CompletedTask);
|
||
|
||
var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
|
||
"conn", adapter, _options, _health, _factory, "OpcUa")));
|
||
|
||
actor.Tell(new SubscribeAlarmsRequest("c","inst","conn","Tank01",null,DateTimeOffset.UtcNow));
|
||
ExpectMsg<SubscribeAlarmsResponse>(m => m.Success);
|
||
|
||
var t = new NativeAlarmTransition("Tank01.Hi","Tank01","x",AlarmTransitionKind.Raise,
|
||
new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,500),"","","","","",null,DateTimeOffset.UtcNow,"","");
|
||
cb!(t); // adapter fires a transition
|
||
ExpectMsg<NativeAlarmTransitionUpdate>(u => u.Transition.SourceObjectReference == "Tank01");
|
||
}
|
||
}
|
||
```
|
||
|
||
(Wire `_options`/`_health`/`_factory` per the existing `DataConnectionActorTests` ctor.)
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** the handlers + routing table + stash + unavailable signal as described. Add an internal `record AlarmTransitionReceived(NativeAlarmTransition Transition, int Generation)`.
|
||
|
||
**Step 4: Run test** → PASS. Run full DCL actor tests to confirm no regression in tag-subscription paths.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/DataConnectionActorAlarmTests.cs
|
||
git commit -m "feat(dcl): DataConnectionActor native alarm subscribe + source-ref routing + unavailable signal"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: OPC UA A&C adapter
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 10, Task 12
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs` (add alarm-subscription method)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs` (event MonitoredItem + EventFilter + ConditionRefresh + field→transition mapping)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs` (implement `IAlarmSubscribableConnection`, map client callback → `NativeAlarmTransition`)
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs` (pure mapping helpers — unit-testable without a server)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs`
|
||
|
||
**Strategy:** Keep server I/O in `RealOpcUaClient`, but isolate the **field → AlarmConditionState/transition** logic in `OpcUaAlarmMapper` so it is unit-testable without a live server (live behavior is covered by the integration task / SkippableFact). `RealOpcUaClient` creates one event `MonitoredItem` (`AttributeId = Attributes.EventNotifier`) on the Server object (`ObjectIds.Server`) with an `EventFilter` selecting EventType, SourceNode, SourceName, Time, Message, Severity, and the `ConditionType`/`AcknowledgeableConditionType`/`AlarmConditionType` state fields; calls the `ConditionRefresh` method on subscribe to replay active conditions; converts each `EventFieldList` to the adapter callback shape.
|
||
|
||
**Step 1: Write failing test** (mapper only):
|
||
|
||
```csharp
|
||
public class OpcUaAlarmMapperTests
|
||
{
|
||
[Fact]
|
||
public void MapSeverity_ClampsTo0_1000()
|
||
{
|
||
Assert.Equal(1000, OpcUaAlarmMapper.NormalizeSeverity(5000));
|
||
Assert.Equal(0, OpcUaAlarmMapper.NormalizeSeverity(-1));
|
||
Assert.Equal(500, OpcUaAlarmMapper.NormalizeSeverity(500));
|
||
}
|
||
|
||
[Fact]
|
||
public void BuildCondition_ActiveUnacked()
|
||
{
|
||
var c = OpcUaAlarmMapper.BuildCondition(active: true, acked: false, confirmed: null,
|
||
shelved: false, suppressed: false, severity: 700);
|
||
Assert.True(c.Active);
|
||
Assert.False(c.Acknowledged);
|
||
Assert.Equal(700, c.Severity);
|
||
}
|
||
|
||
[Fact]
|
||
public void DeriveKind_FromStateDelta_AckYieldsAcknowledge()
|
||
{
|
||
Assert.Equal(AlarmTransitionKind.Acknowledge,
|
||
OpcUaAlarmMapper.DeriveKind(prevAcked: false, nowAcked: true, prevActive: true, nowActive: true));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** `OpcUaAlarmMapper` (pure static helpers: `NormalizeSeverity`, `BuildCondition`, `DeriveKind`). Then wire `RealOpcUaClient`/`OpcUaDataConnection`: add `IOpcUaClient.CreateAlarmSubscriptionAsync(sourceFilter, conditionFilter, callback)` returning a handle; implement `OpcUaDataConnection.SubscribeAlarmsAsync` to call it and translate to `NativeAlarmTransition` via the mapper; trigger `ConditionRefresh` then emit `Snapshot`/`SnapshotComplete`.
|
||
|
||
**Step 4: Run test** → PASS (mapper). Build the project. (Live A&C exercised in Task 28.)
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs
|
||
git commit -m "feat(dcl): OPC UA Alarms & Conditions adapter (event subscription + ConditionRefresh)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: MxGateway StreamAlarms adapter
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 10, Task 11
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs` (add `RunAlarmStreamAsync`)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs` (consume package `StreamAlarmsAsync`; resumable; reconnect → snapshot)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs` (implement `IAlarmSubscribableConnection`; background loop; `RaiseDisconnected` on fault)
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayAlarmMapper.cs` (`AlarmFeedMessage`/`OnAlarmTransitionEvent` → `NativeAlarmTransition`)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/MxGatewayAlarmMapperTests.cs`
|
||
- Verify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.csproj` references a `ZB.MOM.WW.MxGateway.Client` version exposing `StreamAlarmsAsync` (bump if older; OtOpcUa's `GatewayGalaxyAlarmFeed` uses it).
|
||
|
||
**Step 1: Write failing test** (mapper, using the generated proto types from `ZB.MOM.WW.MxGateway.Contracts.Proto`):
|
||
|
||
```csharp
|
||
public class MxGatewayAlarmMapperTests
|
||
{
|
||
[Fact]
|
||
public void MapTransition_ActiveAcked_FromOnAlarmTransitionEvent()
|
||
{
|
||
// Build an OnAlarmTransitionEvent (ACK transition) and assert mapping.
|
||
var t = MxGatewayAlarmMapper.MapTransition(/* proto OnAlarmTransitionEvent w/ ACKNOWLEDGE */ ...);
|
||
Assert.Equal(AlarmTransitionKind.Acknowledge, t.Kind);
|
||
Assert.True(t.Condition.Acknowledged);
|
||
Assert.Equal("operator1", t.OperatorUser);
|
||
}
|
||
|
||
[Fact]
|
||
public void MapState_ActiveAcked_To_ActiveTrue_AckTrue()
|
||
{
|
||
var c = MxGatewayAlarmMapper.MapConditionState(/* ALARM_CONDITION_STATE_ACTIVE_ACKED */, severity: 600);
|
||
Assert.True(c.Active); Assert.True(c.Acknowledged); Assert.Equal(600, c.Severity);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** `MxGatewayAlarmMapper` (proto → `NativeAlarmTransition`; `active_alarm`→`Snapshot`, `snapshot_complete`→`SnapshotComplete`, `transition`→mapped). Add `IMxGatewayClient.RunAlarmStreamAsync(prefix, Action<NativeAlarmTransition>, ct)` and implement in `RealMxGatewayClient` over the package `StreamAlarmsAsync`. Implement `MxGatewayDataConnection.SubscribeAlarmsAsync` (start background loop; `RaiseDisconnected` once-only on fault; the resumed stream re-sends a snapshot).
|
||
|
||
**Step 4: Run test** → PASS. Build.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayAlarmMapper.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/MxGatewayAlarmMapperTests.cs
|
||
git commit -m "feat(dcl): MxGateway StreamAlarms adapter (snapshot + live transitions)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13: SiteRuntimeOptions — alarm cap + retry
|
||
|
||
**Classification:** trivial
|
||
**Estimated implement time:** ~2 min
|
||
**Parallelizable with:** Task 14
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs`
|
||
|
||
**Step 1–3:** Add:
|
||
|
||
```csharp
|
||
/// <summary>Max mirrored native alarms retained per source binding before older entries are dropped (logged).</summary>
|
||
public int MirroredAlarmCapPerSource { get; set; } = 1000;
|
||
/// <summary>Interval to retry a failed native alarm subscription.</summary>
|
||
public int NativeAlarmRetryIntervalMs { get; set; } = 5000;
|
||
```
|
||
|
||
**Step 4: Build** `dotnet build ZB.MOM.WW.ScadaBridge.slnx` → OK (bound from `ScadaBridge:SiteRuntime`).
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs
|
||
git commit -m "feat(siteruntime): native alarm cap + retry options"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: Site SQLite NativeAlarmState store
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 13
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs` (create table in `InitializeAsync`; add `UpsertNativeAlarmAsync`, `DeleteNativeAlarmAsync`, `GetNativeAlarmsAsync(instance, sourceCanonical)`, `ClearNativeAlarmsForInstanceAsync`)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/NativeAlarmStateStoreTests.cs`
|
||
|
||
**Schema** (raw SQL, mirrors `static_attribute_overrides`): `native_alarm_state(instance_unique_name TEXT, source_canonical_name TEXT, source_reference TEXT, condition_json TEXT, last_transition_at TEXT, PRIMARY KEY(instance_unique_name, source_canonical_name, source_reference))`.
|
||
|
||
**Step 1: Write failing test:**
|
||
|
||
```csharp
|
||
public class NativeAlarmStateStoreTests : IDisposable
|
||
{
|
||
private readonly string _db = Path.Combine(Path.GetTempPath(), $"nas-{Guid.NewGuid():N}.db");
|
||
private readonly SiteStorageService _s;
|
||
public NativeAlarmStateStoreTests()
|
||
{ _s = new SiteStorageService($"Data Source={_db}", NullLogger<SiteStorageService>.Instance); _s.InitializeAsync().GetAwaiter().GetResult(); }
|
||
|
||
[Fact]
|
||
public async Task Upsert_Then_Get_RoundTrips()
|
||
{
|
||
await _s.UpsertNativeAlarmAsync("inst","Src","Tank01.Hi","{\"Active\":true}", DateTimeOffset.UnixEpoch);
|
||
var rows = await _s.GetNativeAlarmsAsync("inst","Src");
|
||
Assert.Single(rows);
|
||
Assert.Equal("Tank01.Hi", rows[0].SourceReference);
|
||
}
|
||
public void Dispose() { if (File.Exists(_db)) File.Delete(_db); }
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** the table creation in `InitializeAsync` and the four methods following the `SetStaticOverrideAsync` raw-SQL `ON CONFLICT ... DO UPDATE` pattern. Return a small `NativeAlarmRow(SourceReference, ConditionJson, LastTransitionAt)` record.
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/NativeAlarmStateStoreTests.cs
|
||
git commit -m "feat(siteruntime): site SQLite native_alarm_state store"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15: NativeAlarmActor
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** none
|
||
|
||
**Files:**
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs`
|
||
|
||
**Behavior:** ctor `(ResolvedNativeAlarmSource source, string instanceName, IActorRef instanceActor, IActorRef dclManager, SiteStorageService storage, SiteRuntimeOptions options, ILogger)`. `PreStart`: rehydrate `_alarms` from `storage.GetNativeAlarmsAsync(instanceName, source.CanonicalName)` (PipeTo), then `dclManager.Tell(new SubscribeAlarmsRequest(...))`. On `NativeAlarmTransitionUpdate`: for `Snapshot` buffer; `SnapshotComplete` atomic-swap (drop missing → emit return-to-normal `AlarmStateChanged`); `Raise/Ack/Clear/Retrigger/StateChange` upsert by `SourceReference` (ignore older `TransitionTime`), persist, emit enriched `AlarmStateChanged` (`Kind = NativeOpcUa/NativeMxAccess`, `Condition`, `SourceReference`, etc.) to `instanceActor`. Retention: drop when `!Active && Acknowledged`. On `NativeAlarmSourceUnavailable`: mark `_alarms` uncertain (re-emit with a quality flag — represent "uncertain" by leaving values but flipping a derived display; minimal: re-emit current condition unchanged but log + set health). Cap `_alarms` at `options.MirroredAlarmCapPerSource` and **log** when dropping (no silent truncation). On `SubscribeAlarmsResponse(false,…)`: schedule a retry timer at `NativeAlarmRetryIntervalMs`.
|
||
|
||
**Step 1: Write failing test** (probe as instanceActor, NSubstitute/`TestProbe` as dclManager, temp-file `SiteStorageService`):
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void Raise_EmitsEnrichedAlarmStateChanged()
|
||
{
|
||
var instanceProbe = CreateTestProbe();
|
||
var dclProbe = CreateTestProbe();
|
||
var src = new ResolvedNativeAlarmSource { CanonicalName="Pressure", ConnectionName="Opc", SourceReference="ns=2;s=T01" };
|
||
var actor = ActorOf(Props.Create(() => new NativeAlarmActor(src,"inst",instanceProbe.Ref,dclProbe.Ref,_storage,_options,NullLogger<NativeAlarmActor>.Instance)));
|
||
|
||
dclProbe.ExpectMsg<SubscribeAlarmsRequest>(); // subscribed on start
|
||
var t = new NativeAlarmTransition("T01.Hi","T01","AnalogLimit.Hi",AlarmTransitionKind.Raise,
|
||
new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,800),"Process","hi","hi","","",null,DateTimeOffset.UtcNow,"92","90");
|
||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", t));
|
||
|
||
var emitted = instanceProbe.ExpectMsg<AlarmStateChanged>();
|
||
Assert.Equal(AlarmKind.NativeOpcUa, emitted.Kind);
|
||
Assert.Equal("T01.Hi", emitted.SourceReference);
|
||
Assert.Equal(800, emitted.Condition.Severity);
|
||
}
|
||
```
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** `NativeAlarmActor` per the behavior above. Derive `Kind` from the connection protocol (pass protocol in via the resolved source's connection, or set `NativeOpcUa` when `SourceReference` looks like a NodeId — simplest: pass a `Kind` hint through the subscribe response; pragmatic first cut: set `Kind = NativeOpcUa` and refine in Task 16 once protocol is known from the connection config). Keep `MapTransitionToStateChanged` a private pure method.
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs
|
||
git commit -m "feat(siteruntime): NativeAlarmActor mirrors source alarms (snapshot swap, retention, persistence)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 16: InstanceActor wiring
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** none
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs` (create `NativeAlarmActor` children from `_configuration.NativeAlarmSources`; pass `_dclManager`; ensure `HandleAlarmStateChanged` stores enriched state + include native alarms in `DebugViewSnapshot`)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs`
|
||
|
||
**Detail:** After the `foreach (var alarm in _configuration.Alarms)` block (~line 748), add a `foreach (var src in _configuration.NativeAlarmSources)` that does `Context.ActorOf(Props.Create(() => new NativeAlarmActor(src, _instanceUniqueName, Self, _dclManager, _storage, _options, _logger)), $"native-alarm-{src.CanonicalName}")` and tracks it in a `_nativeAlarmActors` dictionary. `HandleAlarmStateChanged` already routes through `_alarmStates`/`_streamManager.PublishAlarmStateChanged`; extend it to also retain the latest enriched `AlarmStateChanged` (store the whole record in a `Dictionary<string, AlarmStateChanged> _latestAlarmEvents` keyed by `AlarmName`) so the snapshot can carry enriched fields. In `HandleSubscribeDebugView`, build `alarmStates` from `_latestAlarmEvents.Values` (falling back to the computed projection for alarms with no event yet). Supervision (Resume for children) already covers `NativeAlarmActor`. On redeploy/stop, `ClearNativeAlarmsForInstanceAsync`.
|
||
|
||
**Step 1: Write failing test:**
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void DebugViewSnapshot_IncludesNativeAlarm_AfterTransition()
|
||
{
|
||
var config = new FlattenedConfiguration {
|
||
InstanceUniqueName = "inst",
|
||
NativeAlarmSources = [ new ResolvedNativeAlarmSource { CanonicalName="Pressure", ConnectionName="Opc", SourceReference="ns=2;s=T01" } ]
|
||
};
|
||
var actor = CreateInstanceActorWithDcl("inst", config, dclProbe.Ref);
|
||
// simulate the NativeAlarmActor emitting upward:
|
||
actor.Tell(new AlarmStateChanged("inst","T01.Hi",AlarmState.Active,800,DateTimeOffset.UtcNow)
|
||
{ Kind = AlarmKind.NativeOpcUa, SourceReference="T01.Hi",
|
||
Condition = new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,800) });
|
||
actor.Tell(new SubscribeDebugViewRequest("c","inst",DateTimeOffset.UtcNow));
|
||
var snap = ExpectMsg<DebugViewSnapshot>();
|
||
Assert.Contains(snap.AlarmStates, a => a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa);
|
||
}
|
||
```
|
||
|
||
(Extend the test helper to pass a `_dclManager` probe — add an `InstanceActor` ctor overload or thread the probe through `CreateInstanceActor`.)
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** the wiring + `_latestAlarmEvents` retention + enriched snapshot.
|
||
|
||
**Step 4: Run test** → PASS. Run full SiteRuntime.Tests.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs
|
||
git commit -m "feat(siteruntime): InstanceActor spawns NativeAlarmActors + enriched alarm snapshot"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 17: Enrich computed AlarmActor emit
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~3 min
|
||
**Parallelizable with:** Task 18
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs` (set `Condition`/`Kind` on each `AlarmStateChanged` it constructs)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/AlarmActorTests.cs` (add assertion)
|
||
|
||
**Detail:** At each `new AlarmStateChanged(...)` site (binary activate/clear ~lines 207–227; HiLo ~260–266), add the init block:
|
||
|
||
```csharp
|
||
{ Kind = AlarmKind.Computed, Condition = AlarmConditionStateFactory.ForComputed(state, _priority), Level = ..., Message = ... }
|
||
```
|
||
|
||
(The `Condition` getter already defaults to this, so the explicit set is belt-and-suspenders + makes intent clear; minimum change is just confirming `Kind = Computed` which is the default. Add a regression test asserting `Condition.Severity == priority`.)
|
||
|
||
**Step 1: Add failing assertion** to an existing activation test: `Assert.Equal(msg.Priority, msg.Condition.Severity);` and `Assert.Equal(AlarmKind.Computed, msg.Kind);`
|
||
|
||
**Step 2: Run test** → likely PASS already via defaults; if so this task reduces to adding the regression test. If a code path constructs `AlarmStateChanged` in a way that doesn't reflect `_priority`, fix it.
|
||
|
||
**Step 3–4:** Ensure pass.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/AlarmActorTests.cs
|
||
git commit -m "test(siteruntime): assert computed alarms carry unified condition state"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 18: Extend sitestream.proto + regenerate
|
||
|
||
**Classification:** high-risk
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 17
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto` (append fields 8–21 to `AlarmStateUpdate`)
|
||
- Modify (generated): `src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs` (+ any `*Grpc.cs`)
|
||
- Modify (toggle, then revert): `src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj`
|
||
|
||
**Step 1: Edit the proto** — append to `AlarmStateUpdate` (do NOT renumber existing 1–7):
|
||
|
||
```protobuf
|
||
string kind = 8;
|
||
bool active = 9;
|
||
bool acknowledged = 10;
|
||
bool confirmed = 11;
|
||
string shelve_state = 12;
|
||
bool suppressed = 13;
|
||
string source_reference = 14;
|
||
string alarm_type_name = 15;
|
||
string category = 16;
|
||
string operator_user = 17;
|
||
string operator_comment = 18;
|
||
google.protobuf.Timestamp original_raise_time = 19;
|
||
string current_value = 20;
|
||
string limit_value = 21;
|
||
```
|
||
|
||
**Step 2: Regenerate (macOS only — the documented manual process):**
|
||
1. In the `.csproj`, uncomment the `<Protobuf Include="Protos\sitestream.proto" GrpcServices="Both" />` block.
|
||
2. Temporarily delete/rename the checked-in generated files under `SiteStreamGrpc/`.
|
||
3. `dotnet build src/ZB.MOM.WW.ScadaBridge.Communication` (protoc runs on macOS arm64; fails on arm64 Linux — that's why generated files are vendored).
|
||
4. Copy generated `Sitestream.cs` / `SitestreamGrpc.cs` from `obj/Debug/net10.0/Protos/` to `SiteStreamGrpc/`.
|
||
5. Re-comment the `<Protobuf>` block in the `.csproj`.
|
||
|
||
**Step 3: Verify** `dotnet build ZB.MOM.WW.ScadaBridge.slnx` compiles and the generated `AlarmStateUpdate` exposes the new properties (e.g. `SourceReference`, `Acknowledged`).
|
||
|
||
**Step 4:** No new unit test here (mapping is Task 19). Build is the gate.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj
|
||
git commit -m "feat(communication): extend AlarmStateUpdate proto with native alarm fields (regenerated)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 19: gRPC alarm mapping (server + client)
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** none
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs` (`HandleAlarmStateChanged` — map new fields out)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs` (`ConvertToDomainEvent` — map new fields back)
|
||
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs` (string ↔ `AlarmShelveState`)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs` (extend round-trip)
|
||
|
||
**Step 1: Write failing test** — extend `RelaysAlarmStateChanged_ToProtoEvent`:
|
||
|
||
```csharp
|
||
var domainEvent = new AlarmStateChanged("Site1.Pump01","T01.Hi",AlarmState.Active,2,timestamp)
|
||
{
|
||
Kind = AlarmKind.NativeMxAccess, SourceReference = "T01.Hi", Category = "Process",
|
||
OperatorUser = "op1", OperatorComment = "ack",
|
||
Condition = new AlarmConditionState(true, true, null, AlarmShelveState.OneShotShelved, false, 900)
|
||
};
|
||
actor.Tell(domainEvent);
|
||
// ...read protoEvent...
|
||
Assert.Equal("T01.Hi", protoEvent.AlarmChanged.SourceReference);
|
||
Assert.True(protoEvent.AlarmChanged.Acknowledged);
|
||
Assert.Equal(900, protoEvent.AlarmChanged.Suppressed ? 0 : 900); // severity via priority->condition mapping
|
||
Assert.Equal("OneShotShelved", protoEvent.AlarmChanged.ShelveState);
|
||
Assert.Equal("op1", protoEvent.AlarmChanged.OperatorUser);
|
||
```
|
||
|
||
Plus a client-side test: build an `AlarmStateUpdate` with the new fields and assert `ConvertToDomainEvent` populates `Kind`, `Condition`, `SourceReference`, etc.
|
||
|
||
**Step 2: Run test** → FAIL.
|
||
|
||
**Step 3: Implement** — In `StreamRelayActor.HandleAlarmStateChanged`, set the new proto fields from `msg.Condition`/`msg.Kind`/`msg.SourceReference`/… (severity → `priority` stays; also map `active/acknowledged/confirmed/suppressed`, `shelve_state` via codec, `original_raise_time` via `Timestamp.FromDateTimeOffset` when non-null). In `SiteStreamGrpcClient.ConvertToDomainEvent`, build the `AlarmStateChanged` init block with `Kind = ParseKind(...)`, `Condition = new AlarmConditionState(active, acknowledged, confirmed?, codec.Parse(shelve_state), suppressed, priority)`, and the metadata strings. Add `AlarmShelveStateCodec`.
|
||
|
||
**Step 4: Run test** → PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs
|
||
git commit -m "feat(communication): map enriched alarm fields across gRPC (server + client)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 20: Management command contracts + registry
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 5, Task 8
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs` (add native-alarm-source commands)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs` (add override commands)
|
||
- Modify: the management command registry (find via `ManagementCommandRegistry` — referenced by `tests/.../ManagementCommandRegistryTests.cs`) to register the new command type names
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs` (assert new commands resolve)
|
||
|
||
**Contracts** (mirror `AddTemplateAlarmCommand` / `SetInstanceAlarmOverrideCommand`):
|
||
|
||
```csharp
|
||
// TemplateCommands.cs
|
||
public record AddTemplateNativeAlarmSourceCommand(int TemplateId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked);
|
||
public record UpdateTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked);
|
||
public record DeleteTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId);
|
||
public record ListTemplateNativeAlarmSourcesCommand(int TemplateId);
|
||
|
||
// InstanceCommands.cs
|
||
public record SetInstanceNativeAlarmSourceOverrideCommand(int InstanceId, string SourceCanonicalName, string? ConnectionNameOverride, string? SourceReferenceOverride, string? ConditionFilterOverride);
|
||
public record DeleteInstanceNativeAlarmSourceOverrideCommand(int InstanceId, string SourceCanonicalName);
|
||
public record ListInstanceNativeAlarmSourceOverridesCommand(int InstanceId);
|
||
```
|
||
|
||
**Step 1: Write failing test** asserting each new command name resolves through the registry. **Step 2:** FAIL. **Step 3:** add the records + register them. **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs
|
||
git commit -m "feat(commons): management command contracts for native alarm sources"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 21: ManagementActor handlers
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 22, Task 24, Task 25
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/` (the `ManagementActor` and/or its command handler partials — find where `AddTemplateAlarmCommand`/`SetInstanceAlarmOverrideCommand` are handled and add the parallel handlers, calling the Task 6 repository methods within the existing unit-of-work)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs` (CRUD round-trips, incl. authorization: Design role for template, Deployment for instance override — match the existing alarm commands' role checks)
|
||
|
||
**Step 1: Write failing tests** mirroring the existing template-alarm + instance-alarm-override handler tests (Add returns `ManagementSuccess`; unauthorized role returns `ManagementUnauthorized`; List returns the rows). **Step 2:** FAIL. **Step 3:** implement handlers + role checks + repository calls + `SaveChangesAsync`. **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.ManagementService tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs
|
||
git commit -m "feat(management): handlers for native alarm source CRUD"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 22: CLI commands
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 21, Task 24, Task 25
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs` (add a `native-alarm-source` compound subcommand with `add`/`list`/`remove`, mirroring `BuildAttribute`)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs` (add `native-alarm-source set`/`clear`)
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` (document new commands)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs` (assert tree builds + commands resolve)
|
||
|
||
**Step 1: Write failing test** asserting the command tree includes `template native-alarm-source add/list/remove` and `instance native-alarm-source set/clear` and the underlying management commands resolve. **Step 2:** FAIL. **Step 3:** implement via `cmd.SetAction(... CommandHelpers.ExecuteCommandAsync(... AddTemplateNativeAlarmSourceCommand ...))` per the `DataConnectionCommands` pattern. **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.CLI tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs
|
||
git commit -m "feat(cli): native-alarm-source commands (template + instance)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 23: DebugView alarm table enrichment
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 24, Task 25
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor` (add columns/badges; the model already is `AlarmStateChanged`, now enriched)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/DebugViewAlarmTableTests.cs` (bUnit)
|
||
|
||
**Detail:** Add header cells (Severity, State badge set, Kind, Source, Type, Category) and body cells; render a composite condition badge from `AlarmConditionState` (Active→danger, Acked→muted, Shelved/Suppressed→info), Kind badge (Computed vs Native), and tooltips for operator/comment, raise time, current/limit value. Use the `frontend-design` skill for styling; Bootstrap only. Keep `UpsertWithCap`/filter logic; extend `FilteredAlarmStates` to also match `SourceReference`.
|
||
|
||
**Step 1: Write failing bUnit test** — render `DebugView` with a seeded snapshot containing one native `AlarmStateChanged` (Kind=NativeOpcUa, Severity=800, Shelved) and assert the rendered markup shows the severity and a shelved badge. **Step 2:** FAIL. **Step 3:** implement. **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/DebugViewAlarmTableTests.cs
|
||
git commit -m "feat(ui): enrich DebugView alarm table with severity + condition state + native metadata"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 24: Template editor — Native Alarm Sources subsection
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 23, Task 25
|
||
|
||
**Files:**
|
||
- Modify: the template detail/edit page (`src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor` and/or `TemplateEdit.razor` — locate the Alarms subsection during impl)
|
||
- Create (optional): `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/NativeAlarmSourceEditor.razor`
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplateNativeAlarmSourceEditorTests.cs`
|
||
|
||
**Detail:** A subsection listing native alarm sources with add/edit/delete; row fields: Name, Connection (dropdown filtered to alarm-capable protocols — reuse the connection-dropdown pattern from `InstanceConfigure`), Source Reference, optional Filter, Lock toggle. Wire add/edit/delete to the Task 20 management commands via the existing management HTTP client. Respect lock/inherit display like the Alarms section.
|
||
|
||
**Step 1: Write failing bUnit test** (renders the subsection, simulates add, asserts the management command is posted). **Step 2:** FAIL. **Step 3:** implement (frontend-design skill). **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplateNativeAlarmSourceEditorTests.cs
|
||
git commit -m "feat(ui): template editor Native Alarm Sources subsection"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 25: Instance Configure — native alarm source override panel
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 23, Task 24
|
||
|
||
**Files:**
|
||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor` (new card after Alarm Overrides)
|
||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/InstanceConfigureNativeAlarmTests.cs`
|
||
|
||
**Detail:** A card listing the instance's resolved native alarm sources with per-row override of Connection/Source Reference/Filter (blank = inherited), saving via `SetInstanceNativeAlarmSourceOverrideCommand`; clear via `DeleteInstanceNativeAlarmSourceOverrideCommand`. Mirror the Connection Bindings + Alarm Overrides UX.
|
||
|
||
**Step 1: Write failing bUnit test** (renders card, edits a source-ref override, asserts the set command posts). **Step 2:** FAIL. **Step 3:** implement. **Step 4:** PASS.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/InstanceConfigureNativeAlarmTests.cs
|
||
git commit -m "feat(ui): instance configure native alarm source override panel"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 26: docker-env2 seed — sample native alarm source
|
||
|
||
**Classification:** small
|
||
**Estimated implement time:** ~4 min
|
||
**Parallelizable with:** Task 27
|
||
|
||
**Files:**
|
||
- Modify: the `docker-env2` seed (the script/CLI calls that created the `ScadaBridge Site <X>` MxGateway connection — find under `docker-env2/`)
|
||
- Modify (if needed): `docker-env2/README.md`
|
||
|
||
**Detail:** After the existing MxGateway connection seed, add a template `native-alarm-source add` (connection = the seeded MxGateway connection, a representative source reference / MxAccess area) and deploy an instance, so the feature is manually verifiable end-to-end. Use the CLI commands from Task 22.
|
||
|
||
**Step 1–3:** Add the seed lines; `bash docker-env2/deploy.sh` (or the seed step) runs without error. **Step 4:** Verify the connection + binding exist via `scadabridge ... template native-alarm-source list`.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add docker-env2
|
||
git commit -m "chore(docker-env2): seed sample native alarm source binding on site-x"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 27: Documentation sync
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min
|
||
**Parallelizable with:** Task 26
|
||
|
||
**Files:**
|
||
- Modify: `docs/requirements/Component-DataConnectionLayer.md`, `Component-SiteRuntime.md`, `Component-TemplateEngine.md`, `Component-CentralUI.md`, `Component-CLI.md`, `Component-Communication.md` (a.k.a. Central–Site Communication), `Component-ConfigurationDatabase.md`
|
||
- Modify: `README.md` (component table notes if needed), `CLAUDE.md` (add a "Native Alarms" bullet under Key Design Decisions → Data & Communication)
|
||
|
||
**Detail:** Document the read-only native alarm mirror: the `IAlarmSubscribableConnection` seam + per-connection feed + source-ref routing (DCL doc); `NativeAlarmActor` + `native_alarm_state` SQLite + enriched `AlarmStateChanged` (SiteRuntime doc); `ResolvedNativeAlarmSource` flattening + validation (TemplateEngine doc); enriched `AlarmStateUpdate` proto (Communication doc); new entities/migration/repo (ConfigurationDatabase doc); DebugView enrichment + authoring panels (CentralUI doc); CLI commands (CLI doc). Keep cross-references consistent per CLAUDE.md editing rules.
|
||
|
||
**Step 1–3:** Edit docs. **Step 4:** Re-read to confirm no stale cross-references. **Step 5: Commit**
|
||
|
||
```bash
|
||
git add docs README.md CLAUDE.md
|
||
git commit -m "docs: native alarm ingestion across component docs + CLAUDE.md"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 28: Integration / live verification
|
||
|
||
**Classification:** standard
|
||
**Estimated implement time:** ~5 min (plus manual)
|
||
**Parallelizable with:** none
|
||
|
||
**Files:**
|
||
- Create: `tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs` (SkippableFact — skips when no alarm-capable OPC UA server is reachable, mirroring the existing live OPC UA smoke pattern)
|
||
- Modify (if needed): `docs/test_infra/test_infra.md` (note A&C requirement)
|
||
|
||
**Detail:** Add a SkippableFact that, against an alarm-capable OPC UA endpoint, subscribes via the adapter and asserts a `Snapshot`/`SnapshotComplete` sequence arrives (skip if the infra OPC UA server does not expose Alarms & Conditions). **Document** in `test_infra.md` whether the infra OPC UA server exposes A&C; if not, note the simulation/external-server requirement (open item from the design). Then perform the manual end-to-end check:
|
||
|
||
```bash
|
||
dotnet build ZB.MOM.WW.ScadaBridge.slnx
|
||
bash docker/deploy.sh # or docker-env2/deploy.sh for the MxGateway path
|
||
# Open Central UI DebugView for the seeded instance; confirm native alarms appear with severity + condition badges.
|
||
```
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs docs/test_infra/test_infra.md
|
||
git commit -m "test(dcl): OPC UA A&C live smoke (skippable) + test-infra note"
|
||
```
|
||
|
||
---
|
||
|
||
## Final verification (after all tasks)
|
||
|
||
```bash
|
||
dotnet build ZB.MOM.WW.ScadaBridge.slnx
|
||
dotnet test ZB.MOM.WW.ScadaBridge.slnx
|
||
bash docker/deploy.sh # rebuild cluster image for runtime changes (DCL/SiteRuntime/Communication/Host)
|
||
```
|
||
|
||
Confirm: native alarms appear in DebugView with severity + condition badges (Active/Acked/Shelved/Suppressed), computed alarms unchanged, no central tables added, and the migration applies cleanly.
|
||
|
||
## Open items carried from design
|
||
- MxGateway `StreamAlarms` must deliver end-to-end (x86 COM-worker caveat from OtOpcUa) — verified in Task 28 manual check.
|
||
- Infra OPC UA server A&C support — resolved/noted in Task 28.
|
||
- `ZB.MOM.WW.MxGateway.Client` package must expose `StreamAlarmsAsync` — checked in Task 12.
|