feat(native-alarms): per-binding IsMesReportable on native alarm source bindings (ScadaBridge#59)

TemplateNativeAlarmSource.IsMesReportable + tri-state InstanceNativeAlarmSourceOverride
.IsMesReportableOverride flatten onto ResolvedNativeAlarmSource; NativeAlarmActor stamps every
mirrored condition and InstanceActor the quiet-binding placeholder, so ScriptAlarm,
Alarms.MesReportableAsync() and the #60 wire fields carry it. Additive to the 900-999 severity band.
Migration AddNativeAlarmSourceMesReportable. Management commands, CLI --mes-reportable, Central UI
(template dialog checkbox, instance override select), Transport DTOs/importer/diff and the revision
hash (true-only, so existing hashes are unchanged) carry it.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:09:54 -05:00
parent 69a2017000
commit f98369a786
38 changed files with 2579 additions and 43 deletions
@@ -651,9 +651,10 @@ public static class InstanceCommands
var setConnectionOption = new Option<string?>("--connection") { Description = "Connection name override (blank = inherited)" };
var setSourceRefOption = new Option<string?>("--source-ref") { Description = "Source reference override (blank = inherited)" };
var setFilterOption = new Option<string?>("--filter") { Description = "Condition filter override (blank = inherited)" };
var setMesOption = new Option<bool?>("--mes-reportable") { Description = "MES-reportable override for the binding: true or false; omit to inherit" };
var setCmd = new Command("set") { Description = "Set (upsert) a native alarm source override on an instance" };
setCmd.Add(setIdOption); setCmd.Add(setSourceOption); setCmd.Add(setConnectionOption);
setCmd.Add(setSourceRefOption); setCmd.Add(setFilterOption);
setCmd.Add(setSourceRefOption); setCmd.Add(setFilterOption); setCmd.Add(setMesOption);
setCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -663,7 +664,8 @@ public static class InstanceCommands
result.GetValue(setSourceOption)!,
result.GetValue(setConnectionOption),
result.GetValue(setSourceRefOption),
result.GetValue(setFilterOption)));
result.GetValue(setFilterOption),
result.GetValue(setMesOption)));
});
group.Add(setCmd);
@@ -180,6 +180,7 @@ public static class TemplateCommands
var filterOption = new Option<string?>("--filter") { Description = "Condition filter; omit to clear and mirror all conditions" };
var descriptionOption = new Option<string?>("--description") { Description = "Description; omit to clear" };
var lockedOption = new Option<bool>("--locked") { Description = "Lock status; omit for unlocked" };
var mesOption = new Option<bool>("--mes-reportable") { Description = "Every condition under this binding is MES-reportable (additive to the 900-999 severity band); omit for false" };
var cmd = new Command("update") { Description = "Replace the fields of a native alarm source binding" };
cmd.Add(idOption);
cmd.Add(nameOption);
@@ -188,6 +189,7 @@ public static class TemplateCommands
cmd.Add(filterOption);
cmd.Add(descriptionOption);
cmd.Add(lockedOption);
cmd.Add(mesOption);
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -195,7 +197,8 @@ public static class TemplateCommands
new UpdateTemplateNativeAlarmSourceCommand(
result.GetValue(idOption), result.GetValue(nameOption)!,
result.GetValue(connectionOption)!, result.GetValue(sourceOption)!,
result.GetValue(filterOption), result.GetValue(descriptionOption), result.GetValue(lockedOption)));
result.GetValue(filterOption), result.GetValue(descriptionOption), result.GetValue(lockedOption),
result.GetValue(mesOption)));
});
return cmd;
}
@@ -838,6 +841,8 @@ public static class TemplateCommands
var descOption = new Option<string?>("--description") { Description = "Description" };
var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
var mesOption = new Option<bool>("--mes-reportable") { Description = "Every condition under this binding is MES-reportable (additive to the 900-999 severity band)" };
mesOption.DefaultValueFactory = _ => false;
var addCmd = new Command("add") { Description = "Add a native alarm source binding to a template" };
addCmd.Add(templateIdOption);
@@ -847,6 +852,7 @@ public static class TemplateCommands
addCmd.Add(filterOption);
addCmd.Add(descOption);
addCmd.Add(lockedOption);
addCmd.Add(mesOption);
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -858,7 +864,8 @@ public static class TemplateCommands
result.GetValue(sourceRefOption)!,
result.GetValue(filterOption),
result.GetValue(descOption),
result.GetValue(lockedOption)));
result.GetValue(lockedOption),
result.GetValue(mesOption)));
});
group.Add(addCmd);
+6 -4
View File
@@ -333,7 +333,7 @@ Bind a native alarm source to a template — a read-only mirror of an OPC UA Ala
Conditions server's or the MxAccess Gateway's alarms (no ack-back).
```sh
scadabridge --url <url> template native-alarm-source add --template-id <int> --name <string> --connection <string> --source-ref <string> [--filter <string>] [--description <string>] [--locked]
scadabridge --url <url> template native-alarm-source add --template-id <int> --name <string> --connection <string> --source-ref <string> [--filter <string>] [--description <string>] [--locked] [--mes-reportable]
```
| Option | Required | Description |
@@ -345,6 +345,7 @@ scadabridge --url <url> template native-alarm-source add --template-id <int> --n
| `--filter` | no | Condition filter (omit to mirror all conditions under the source) |
| `--description` | no | Description |
| `--locked` | no | Lock the binding in derived templates |
| `--mes-reportable` | no | Every condition under this binding is MES-reportable (additive to the 900-999 severity band) |
#### `template native-alarm-source list`
@@ -359,10 +360,10 @@ scadabridge --url <url> template native-alarm-source list --template-id <int>
Replace the fields of an existing native alarm source binding.
```sh
scadabridge template native-alarm-source update --id <source-id> --name <name> --connection <name> --source-ref <reference> [--filter <expression>] [--description <text>] [--locked]
scadabridge template native-alarm-source update --id <source-id> --name <name> --connection <name> --source-ref <reference> [--filter <expression>] [--description <text>] [--locked] [--mes-reportable]
```
The ID, name, connection, and source reference are required. Omitted filter and description clear those values; omitted `--locked` means unlocked. Requires Designer access; the server enforces locking rules.
The ID, name, connection, and source reference are required. Omitted filter and description clear those values; omitted `--locked` means unlocked and omitted `--mes-reportable` means not MES-reportable. Requires Designer access; the server enforces locking rules.
#### `template native-alarm-source remove`
@@ -621,7 +622,7 @@ Override an inherited native alarm source binding for a single instance (upsert)
options keep the inherited value.
```sh
scadabridge --url <url> instance native-alarm-source set --instance-id <int> --source <string> [--connection <string>] [--source-ref <string>] [--filter <string>]
scadabridge --url <url> instance native-alarm-source set --instance-id <int> --source <string> [--connection <string>] [--source-ref <string>] [--filter <string>] [--mes-reportable <true|false>]
```
| Option | Required | Description |
@@ -631,6 +632,7 @@ scadabridge --url <url> instance native-alarm-source set --instance-id <int> --s
| `--connection` | no | Connection name override (blank = inherited) |
| `--source-ref` | no | Source reference override (blank = inherited) |
| `--filter` | no | Condition filter override (blank = inherited) |
| `--mes-reportable` | no | MES-reportable override for the binding: `true` or `false` (omit = inherited) |
#### `instance native-alarm-source clear`
@@ -653,6 +653,7 @@
<th style="width: 220px;">Connection override</th>
<th style="width: 220px;">Source reference override</th>
<th style="width: 170px;">Filter override</th>
<th style="width: 150px;">MES reportable</th>
<th style="width: 140px;">Actions</th>
</tr>
</thead>
@@ -697,6 +698,15 @@
value="@(_nasFilterEdit.GetValueOrDefault(src.Name) ?? "")"
@onchange="e => _nasFilterEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" />
</td>
<td>
<select class="form-select form-select-sm" data-test="@($"nas-mes-{src.Name}")"
value="@MesText(_nasMesEdit.GetValueOrDefault(src.Name))"
@onchange="e => _nasMesEdit[src.Name] = ParseMes((string?)e.Value)">
<option value="" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == null)">Inherited (@(src.IsMesReportable ? "reportable" : "not reportable"))</option>
<option value="true" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == true)">Reportable</option>
<option value="false" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == false)">Not reportable</option>
</select>
</td>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="@($"Override actions for {src.Name}")">
<button class="btn btn-success"
@@ -843,6 +853,9 @@
private Dictionary<string, string?> _nasConnEdit = new();
private Dictionary<string, string?> _nasRefEdit = new();
private Dictionary<string, string?> _nasFilterEdit = new();
private Dictionary<string, bool?> _nasMesEdit = new();
private static string MesText(bool? v) => v switch { true => "true", false => "false", _ => "" };
private static bool? ParseMes(string? v) => v switch { "true" => true, "false" => false, _ => null };
// Native-alarm-source CSV bulk-import result summary — same shape as the
// attribute importer's (_csvImport*) trio. Null until an import runs.
@@ -971,6 +984,7 @@
_nasConnEdit[s.Name] = ovr?.ConnectionNameOverride;
_nasRefEdit[s.Name] = ovr?.SourceReferenceOverride;
_nasFilterEdit[s.Name] = ovr?.ConditionFilterOverride;
_nasMesEdit[s.Name] = ovr?.IsMesReportableOverride;
}
_flattenedAttributes = await BuildFlattenedAttributesAsync();
@@ -1751,16 +1765,17 @@
var conn = Blank(_nasConnEdit.GetValueOrDefault(sourceName));
var sref = Blank(_nasRefEdit.GetValueOrDefault(sourceName));
var filt = Blank(_nasFilterEdit.GetValueOrDefault(sourceName));
var mes = _nasMesEdit.GetValueOrDefault(sourceName);
// All blank → no override; clear any existing row.
if (conn == null && sref == null && filt == null)
if (conn == null && sref == null && filt == null && mes == null)
{
await ClearNativeOverrideCore(sourceName);
_toast.ShowSuccess($"No override on '{sourceName}' (inherited).");
return;
}
await UpsertNativeOverrideCore(sourceName, conn, sref, filt);
await UpsertNativeOverrideCore(sourceName, conn, sref, filt, mes);
_toast.ShowSuccess($"Saved native alarm source override on '{sourceName}'.");
}
catch (Exception ex)
@@ -1791,15 +1806,16 @@
/// so a bulk import commits the whole batch in one go. Shared by the inline Save
/// and the CSV import so both take the identical persistence path.
/// </summary>
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt)
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt, bool? mes = null)
{
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt));
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt, mes));
if (result is InstanceNativeAlarmSourceOverride saved) _existingNativeOverrides[sourceName] = saved;
// Keep the inline editor showing what was just persisted.
_nasConnEdit[sourceName] = conn;
_nasRefEdit[sourceName] = sref;
_nasFilterEdit[sourceName] = filt;
_nasMesEdit[sourceName] = mes;
}
private async Task ClearNativeOverrideCore(string sourceName)
@@ -1809,6 +1825,7 @@
_nasConnEdit[sourceName] = null;
_nasRefEdit[sourceName] = null;
_nasFilterEdit[sourceName] = null;
_nasMesEdit[sourceName] = null;
}
private static string? Blank(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim();
@@ -1868,7 +1885,8 @@
}
await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverridesCommand(Id,
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter)).ToList()));
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter,
_existingNativeOverrides.GetValueOrDefault(row.SourceName)?.IsMesReportableOverride)).ToList()));
await OnInitializedAsync();
_nasCsvImportSucceeded = true;
@@ -1041,7 +1041,7 @@
=> ShowNativeSourceDialogAsync(
editId: null,
new TemplateNativeAlarmSourceDialog.NativeSourceDraft(
string.Empty, string.Empty, string.Empty, null, null, false));
string.Empty, string.Empty, string.Empty, null, null, false, false));
private Task OpenEditNativeSourceDialog(TemplateNativeAlarmSource src)
=> ShowNativeSourceDialogAsync(
@@ -1052,7 +1052,8 @@
src.SourceReference,
src.ConditionFilter,
src.Description,
src.IsLocked));
src.IsLocked,
src.IsMesReportable));
private async Task ShowNativeSourceDialogAsync(
int? editId,
@@ -1070,6 +1071,7 @@
InitialFilter="@initial.ConditionFilter"
InitialDescription="@initial.Description"
InitialIsLocked="initial.IsLocked"
InitialIsMesReportable="initial.IsMesReportable"
AlarmCapableConnections="connections"
OnSaveAsync="d => SaveNativeSourceDraftAsync(editId, d)" />);
@@ -1097,7 +1099,8 @@
await MutationServices.ExecuteConfigurationAsync(new UpdateTemplateNativeAlarmSourceCommand(id, existing.Name,
draft.ConnectionName.Trim(), draft.SourceReference.Trim(),
string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim(),
string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(), draft.IsLocked));
string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(), draft.IsLocked,
draft.IsMesReportable));
_toast.ShowSuccess($"Native alarm source '{existing.Name}' updated.");
return null;
}
@@ -1114,10 +1117,12 @@
SourceReference = draft.SourceReference.Trim(),
ConditionFilter = string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim(),
Description = string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(),
IsLocked = draft.IsLocked
IsLocked = draft.IsLocked,
IsMesReportable = draft.IsMesReportable
};
await MutationServices.ExecuteConfigurationAsync(new AddTemplateNativeAlarmSourceCommand(source.TemplateId, source.Name,
source.ConnectionName, source.SourceReference, source.ConditionFilter, source.Description, source.IsLocked));
source.ConnectionName, source.SourceReference, source.ConditionFilter, source.Description, source.IsLocked,
source.IsMesReportable));
_toast.ShowSuccess($"Native alarm source '{source.Name}' added.");
return null;
}
@@ -51,6 +51,12 @@
<label class="form-check-label" for="nasLocked">Locked</label>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="_nasIsMesReportable" id="nasMesReportable" />
<label class="form-check-label" for="nasMesReportable">MES reportable (every condition under this source)</label>
</div>
</div>
@if (_error != null)
{
<div class="col-12"><div class="text-danger small" data-test="nas-form-error">@_error</div></div>
@@ -77,6 +83,7 @@
[Parameter] public string? InitialFilter { get; set; }
[Parameter] public string? InitialDescription { get; set; }
[Parameter] public bool InitialIsLocked { get; set; }
[Parameter] public bool InitialIsMesReportable { get; set; }
/// <summary>Connections whose protocol can surface native alarms (OPC UA / MxGateway).</summary>
[Parameter] public IReadOnlyList<DataConnection> AlarmCapableConnections { get; set; }
@@ -94,6 +101,7 @@
private string? _nasFilter;
private string? _nasDescription;
private bool _nasIsLocked;
private bool _nasIsMesReportable;
private string? _error;
private bool _busy;
@@ -105,6 +113,7 @@
_nasFilter = InitialFilter;
_nasDescription = InitialDescription;
_nasIsLocked = InitialIsLocked;
_nasIsMesReportable = InitialIsMesReportable;
}
private async Task Submit()
@@ -121,7 +130,8 @@
_nasSourceRef,
_nasFilter,
_nasDescription,
_nasIsLocked);
_nasIsLocked,
_nasIsMesReportable);
var error = await OnSaveAsync(draft);
if (error == null)
@@ -146,11 +156,13 @@
/// <param name="ConditionFilter">Optional condition filter; blank mirrors every condition.</param>
/// <param name="Description">Optional free-text description.</param>
/// <param name="IsLocked">Whether instances may override the binding.</param>
/// <param name="IsMesReportable">Whether every condition under the source is MES-reportable.</param>
public sealed record NativeSourceDraft(
string Name,
string ConnectionName,
string SourceReference,
string? ConditionFilter,
string? Description,
bool IsLocked);
bool IsLocked,
bool IsMesReportable = false);
}
@@ -24,6 +24,8 @@ public class InstanceNativeAlarmSourceOverride
public string? SourceReferenceOverride { get; set; }
/// <summary>Overrides the condition filter when set; null = keep inherited.</summary>
public string? ConditionFilterOverride { get; set; }
/// <summary>Overrides the binding's <c>IsMesReportable</c> flag when set; null = keep inherited.</summary>
public bool? IsMesReportableOverride { get; set; }
/// <summary>Initializes a new override for the specified source binding.</summary>
/// <param name="sourceCanonicalName">Canonical name of the source to override.</param>
@@ -23,6 +23,13 @@ public class TemplateNativeAlarmSource
public string SourceReference { get; set; } = string.Empty;
/// <summary>Optional condition filter; null = mirror all conditions under the source.</summary>
public string? ConditionFilter { get; set; }
/// <summary>
/// When true, every condition mirrored under this binding is MES-reportable
/// (<c>AlarmStateChanged.IsMesReportable</c> / <c>ScriptAlarm.IsMesReportable</c>). Additive to the
/// 900-999 severity-band convention, which still flags a condition on its own. Default false.
/// Overridable per instance via <c>InstanceNativeAlarmSourceOverride.IsMesReportableOverride</c>.
/// </summary>
public bool IsMesReportable { get; set; }
/// <summary>When true, this binding cannot be overridden in derived templates.</summary>
public bool IsLocked { get; set; }
/// <summary>True when copied from a base template and not yet overridden on the derived template.</summary>
@@ -65,7 +65,8 @@ public record SetInstanceNativeAlarmSourceOverrideCommand(
string SourceCanonicalName,
string? ConnectionNameOverride,
string? SourceReferenceOverride,
string? ConditionFilterOverride);
string? ConditionFilterOverride,
bool? IsMesReportableOverride = null); // additive trailing optional; null = inherit the binding's flag
public record DeleteInstanceNativeAlarmSourceOverrideCommand(
int InstanceId,
@@ -83,7 +84,8 @@ public record NativeAlarmSourceOverrideEntry(
string SourceCanonicalName,
string? ConnectionNameOverride,
string? SourceReferenceOverride,
string? ConditionFilterOverride);
string? ConditionFilterOverride,
bool? IsMesReportableOverride = null);
/// <summary>
/// Bulk, all-or-nothing apply of per-instance native-alarm-source overrides — the
@@ -49,8 +49,8 @@ public record AddTemplateAlarmCommand(int TemplateId, string Name, string Trigge
public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false);
public record DeleteTemplateAlarmCommand(int AlarmId);
// Native alarm source bindings (read-only mirror of an OPC UA A&C / MxGateway source's alarms)
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 AddTemplateNativeAlarmSourceCommand(int TemplateId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked, bool IsMesReportable = false);
public record UpdateTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked, bool IsMesReportable = false);
public record DeleteTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId);
public record ListTemplateNativeAlarmSourcesCommand(int TemplateId);
// MinTimeBetweenRuns/ExecutionTimeoutSeconds are additive trailing optionals (default null = "unset"):
@@ -116,8 +116,10 @@ public record AlarmStateChanged(
/// <summary>
/// True when the alarm's deployed configuration flags it as MES-reportable
/// (<c>ResolvedAlarm.IsMesReportable</c>). Stamped by the Instance Actor on its alarm
/// snapshot and on every event it relays to the site stream; always false for native
/// conditions, which have no per-alarm config row. Carried to central on
/// snapshot and on every event it relays to the site stream. For a native condition it is
/// the binding-level flag (<c>ResolvedNativeAlarmSource.IsMesReportable</c>, Gitea #59), set by
/// the owning <c>NativeAlarmActor</c> — there is still no per-condition config row; the
/// 900-999 severity band remains an independent, additive signal. Carried to central on
/// <c>AlarmStateUpdate</c> field 25 and <c>DebugAlarmStateDto</c> field 20 (Gitea #60);
/// a pre-#60 site sends the proto3 default. Additive; default false.
/// </summary>
@@ -160,6 +160,11 @@ public sealed record ResolvedNativeAlarmSource
/// <summary>Gets the source of this binding: "Template", "Inherited", "Composed", or "Override".</summary>
public string Source { get; init; } = "Template";
/// <summary>
/// Gets whether every condition under this binding is MES-reportable (effective value after
/// the instance override). Additive to the 900-999 severity band.
/// </summary>
public bool IsMesReportable { get; init; }
/// <summary>
/// Gets whether this native alarm source is locked at the template level. A locked
/// source cannot be repointed by an instance override — mirrors the attribute/alarm
/// lock semantics.
@@ -57,7 +57,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
/// </param>
/// <param name="IsMesReportable">
/// True when the deployed configuration flags the alarm as MES-reportable (template
/// checkbox or instance override). Always false for native conditions.
/// checkbox or instance override). For native conditions it is the source binding's flag (Gitea #59).
/// </param>
public sealed record ScriptAlarm(
string Name,
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
/// <inheritdoc />
public partial class AddNativeAlarmSourceMesReportable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsMesReportable",
table: "TemplateNativeAlarmSources",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsMesReportableOverride",
table: "InstanceNativeAlarmSourceOverrides",
type: "bit",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsMesReportable",
table: "TemplateNativeAlarmSources");
migrationBuilder.DropColumn(
name: "IsMesReportableOverride",
table: "InstanceNativeAlarmSourceOverrides");
}
}
}
@@ -17,7 +17,7 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("ProductVersion", "10.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
@@ -724,6 +724,9 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
b.Property<int>("InstanceId")
.HasColumnType("int");
b.Property<bool?>("IsMesReportableOverride")
.HasColumnType("bit");
b.Property<string>("SourceCanonicalName")
.IsRequired()
.HasMaxLength(400)
@@ -1673,6 +1676,9 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
b.Property<bool>("IsLocked")
.HasColumnType("bit");
b.Property<bool>("IsMesReportable")
.HasColumnType("bit");
b.Property<bool>("LockedInDerived")
.HasColumnType("bit");
@@ -1295,7 +1295,8 @@ public class ManagementActor : ReceiveActor
InstanceId = cmd.InstanceId,
ConnectionNameOverride = cmd.ConnectionNameOverride,
SourceReferenceOverride = cmd.SourceReferenceOverride,
ConditionFilterOverride = cmd.ConditionFilterOverride
ConditionFilterOverride = cmd.ConditionFilterOverride,
IsMesReportableOverride = cmd.IsMesReportableOverride
};
await repo.AddInstanceNativeAlarmSourceOverrideAsync(ovr);
await repo.SaveChangesAsync();
@@ -1305,6 +1306,7 @@ public class ManagementActor : ReceiveActor
existing.ConnectionNameOverride = cmd.ConnectionNameOverride;
existing.SourceReferenceOverride = cmd.SourceReferenceOverride;
existing.ConditionFilterOverride = cmd.ConditionFilterOverride;
existing.IsMesReportableOverride = cmd.IsMesReportableOverride;
await repo.UpdateInstanceNativeAlarmSourceOverrideAsync(existing);
await repo.SaveChangesAsync();
return existing;
@@ -1363,7 +1365,8 @@ public class ManagementActor : ReceiveActor
InstanceId = cmd.InstanceId,
ConnectionNameOverride = entry.ConnectionNameOverride,
SourceReferenceOverride = entry.SourceReferenceOverride,
ConditionFilterOverride = entry.ConditionFilterOverride
ConditionFilterOverride = entry.ConditionFilterOverride,
IsMesReportableOverride = entry.IsMesReportableOverride
};
await repo.AddInstanceNativeAlarmSourceOverrideAsync(ovr);
results.Add(ovr);
@@ -1373,6 +1376,7 @@ public class ManagementActor : ReceiveActor
existing.ConnectionNameOverride = entry.ConnectionNameOverride;
existing.SourceReferenceOverride = entry.SourceReferenceOverride;
existing.ConditionFilterOverride = entry.ConditionFilterOverride;
existing.IsMesReportableOverride = entry.IsMesReportableOverride;
await repo.UpdateInstanceNativeAlarmSourceOverrideAsync(existing);
results.Add(existing);
}
@@ -3441,7 +3445,8 @@ public class ManagementActor : ReceiveActor
SourceReference = cmd.SourceReference,
ConditionFilter = cmd.ConditionFilter,
Description = cmd.Description,
IsLocked = cmd.IsLocked
IsLocked = cmd.IsLocked,
IsMesReportable = cmd.IsMesReportable
};
await repo.AddTemplateNativeAlarmSourceAsync(source);
await repo.SaveChangesAsync();
@@ -3463,6 +3468,7 @@ public class ManagementActor : ReceiveActor
source.ConditionFilter = cmd.ConditionFilter;
source.Description = cmd.Description;
source.IsLocked = cmd.IsLocked;
source.IsMesReportable = cmd.IsMesReportable;
await repo.UpdateTemplateNativeAlarmSourceAsync(source);
await repo.SaveChangesAsync();
// Propagate the changed source to derived descendants.
@@ -59,9 +59,16 @@ public class InstanceActor : ReceiveActor
/// children are created and read back in <see cref="BuildAlarmStatesSnapshot"/>, which
/// is the single place the flag is stamped — so a fired alarm (served from the retained
/// event) and an unfired one (served from the bare fallback projection) cannot drift.
/// Native conditions have no entry here and correctly resolve to false.
/// Native conditions have no entry here (their flag is the binding's, set by NativeAlarmActor).
/// </summary>
private readonly Dictionary<string, bool> _alarmMesReportable = new();
/// <summary>
/// Native binding canonical name → <c>ResolvedNativeAlarmSource.IsMesReportable</c> (Gitea #59),
/// used to stamp the quiet-binding placeholder rows. Live condition rows are stamped by the
/// <see cref="NativeAlarmActor"/> that owns the binding.
/// </summary>
private readonly Dictionary<string, bool> _nativeAlarmMesReportable = new();
private readonly Dictionary<string, IActorRef> _scriptActors = new();
private readonly Dictionary<string, IActorRef> _alarmActors = new();
private readonly Dictionary<string, IActorRef> _nativeAlarmActors = new();
@@ -1775,6 +1782,7 @@ public class InstanceActor : ReceiveActor
{
Kind = _nativeAlarmKinds.GetValueOrDefault(binding, AlarmKind.NativeOpcUa),
NativeSourceCanonicalName = binding,
IsMesReportable = _nativeAlarmMesReportable.GetValueOrDefault(binding),
IsConfiguredPlaceholder = true
});
}
@@ -2250,6 +2258,7 @@ public class InstanceActor : ReceiveActor
// Same kind handed to the NativeAlarmActor — used to stamp the
// (cosmetic) kind badge on placeholder rows for quiet bindings.
_nativeAlarmKinds[nativeSource.CanonicalName] = nativeKind;
_nativeAlarmMesReportable[nativeSource.CanonicalName] = nativeSource.IsMesReportable;
}
_logger.LogInformation(
@@ -496,6 +496,9 @@ public class NativeAlarmActor : ReceiveActor
CurrentValue = t.CurrentValue,
LimitValue = t.LimitValue,
NativeSourceCanonicalName = _source.CanonicalName,
// Gitea #59: the binding-level flag ("every condition under this source is
// MES-reportable"). Additive to the 900-999 severity band the consumer applies.
IsMesReportable = _source.IsMesReportable,
// MES alarm-status API §6.4: carried verbatim from the transition — the DCL
// adapter already decided whether an ack instant applies (null while unacked,
// cleared on re-raise), so the mirror never invents or suppresses one. A
@@ -151,6 +151,7 @@ public class DiffService
a.ConnectionName == b.ConnectionName &&
a.SourceReference == b.SourceReference &&
a.ConditionFilter == b.ConditionFilter &&
a.IsMesReportable == b.IsMesReportable &&
a.IsLocked == b.IsLocked;
private static bool ScriptsEqual(ResolvedScript a, ResolvedScript b) =>
@@ -730,6 +730,7 @@ public class FlatteningService
ConnectionName = binding.ConnectionName,
SourceReference = binding.SourceReference,
ConditionFilter = binding.ConditionFilter,
IsMesReportable = binding.IsMesReportable,
Source = source,
IsLocked = binding.IsLocked || lockedNames.Contains(binding.Name)
};
@@ -832,6 +833,7 @@ public class FlatteningService
ConnectionName = ovr.ConnectionNameOverride ?? existing.ConnectionName,
SourceReference = ovr.SourceReferenceOverride ?? existing.SourceReference,
ConditionFilter = ovr.ConditionFilterOverride ?? existing.ConditionFilter,
IsMesReportable = ovr.IsMesReportableOverride ?? existing.IsMesReportable,
Source = "Override"
};
}
@@ -109,6 +109,8 @@ public class RevisionHashService
ConditionFilter = n.ConditionFilter,
ConnectionName = n.ConnectionName,
IsLocked = n.IsLocked,
// true-only (null when false) so existing revision hashes are unchanged.
IsMesReportable = n.IsMesReportable ? true : null,
SourceReference = n.SourceReference
})
.ToList()
@@ -296,6 +298,11 @@ public class RevisionHashService
/// </summary>
public bool IsLocked { get; init; }
/// <summary>
/// MES-reportable binding flag; null when false (the serializer drops nulls), so
/// pre-existing configurations hash identically.
/// </summary>
public bool? IsMesReportable { get; init; }
/// <summary>
/// The source reference (OPC UA SourceNode/notifier nodeId, or MxAccess object/area).
/// </summary>
public string SourceReference { get; init; } = string.Empty;
@@ -1176,6 +1176,7 @@ public class TemplateService
ConnectionName = src.ConnectionName,
SourceReference = src.SourceReference,
ConditionFilter = src.ConditionFilter,
IsMesReportable = src.IsMesReportable,
IsLocked = src.IsLocked,
IsInherited = true,
LockedInDerived = false,
@@ -1546,6 +1547,7 @@ public class TemplateService
ConnectionName = win.ConnectionName,
SourceReference = win.SourceReference,
ConditionFilter = win.ConditionFilter,
IsMesReportable = win.IsMesReportable,
IsLocked = win.IsLocked,
IsInherited = true,
LockedInDerived = false,
@@ -1556,12 +1558,14 @@ public class TemplateService
existing.ConnectionName != win.ConnectionName ||
existing.SourceReference != win.SourceReference ||
existing.ConditionFilter != win.ConditionFilter ||
existing.IsMesReportable != win.IsMesReportable ||
existing.Description != win.Description ||
existing.IsLocked != win.IsLocked))
{
existing.ConnectionName = win.ConnectionName;
existing.SourceReference = win.SourceReference;
existing.ConditionFilter = win.ConditionFilter;
existing.IsMesReportable = win.IsMesReportable;
existing.Description = win.Description;
existing.IsLocked = win.IsLocked;
await _repository.UpdateTemplateNativeAlarmSourceAsync(existing, ct);
@@ -503,7 +503,8 @@ public sealed class ArtifactDiff
i => i.SourceCanonicalName,
(e, i) => e.ConnectionNameOverride == i.ConnectionNameOverride
&& e.SourceReferenceOverride == i.SourceReferenceOverride
&& e.ConditionFilterOverride == i.ConditionFilterOverride,
&& e.ConditionFilterOverride == i.ConditionFilterOverride
&& e.IsMesReportableOverride == i.IsMesReportableOverride,
"NativeAlarmSourceOverrides",
changes);
@@ -2610,6 +2610,7 @@ public sealed class BundleImporter : IBundleImporter
ConnectionName = n.ConnectionName,
SourceReference = n.SourceReference,
ConditionFilter = n.ConditionFilter,
IsMesReportable = n.IsMesReportable,
IsLocked = n.IsLocked,
IsInherited = n.IsInherited,
LockedInDerived = n.LockedInDerived,
@@ -3020,6 +3021,7 @@ public sealed class BundleImporter : IBundleImporter
current.ConnectionName = srcDto.ConnectionName;
current.SourceReference = srcDto.SourceReference;
current.ConditionFilter = srcDto.ConditionFilter;
current.IsMesReportable = srcDto.IsMesReportable;
current.IsLocked = srcDto.IsLocked;
current.IsInherited = srcDto.IsInherited;
current.LockedInDerived = srcDto.LockedInDerived;
@@ -3038,6 +3040,7 @@ public sealed class BundleImporter : IBundleImporter
current.ConnectionName,
current.SourceReference,
current.ConditionFilter,
current.IsMesReportable,
current.IsLocked,
current.IsInherited,
current.LockedInDerived,
@@ -3052,6 +3055,7 @@ public sealed class BundleImporter : IBundleImporter
ConnectionName = srcDto.ConnectionName,
SourceReference = srcDto.SourceReference,
ConditionFilter = srcDto.ConditionFilter,
IsMesReportable = srcDto.IsMesReportable,
IsLocked = srcDto.IsLocked,
IsInherited = srcDto.IsInherited,
LockedInDerived = srcDto.LockedInDerived,
@@ -3071,6 +3075,7 @@ public sealed class BundleImporter : IBundleImporter
newSource.ConnectionName,
newSource.SourceReference,
newSource.ConditionFilter,
newSource.IsMesReportable,
newSource.IsLocked,
newSource.IsInherited,
newSource.LockedInDerived,
@@ -4962,6 +4967,7 @@ public sealed class BundleImporter : IBundleImporter
o.ConnectionNameOverride, sourceSiteIdentifier, connectionMaps),
SourceReferenceOverride = o.SourceReferenceOverride,
ConditionFilterOverride = o.ConditionFilterOverride,
IsMesReportableOverride = o.IsMesReportableOverride,
});
}
foreach (var b in dto.ConnectionBindings)
@@ -96,6 +96,7 @@ internal static class TemplateChildEquality
&& e.ConnectionName == i.ConnectionName
&& e.SourceReference == i.SourceReference
&& e.ConditionFilter == i.ConditionFilter
&& e.IsMesReportable == i.IsMesReportable
&& e.IsLocked == i.IsLocked
&& e.IsInherited == i.IsInherited
&& e.LockedInDerived == i.LockedInDerived;
@@ -192,7 +192,9 @@ public sealed record TemplateNativeAlarmSourceDto(
string? ConditionFilter,
bool IsLocked,
bool IsInherited,
bool LockedInDerived);
bool LockedInDerived,
// Additive trailing field: false on bundles written before it existed.
bool IsMesReportable = false);
public sealed record TemplateAttributeDto(
string Name,
@@ -407,7 +409,9 @@ public sealed record InstanceNativeAlarmSourceOverrideDto(
string SourceCanonicalName,
string? ConnectionNameOverride,
string? SourceReferenceOverride,
string? ConditionFilterOverride);
string? ConditionFilterOverride,
// Additive trailing field: null ("keep inherited") on bundles written before it existed.
bool? IsMesReportableOverride = null);
public sealed record InstanceConnectionBindingDto(
string AttributeName,
@@ -104,7 +104,8 @@ public sealed class EntitySerializer
ConditionFilter: n.ConditionFilter,
IsLocked: n.IsLocked,
IsInherited: n.IsInherited,
LockedInDerived: n.LockedInDerived)).ToList(),
LockedInDerived: n.LockedInDerived,
IsMesReportable: n.IsMesReportable)).ToList(),
};
}).ToList(),
SharedScripts: aggregate.SharedScripts.Select(s => new SharedScriptDto(
@@ -292,7 +293,8 @@ public sealed class EntitySerializer
SourceCanonicalName: o.SourceCanonicalName,
ConnectionNameOverride: o.ConnectionNameOverride,
SourceReferenceOverride: o.SourceReferenceOverride,
ConditionFilterOverride: o.ConditionFilterOverride)).ToList(),
ConditionFilterOverride: o.ConditionFilterOverride,
IsMesReportableOverride: o.IsMesReportableOverride)).ToList(),
// Connection bindings reference their DataConnection by NAME — the
// numeric DataConnectionId FK can't survive a cross-environment
// bundle, so the importer resolves ConnectionName→target FK at
@@ -386,6 +388,7 @@ public sealed class EntitySerializer
IsLocked = n.IsLocked,
IsInherited = n.IsInherited,
LockedInDerived = n.LockedInDerived,
IsMesReportable = n.IsMesReportable,
});
}
return t;
@@ -621,6 +624,7 @@ public sealed class EntitySerializer
ConnectionNameOverride = o.ConnectionNameOverride,
SourceReferenceOverride = o.SourceReferenceOverride,
ConditionFilterOverride = o.ConditionFilterOverride,
IsMesReportableOverride = o.IsMesReportableOverride,
});
}
foreach (var b in dto.ConnectionBindings)