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:
@@ -87,6 +87,17 @@ Consequences, stated plainly:
|
||||
binding is reportable"), or (b) a new per-condition config entity — the first of its kind. Neither
|
||||
is designed here.
|
||||
|
||||
**Update (Gitea #59, 2026-11-09): option (a) is built.** `TemplateNativeAlarmSource.IsMesReportable`
|
||||
(bool, default false) plus a tri-state `InstanceNativeAlarmSourceOverride.IsMesReportableOverride`
|
||||
(null inherits) mean "every condition mirrored under this binding is MES-reportable". It flattens onto
|
||||
`ResolvedNativeAlarmSource.IsMesReportable`, `NativeAlarmActor` stamps it on every condition it emits
|
||||
and `InstanceActor` stamps it on the quiet-binding placeholder rows, so `ScriptAlarm.IsMesReportable`
|
||||
/ `Alarms.MesReportableAsync()` and the wire fields from #60 carry it. It is **additive** to the
|
||||
900-999 severity band, which still flags a condition on its own (an inbound-API method ORs the two).
|
||||
Migration `AddNativeAlarmSourceMesReportable`; CLI `--mes-reportable` on
|
||||
`template native-alarm-source add|update` and `instance native-alarm-source set`; Transport carries
|
||||
both fields. Per-condition flags (option b) remain unbuilt.
|
||||
|
||||
### 2.4 Central visibility is a follow-up
|
||||
|
||||
The Alarm Summary / Debug View rows are `AlarmStateChanged` values that arrive over gRPC. Showing a
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
+23
-5
@@ -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;
|
||||
}
|
||||
|
||||
+14
-2
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2175
File diff suppressed because it is too large
Load Diff
+39
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -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)
|
||||
|
||||
@@ -33,8 +33,8 @@ public class EditingParityTests
|
||||
[InlineData("template resolved-members --id 12", "GetResolvedTemplateMembers", "{\"templateId\":12}")]
|
||||
[InlineData("template move --id 12 --folder-id 3", "MoveTemplateToFolder", "{\"templateId\":12,\"newFolderId\":3}")]
|
||||
[InlineData("template move --id 12", "MoveTemplateToFolder", "{\"templateId\":12,\"newFolderId\":null}")]
|
||||
[InlineData("template native-alarm-source update --id 9 --name Pressure --connection OPC --source-ref ns=2;s=Source --filter Active --description Updated --locked", "UpdateTemplateNativeAlarmSource", "{\"nativeAlarmSourceId\":9,\"name\":\"Pressure\",\"connectionName\":\"OPC\",\"sourceReference\":\"ns=2;s=Source\",\"conditionFilter\":\"Active\",\"description\":\"Updated\",\"isLocked\":true}")]
|
||||
[InlineData("template native-alarm-source update --id 9 --name Pressure --connection OPC --source-ref ns=2;s=Source", "UpdateTemplateNativeAlarmSource", "{\"nativeAlarmSourceId\":9,\"name\":\"Pressure\",\"connectionName\":\"OPC\",\"sourceReference\":\"ns=2;s=Source\",\"conditionFilter\":null,\"description\":null,\"isLocked\":false}")]
|
||||
[InlineData("template native-alarm-source update --id 9 --name Pressure --connection OPC --source-ref ns=2;s=Source --filter Active --description Updated --locked --mes-reportable", "UpdateTemplateNativeAlarmSource", "{\"nativeAlarmSourceId\":9,\"name\":\"Pressure\",\"connectionName\":\"OPC\",\"sourceReference\":\"ns=2;s=Source\",\"conditionFilter\":\"Active\",\"description\":\"Updated\",\"isLocked\":true,\"isMesReportable\":true}")]
|
||||
[InlineData("template native-alarm-source update --id 9 --name Pressure --connection OPC --source-ref ns=2;s=Source", "UpdateTemplateNativeAlarmSource", "{\"nativeAlarmSourceId\":9,\"name\":\"Pressure\",\"connectionName\":\"OPC\",\"sourceReference\":\"ns=2;s=Source\",\"conditionFilter\":null,\"description\":null,\"isLocked\":false,\"isMesReportable\":false}")]
|
||||
[InlineData("instance attribute-override delete --instance-id 6 --attribute Module.Value", "DeleteInstanceAttributeOverride", "{\"instanceId\":6,\"attributeName\":\"Module.Value\"}")]
|
||||
[InlineData("instance native-alarm-source list --instance-id 6", "ListInstanceNativeAlarmSourceOverrides", "{\"instanceId\":6}")]
|
||||
[InlineData("instance alarm-override list --instance-id 6", "ListInstanceAlarmOverrides", "{\"instanceId\":6}")]
|
||||
|
||||
@@ -165,6 +165,36 @@ public class SchemaConfigurationTests : IDisposable
|
||||
Assert.True((await _context.TemplateAlarms.SingleAsync(a => a.Id == alarm.Id)).IsMesReportable);
|
||||
Assert.False((await _context.InstanceAlarmOverrides.SingleAsync(o => o.Id == ovr.Id)).IsMesReportableOverride);
|
||||
}
|
||||
|
||||
// Gitea #59: per-binding flag on native alarm sources. Backed on SQL Server by the
|
||||
// AddNativeAlarmSourceMesReportable migration; pinned here on the model/persist path.
|
||||
/// <summary>Verifies that native alarm sources and their overrides persist the MES-reportable flags.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NativeAlarmSource_And_Override_PersistMesReportableFlags()
|
||||
{
|
||||
var site = new Site("Site59", "S-059");
|
||||
var template = new Template("MesNativeT1");
|
||||
_context.Sites.Add(site);
|
||||
_context.Templates.Add(template);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var instance = new Instance("MesNativeInst1") { SiteId = site.Id, TemplateId = template.Id };
|
||||
_context.Instances.Add(instance);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var src = new TemplateNativeAlarmSource("Pressure")
|
||||
{ TemplateId = template.Id, ConnectionName = "Opc", SourceReference = "ns=2;s=P", IsMesReportable = true };
|
||||
var ovr = new InstanceNativeAlarmSourceOverride("Pressure")
|
||||
{ InstanceId = instance.Id, IsMesReportableOverride = false };
|
||||
_context.Add(src);
|
||||
_context.Add(ovr);
|
||||
await _context.SaveChangesAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
|
||||
Assert.True((await _context.Set<TemplateNativeAlarmSource>().SingleAsync(a => a.Id == src.Id)).IsMesReportable);
|
||||
Assert.False((await _context.Set<InstanceNativeAlarmSourceOverride>().SingleAsync(o => o.Id == ovr.Id)).IsMesReportableOverride);
|
||||
}
|
||||
}
|
||||
|
||||
public class SplitQueryBehaviourTests : IDisposable
|
||||
|
||||
@@ -4706,6 +4706,37 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
_templateRepo.ReceivedWithAnyArgs(1).SaveChangesAsync(default);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: the instance override command carries IsMesReportableOverride onto the persisted row.</summary>
|
||||
[Fact]
|
||||
public void SetInstanceNativeAlarmSourceOverride_CarriesIsMesReportableOverride()
|
||||
{
|
||||
var pipeline = Substitute.For<IFlatteningPipeline>();
|
||||
var flattened = new FlattenedConfiguration
|
||||
{
|
||||
NativeAlarmSources = new List<ResolvedNativeAlarmSource>
|
||||
{
|
||||
new() { CanonicalName = "Pressure", IsLocked = false, Source = "Template" }
|
||||
}
|
||||
};
|
||||
pipeline.FlattenAndValidateAsync(1, Arg.Any<CancellationToken>(), Arg.Any<bool>())
|
||||
.Returns(Result<FlatteningPipelineResult>.Success(
|
||||
new FlatteningPipelineResult(flattened, "hash", ValidationResult.Success())));
|
||||
_services.AddScoped(_ => pipeline);
|
||||
|
||||
InstanceNativeAlarmSourceOverride? added = null;
|
||||
_templateRepo
|
||||
.When(r => r.AddInstanceNativeAlarmSourceOverrideAsync(Arg.Any<InstanceNativeAlarmSourceOverride>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => added = ci.Arg<InstanceNativeAlarmSourceOverride>());
|
||||
|
||||
CreateActor().Tell(Envelope(
|
||||
new SetInstanceNativeAlarmSourceOverrideCommand(1, "Pressure", null, null, null, IsMesReportableOverride: true),
|
||||
"Deployer"));
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(added);
|
||||
Assert.True(added!.IsMesReportableOverride);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that set instance native alarm source override, when with design role, returns unauthorized.</summary>
|
||||
[Fact]
|
||||
public void SetInstanceNativeAlarmSourceOverride_WithDesignRole_ReturnsUnauthorized()
|
||||
|
||||
+19
@@ -50,6 +50,25 @@ public class NativeAlarmSourceNameValidationTests : TestKit
|
||||
_repo.DidNotReceive().AddTemplateNativeAlarmSourceAsync(Arg.Any<TemplateNativeAlarmSource>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: the update command carries IsMesReportable onto the persisted binding.</summary>
|
||||
[Fact]
|
||||
public void Update_CarriesIsMesReportable()
|
||||
{
|
||||
TemplateNativeAlarmSource? updated = null;
|
||||
_repo.When(r => r.UpdateTemplateNativeAlarmSourceAsync(Arg.Any<TemplateNativeAlarmSource>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => updated = ci.Arg<TemplateNativeAlarmSource>());
|
||||
_services.AddScoped<ZB.MOM.WW.ScadaBridge.TemplateEngine.TemplateService>();
|
||||
|
||||
CreateActor().Tell(Designer(new UpdateTemplateNativeAlarmSourceCommand(
|
||||
4, "Pressure", "Opc", "ns=2;s=T01", null, null, false, IsMesReportable: true)));
|
||||
|
||||
// The reconcile that follows needs TemplateService collaborators this harness does not
|
||||
// wire, so only the persisted binding is asserted (the Update call precedes the reconcile).
|
||||
ExpectMsg<object>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(updated);
|
||||
Assert.True(updated!.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Renaming a source to an illegal actor name is rejected before any write.</summary>
|
||||
[Fact]
|
||||
public void Update_RenameToInvalidActorName_IsRejected()
|
||||
|
||||
+19
-2
@@ -51,7 +51,7 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
dclManager)));
|
||||
|
||||
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName) => new()
|
||||
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName, bool mesReportable = false) => new()
|
||||
{
|
||||
InstanceUniqueName = instanceName,
|
||||
NativeAlarmSources =
|
||||
@@ -60,7 +60,8 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
|
||||
{
|
||||
CanonicalName = "Pressure",
|
||||
ConnectionName = "Opc",
|
||||
SourceReference = "ns=2;s=T01"
|
||||
SourceReference = "ns=2;s=T01",
|
||||
IsMesReportable = mesReportable
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -100,6 +101,22 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
|
||||
a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: the quiet-binding placeholder row carries the binding's MES flag.</summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Snapshot_QuietNativeBinding_PlaceholderCarriesBindingMesFlag(bool flag)
|
||||
{
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst", flag), dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
|
||||
var snap = ExpectMsg<DebugViewSnapshot>();
|
||||
|
||||
Assert.Equal(flag, Assert.Single(snap.AlarmStates, a => a.IsConfiguredPlaceholder).IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a quiet native binding emits a placeholder in the snapshot.</summary>
|
||||
[Fact]
|
||||
public void Snapshot_QuietNativeBinding_EmitsPlaceholder()
|
||||
|
||||
@@ -36,11 +36,12 @@ public class NativeAlarmActorTests : TestKit, IDisposable
|
||||
_storage.InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static ResolvedNativeAlarmSource Source() => new()
|
||||
private static ResolvedNativeAlarmSource Source(bool mesReportable = false) => new()
|
||||
{
|
||||
CanonicalName = "Pressure",
|
||||
ConnectionName = "Opc",
|
||||
SourceReference = "ns=2;s=T01"
|
||||
SourceReference = "ns=2;s=T01",
|
||||
IsMesReportable = mesReportable
|
||||
};
|
||||
|
||||
private static NativeAlarmTransition Transition(
|
||||
@@ -48,6 +49,11 @@ public class NativeAlarmActorTests : TestKit, IDisposable
|
||||
new(sourceRef, "T01", "AnalogLimit.Hi", kind, condition,
|
||||
"Process", "hi", "hi", "", "", null, time ?? DateTimeOffset.UtcNow, "92", "90");
|
||||
|
||||
private IActorRef SpawnMes(IActorRef instanceActor, IActorRef dclManager, bool mesReportable) =>
|
||||
ActorOf(Props.Create(() => new NativeAlarmActor(
|
||||
Source(mesReportable), "inst", instanceActor, dclManager, _storage, _options,
|
||||
NullLogger<NativeAlarmActor>.Instance, AlarmKind.NativeOpcUa, null)));
|
||||
|
||||
private IActorRef Spawn(IActorRef instanceActor, IActorRef dclManager, IServiceProvider? serviceProvider = null) =>
|
||||
Spawn(instanceActor, dclManager, _options, serviceProvider);
|
||||
|
||||
@@ -157,6 +163,25 @@ public class NativeAlarmActorTests : TestKit, IDisposable
|
||||
Assert.Equal("Pressure", emitted.NativeSourceCanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: a binding flagged MES-reportable stamps every condition it mirrors; an unflagged one does not.</summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Raise_StampsIsMesReportable_FromBindingFlag(bool bindingFlag)
|
||||
{
|
||||
var instance = CreateTestProbe();
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = SpawnMes(instance.Ref, dcl.Ref, bindingFlag);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Raise,
|
||||
// Severity 100 is OUTSIDE the 900-999 band, so only the binding flag can set it.
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 100))));
|
||||
|
||||
Assert.Equal(bindingFlag, instance.ExpectMsg<AlarmStateChanged>().IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a snapshot completing with a missing prior alarm emits a return to normal.</summary>
|
||||
[Fact]
|
||||
public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal()
|
||||
|
||||
@@ -952,6 +952,31 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: the binding flag flattens from the template and a tri-state instance override wins (null inherits).</summary>
|
||||
[Theory]
|
||||
[InlineData(true, null, true)]
|
||||
[InlineData(false, null, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
public void Flatten_NativeAlarmSource_IsMesReportable_TemplateFlagAndOverride(bool templateFlag, bool? ovr, bool expected)
|
||||
{
|
||||
var template = CreateTemplate(1, "Base");
|
||||
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
|
||||
{ ConnectionName = "Opc", SourceReference = "ns=2;s=P1", IsMesReportable = templateFlag });
|
||||
var instance = CreateInstance();
|
||||
if (ovr.HasValue)
|
||||
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
|
||||
{ IsMesReportableOverride = ovr });
|
||||
|
||||
var result = _sut.Flatten(instance, [template],
|
||||
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||||
new Dictionary<int, IReadOnlyList<Template>>(),
|
||||
new Dictionary<int, DataConnection>());
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(expected, result.Value.NativeAlarmSources[0].IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a locked native alarm source ignores an instance override.</summary>
|
||||
[Fact]
|
||||
public void Flatten_LockedNativeAlarmSource_IgnoresInstanceOverride()
|
||||
|
||||
+23
@@ -413,6 +413,29 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: the binding's MES-reportable flag participates in the hash; false hashes as before the field existed.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceIsMesReportableChange_ChangesHash()
|
||||
{
|
||||
var baseSource = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "Boiler.Alarms",
|
||||
ConnectionName = "plc1",
|
||||
SourceReference = "ns=2;s=Boiler"
|
||||
};
|
||||
var configFalse = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1", TemplateId = 1, SiteId = 1,
|
||||
NativeAlarmSources = [baseSource]
|
||||
};
|
||||
var configTrue = configFalse with { NativeAlarmSources = [baseSource with { IsMesReportable = true }] };
|
||||
|
||||
Assert.NotEqual(_sut.ComputeHash(configFalse), _sut.ComputeHash(configTrue));
|
||||
// false is omitted from the canonical JSON, so adding the field cannot invalidate stored hashes.
|
||||
Assert.Equal(_sut.ComputeHash(configFalse),
|
||||
_sut.ComputeHash(configFalse with { NativeAlarmSources = [baseSource with { IsMesReportable = false }] }));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing a native alarm source's locked flag changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceIsLockedChange_ChangesHash()
|
||||
|
||||
@@ -227,6 +227,7 @@ public sealed class EntitySerializerTests
|
||||
IsLocked = true,
|
||||
IsInherited = true,
|
||||
LockedInDerived = true,
|
||||
IsMesReportable = true,
|
||||
});
|
||||
|
||||
var aggregate = MakeEmptyAggregate() with { Templates = new[] { basic } };
|
||||
@@ -242,6 +243,7 @@ public sealed class EntitySerializerTests
|
||||
Assert.True(dtoSrc.IsLocked);
|
||||
Assert.True(dtoSrc.IsInherited);
|
||||
Assert.True(dtoSrc.LockedInDerived);
|
||||
Assert.True(dtoSrc.IsMesReportable);
|
||||
|
||||
var rtSrc = Assert.Single(Assert.Single(sut.FromBundleContent(dto).Templates).NativeAlarmSources);
|
||||
Assert.Equal("Boiler", rtSrc.Name);
|
||||
@@ -252,6 +254,27 @@ public sealed class EntitySerializerTests
|
||||
Assert.True(rtSrc.IsLocked);
|
||||
Assert.True(rtSrc.IsInherited);
|
||||
Assert.True(rtSrc.LockedInDerived);
|
||||
Assert.True(rtSrc.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #59: an instance native-source override round-trips its tri-state MES flag.</summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
[InlineData(null)]
|
||||
public void Roundtrip_instance_NativeAlarmSourceOverride_preserves_IsMesReportableOverride(bool? flag)
|
||||
{
|
||||
var inst = new Instance("I1") { Id = 1, TemplateId = 1, SiteId = 1 };
|
||||
inst.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Boiler")
|
||||
{ InstanceId = 1, IsMesReportableOverride = flag });
|
||||
var aggregate = MakeEmptyAggregate() with { Instances = new[] { inst } };
|
||||
|
||||
var sut = new EntitySerializer();
|
||||
var dto = sut.ToBundleContent(aggregate);
|
||||
Assert.Equal(flag, Assert.Single(Assert.Single(dto.Instances).NativeAlarmSourceOverrides).IsMesReportableOverride);
|
||||
|
||||
var rt = Assert.Single(sut.FromBundleContent(dto).Instances);
|
||||
Assert.Equal(flag, Assert.Single(rt.NativeAlarmSourceOverrides).IsMesReportableOverride);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that old-form bundle JSON without the locked-in-derived field deserializes it as false.</summary>
|
||||
|
||||
Reference in New Issue
Block a user