From f98369a786f907dfdbd723b121aa068f7a5e7370 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 9 Nov 2026 15:09:54 -0500 Subject: [PATCH] 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 --- ...-09-11-mes-reportable-alarm-flag-design.md | 11 + .../Commands/InstanceCommands.cs | 6 +- .../Commands/TemplateCommands.cs | 11 +- src/ZB.MOM.WW.ScadaBridge.CLI/README.md | 10 +- .../Pages/Deployment/InstanceConfigure.razor | 28 +- .../Pages/Design/TemplateEdit.razor | 15 +- .../TemplateNativeAlarmSourceDialog.razor | 16 +- .../InstanceNativeAlarmSourceOverride.cs | 2 + .../Templates/TemplateNativeAlarmSource.cs | 7 + .../Messages/Management/InstanceCommands.cs | 6 +- .../Messages/Management/TemplateCommands.cs | 4 +- .../Messages/Streaming/AlarmStateChanged.cs | 6 +- .../Flattening/FlattenedConfiguration.cs | 5 + .../Types/Scripts/ScriptAlarm.cs | 2 +- ...NativeAlarmSourceMesReportable.Designer.cs | 2175 +++++++++++++++++ ...00211_AddNativeAlarmSourceMesReportable.cs | 39 + .../ScadaBridgeDbContextModelSnapshot.cs | 8 +- .../ManagementActor.cs | 12 +- .../Actors/InstanceActor.cs | 11 +- .../Actors/NativeAlarmActor.cs | 3 + .../Flattening/DiffService.cs | 1 + .../Flattening/FlatteningService.cs | 2 + .../Flattening/RevisionHashService.cs | 7 + .../TemplateService.cs | 4 + .../Import/ArtifactDiff.cs | 3 +- .../Import/BundleImporter.cs | 6 + .../Import/TemplateChildEquality.cs | 1 + .../Serialization/EntityDtos.cs | 8 +- .../Serialization/EntitySerializer.cs | 8 +- .../EditingParityTests.cs | 4 +- .../SchemaConfigurationTests.cs | 30 + .../ManagementActorTests.cs | 31 + .../NativeAlarmSourceNameValidationTests.cs | 19 + .../Actors/InstanceActorNativeAlarmTests.cs | 21 +- .../Actors/NativeAlarmActorTests.cs | 29 +- .../Flattening/FlatteningServiceTests.cs | 25 + .../Flattening/RevisionHashServiceTests.cs | 23 + .../Serialization/EntitySerializerTests.cs | 23 + 38 files changed, 2579 insertions(+), 43 deletions(-) create mode 100644 src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.Designer.cs create mode 100644 src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.cs diff --git a/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md b/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md index e681a136..1b97687e 100644 --- a/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md +++ b/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md @@ -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 diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs index 37e372c2..638c0894 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs @@ -651,9 +651,10 @@ public static class InstanceCommands var setConnectionOption = new Option("--connection") { Description = "Connection name override (blank = inherited)" }; var setSourceRefOption = new Option("--source-ref") { Description = "Source reference override (blank = inherited)" }; var setFilterOption = new Option("--filter") { Description = "Condition filter override (blank = inherited)" }; + var setMesOption = new Option("--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); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs index a64066c1..cc863ba1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs @@ -180,6 +180,7 @@ public static class TemplateCommands var filterOption = new Option("--filter") { Description = "Condition filter; omit to clear and mirror all conditions" }; var descriptionOption = new Option("--description") { Description = "Description; omit to clear" }; var lockedOption = new Option("--locked") { Description = "Lock status; omit for unlocked" }; + var mesOption = new Option("--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("--description") { Description = "Description" }; var lockedOption = new Option("--locked") { Description = "Lock status" }; lockedOption.DefaultValueFactory = _ => false; + var mesOption = new Option("--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); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md index 2fcdd81b..032a696c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md @@ -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 template native-alarm-source add --template-id --name --connection --source-ref [--filter ] [--description ] [--locked] +scadabridge --url template native-alarm-source add --template-id --name --connection --source-ref [--filter ] [--description ] [--locked] [--mes-reportable] ``` | Option | Required | Description | @@ -345,6 +345,7 @@ scadabridge --url template native-alarm-source add --template-id --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 template native-alarm-source list --template-id Replace the fields of an existing native alarm source binding. ```sh -scadabridge template native-alarm-source update --id --name --connection --source-ref [--filter ] [--description ] [--locked] +scadabridge template native-alarm-source update --id --name --connection --source-ref [--filter ] [--description ] [--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 instance native-alarm-source set --instance-id --source [--connection ] [--source-ref ] [--filter ] +scadabridge --url instance native-alarm-source set --instance-id --source [--connection ] [--source-ref ] [--filter ] [--mes-reportable ] ``` | Option | Required | Description | @@ -631,6 +632,7 @@ scadabridge --url instance native-alarm-source set --instance-id --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` diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor index 77497d9e..1c267ee0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor @@ -653,6 +653,7 @@ Connection override Source reference override Filter override + MES reportable Actions @@ -697,6 +698,15 @@ value="@(_nasFilterEdit.GetValueOrDefault(src.Name) ?? "")" @onchange="e => _nasFilterEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" /> + + +
+
+
+ + +
+
@if (_error != null) {
@_error
@@ -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; } /// Connections whose protocol can surface native alarms (OPC UA / MxGateway). [Parameter] public IReadOnlyList 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 @@ /// Optional condition filter; blank mirrors every condition. /// Optional free-text description. /// Whether instances may override the binding. + /// Whether every condition under the source is MES-reportable. public sealed record NativeSourceDraft( string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, - bool IsLocked); + bool IsLocked, + bool IsMesReportable = false); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs index 9a2bdb4a..63702235 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs @@ -24,6 +24,8 @@ public class InstanceNativeAlarmSourceOverride public string? SourceReferenceOverride { get; set; } /// Overrides the condition filter when set; null = keep inherited. public string? ConditionFilterOverride { get; set; } + /// Overrides the binding's IsMesReportable flag when set; null = keep inherited. + public bool? IsMesReportableOverride { get; set; } /// Initializes a new override for the specified source binding. /// Canonical name of the source to override. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs index 23f244d5..14aaa226 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs @@ -23,6 +23,13 @@ public class TemplateNativeAlarmSource public string SourceReference { get; set; } = string.Empty; /// Optional condition filter; null = mirror all conditions under the source. public string? ConditionFilter { get; set; } + /// + /// When true, every condition mirrored under this binding is MES-reportable + /// (AlarmStateChanged.IsMesReportable / ScriptAlarm.IsMesReportable). Additive to the + /// 900-999 severity-band convention, which still flags a condition on its own. Default false. + /// Overridable per instance via InstanceNativeAlarmSourceOverride.IsMesReportableOverride. + /// + public bool IsMesReportable { get; set; } /// When true, this binding cannot be overridden in derived templates. public bool IsLocked { get; set; } /// True when copied from a base template and not yet overridden on the derived template. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs index 92eb4370..3a6e2a16 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs @@ -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); /// /// Bulk, all-or-nothing apply of per-instance native-alarm-source overrides — the diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs index ba6f1774..5af223e4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs @@ -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"): diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs index 6e94db13..1dd58d47 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs @@ -116,8 +116,10 @@ public record AlarmStateChanged( /// /// True when the alarm's deployed configuration flags it as MES-reportable /// (ResolvedAlarm.IsMesReportable). 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 (ResolvedNativeAlarmSource.IsMesReportable, Gitea #59), set by + /// the owning NativeAlarmActor — there is still no per-condition config row; the + /// 900-999 severity band remains an independent, additive signal. Carried to central on /// AlarmStateUpdate field 25 and DebugAlarmStateDto field 20 (Gitea #60); /// a pre-#60 site sends the proto3 default. Additive; default false. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs index 934a9a75..0bae383d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs @@ -160,6 +160,11 @@ public sealed record ResolvedNativeAlarmSource /// Gets the source of this binding: "Template", "Inherited", "Composed", or "Override". public string Source { get; init; } = "Template"; /// + /// Gets whether every condition under this binding is MES-reportable (effective value after + /// the instance override). Additive to the 900-999 severity band. + /// + public bool IsMesReportable { get; init; } + /// /// 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. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs index 7fa17c31..68b271d9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs @@ -57,7 +57,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; /// /// /// 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). /// public sealed record ScriptAlarm( string Name, diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.Designer.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.Designer.cs new file mode 100644 index 00000000..1740f6c4 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.Designer.cs @@ -0,0 +1,2175 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; + +#nullable disable + +namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations +{ + [DbContext(typeof(ScadaBridgeDbContext))] + [Migration("20261109200211_AddNativeAlarmSourceMesReportable")] + partial class AddNativeAlarmSourceMesReportable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Xml") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AfterStateJson") + .HasColumnType("nvarchar(max)"); + + b.Property("BundleImportId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("User") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("BundleImportId") + .HasDatabaseName("IX_AuditLogEntries_BundleImportId"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("Timestamp"); + + b.HasIndex("User"); + + b.HasIndex("EntityType", "Timestamp") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLogEntries_EntityType_Timestamp"); + + b.HasIndex("Timestamp", "Id") + .IsDescending() + .HasDatabaseName("IX_AuditLogEntries_Timestamp_Id"); + + b.HasIndex("User", "Timestamp") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLogEntries_User_Timestamp"); + + b.ToTable("AuditLogEntries"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.SiteCall", b => + { + b.Property("TrackedOperationId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("HttpStatus") + .HasColumnType("int"); + + b.Property("IngestedAtUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("SourceNode") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("SourceSite") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("Target") + .IsRequired() + .HasMaxLength(256) + .IsUnicode(false) + .HasColumnType("varchar(256)"); + + b.Property("TerminalAtUtc") + .HasColumnType("datetime2"); + + b.Property("UpdatedAtUtc") + .HasColumnType("datetime2"); + + b.HasKey("TrackedOperationId"); + + b.HasIndex("CreatedAtUtc") + .HasDatabaseName("IX_SiteCalls_NonTerminal") + .HasFilter("[TerminalAtUtc] IS NULL"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("CreatedAtUtc"), new[] { "SourceSite", "SourceNode", "Status" }); + + b.HasIndex("TerminalAtUtc") + .HasDatabaseName("IX_SiteCalls_Terminal") + .HasFilter("[TerminalAtUtc] IS NOT NULL"); + + b.HasIndex("CreatedAtUtc", "TrackedOperationId") + .IsDescending() + .HasDatabaseName("IX_SiteCalls_Created_TrackedOperationId"); + + b.HasIndex("SourceSite", "CreatedAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_SiteCalls_Source_Created"); + + b.HasIndex("Status", "UpdatedAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_SiteCalls_Status_Updated"); + + b.ToTable("SiteCalls", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeployedConfigSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeployedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DeploymentId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("RevisionHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("DeploymentId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DeployedConfigSnapshots"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeploymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DeployedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DeployedBy") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DeploymentId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("RevisionHash") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("DeploymentId") + .IsUnique(); + + b.HasIndex("InstanceId"); + + b.HasIndex("DeployedAt", "Id") + .IsDescending() + .HasDatabaseName("IX_DeploymentRecords_DeployedAt_Id"); + + b.ToTable("DeploymentRecords"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.PendingDeployment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfigurationJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("DeploymentId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("RevisionHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("DeploymentId") + .IsUnique(); + + b.HasIndex("ExpiresAtUtc"); + + b.HasIndex("InstanceId"); + + b.ToTable("PendingDeployments", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.SystemArtifactDeploymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ArtifactType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DeployedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DeployedBy") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PerSiteStatus") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("DeployedAt"); + + b.ToTable("SystemArtifactDeploymentRecords"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.DatabaseConnectionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("MaxRetries") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RetryDelay") + .HasColumnType("time"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DatabaseConnectionDefinitions"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthConfiguration") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("AuthType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("EndpointUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MaxRetries") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RetryDelay") + .HasColumnType("time"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ExternalSystemDefinitions"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ExternalSystemDefinitionId") + .HasColumnType("int"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParameterDefinitions") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ReturnDefinition") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalSystemDefinitionId", "Name") + .IsUnique(); + + b.ToTable("ExternalSystemMethods"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi.ApiMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParameterDefinitions") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ReturnDefinition") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Script") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TimeoutSeconds") + .HasColumnType("int"); + + b.Property("TraceAllCalls") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ApiMethods"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParentAreaId") + .HasColumnType("int"); + + b.Property("SiteId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentAreaId"); + + b.HasIndex("SiteId", "ParentAreaId", "Name") + .IsUnique() + .HasFilter("[ParentAreaId] IS NOT NULL"); + + b.ToTable("Areas"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("int"); + + b.Property("SiteId") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("SiteId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAlarmOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlarmCanonicalName") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("IsMesReportableOverride") + .HasColumnType("bit"); + + b.Property("PriorityLevelOverride") + .HasColumnType("int"); + + b.Property("TriggerConfigurationOverride") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "AlarmCanonicalName") + .IsUnique(); + + b.ToTable("InstanceAlarmOverrides"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAttributeOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AttributeName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ElementDataType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("OverrideValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "AttributeName") + .IsUnique(); + + b.ToTable("InstanceAttributeOverrides"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceConnectionBinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AttributeName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DataConnectionId") + .HasColumnType("int"); + + b.Property("DataSourceReferenceOverride") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DataConnectionId"); + + b.HasIndex("InstanceId", "AttributeName") + .IsUnique(); + + b.ToTable("InstanceConnectionBindings"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceNativeAlarmSourceOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConditionFilterOverride") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ConnectionNameOverride") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("IsMesReportableOverride") + .HasColumnType("bit"); + + b.Property("SourceCanonicalName") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("SourceReferenceOverride") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "SourceCanonicalName") + .IsUnique(); + + b.ToTable("InstanceNativeAlarmSourceOverrides"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi.KpiRollupHourly", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HourStartUtc") + .HasColumnType("datetime2"); + + b.Property("MaxValue") + .HasColumnType("float"); + + b.Property("Metric") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("MinValue") + .HasColumnType("float"); + + b.Property("SampleCount") + .HasColumnType("int"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("ScopeKey") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("HourStartUtc") + .HasDatabaseName("IX_KpiRollupHourly_Hour"); + + b.HasIndex("Source", "Metric", "Scope", "ScopeKey", "HourStartUtc") + .IsUnique() + .HasDatabaseName("IX_KpiRollupHourly_Series"); + + b.ToTable("KpiRollupHourly", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi.KpiSample", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Metric") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("ScopeKey") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("CapturedAtUtc") + .HasDatabaseName("IX_KpiSample_Captured"); + + b.HasIndex("Source", "Metric", "Scope", "ScopeKey", "CapturedAtUtc") + .HasDatabaseName("IX_KpiSample_Series"); + + b.ToTable("KpiSample", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.Notification", b => + { + b.Property("NotificationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DeliveredAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastAttemptAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastError") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ListName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NextAttemptAt") + .HasColumnType("datetimeoffset"); + + b.Property("OriginExecutionId") + .HasColumnType("uniqueidentifier"); + + b.Property("OriginParentExecutionId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResolvedTargets") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("SiteEnqueuedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SourceInstanceId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SourceNode") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("SourceScript") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SourceSiteId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("TypeData") + .HasColumnType("nvarchar(max)"); + + b.HasKey("NotificationId"); + + b.HasIndex("DeliveredAt") + .HasDatabaseName("IX_Notifications_Delivered") + .HasFilter("[Status] = 'Delivered'"); + + b.HasIndex("CreatedAt", "NotificationId") + .IsDescending() + .HasDatabaseName("IX_Notifications_Created_NotificationId"); + + b.HasIndex("SourceSiteId", "CreatedAt"); + + b.HasIndex("Status", "NextAttemptAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("NotificationLists"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationRecipient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EmailAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NotificationListId") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.HasKey("Id"); + + b.HasIndex("NotificationListId"); + + b.ToTable("NotificationRecipients"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.SmsConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountSid") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApiBaseUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("AuthToken") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("ConnectionTimeoutSeconds") + .HasColumnType("int"); + + b.Property("FromNumber") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("MaxRetries") + .HasColumnType("int"); + + b.Property("MessagingServiceSid") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RetryDelay") + .HasColumnType("time"); + + b.HasKey("Id"); + + b.ToTable("SmsConfigurations"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.SmtpConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConnectionTimeoutSeconds") + .HasColumnType("int"); + + b.Property("Credentials") + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("FromAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MaxConcurrentConnections") + .HasColumnType("int"); + + b.Property("MaxRetries") + .HasColumnType("int"); + + b.Property("OAuth2Authority") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OAuth2Scope") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("RetryDelay") + .HasColumnType("time"); + + b.Property("TlsMode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Transport") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("SmtpConfigurations"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas.SharedSchema", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SchemaJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Scope") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SharedSchemas"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts.SharedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParameterDefinitions") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ReturnDefinition") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SharedScripts"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites.PendingSecuredWrite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConnectionName") + .IsRequired() + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("DecidedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExecutedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExecutionError") + .HasMaxLength(2048) + .IsUnicode(false) + .HasColumnType("varchar(2048)"); + + b.Property("OperatorComment") + .HasMaxLength(1024) + .IsUnicode(false) + .HasColumnType("varchar(1024)"); + + b.Property("OperatorUser") + .IsRequired() + .HasMaxLength(256) + .IsUnicode(false) + .HasColumnType("varchar(256)"); + + b.Property("SiteId") + .IsRequired() + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)"); + + b.Property("SubmittedAtUtc") + .HasColumnType("datetime2"); + + b.Property("TagPath") + .IsRequired() + .HasMaxLength(512) + .IsUnicode(false) + .HasColumnType("varchar(512)"); + + b.Property("ValueJson") + .IsRequired() + .HasMaxLength(4000) + .IsUnicode(false) + .HasColumnType("varchar(4000)"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(128) + .IsUnicode(false) + .HasColumnType("varchar(128)"); + + b.Property("VerifierComment") + .HasMaxLength(1024) + .IsUnicode(false) + .HasColumnType("varchar(1024)"); + + b.Property("VerifierUser") + .HasMaxLength(256) + .IsUnicode(false) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("SiteId") + .HasDatabaseName("IX_PendingSecuredWrites_Site"); + + b.HasIndex("Status", "SubmittedAtUtc") + .HasDatabaseName("IX_PendingSecuredWrites_Status_Submitted"); + + b.ToTable("PendingSecuredWrites", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.LdapGroupMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("LdapGroupName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("LdapGroupName") + .IsUnique(); + + b.ToTable("LdapGroupMappings"); + + b.HasData( + new + { + Id = 1, + LdapGroupName = "SCADA-Admins", + Role = "Administrator" + }, + new + { + Id = 2, + LdapGroupName = "SCADA-Designers", + Role = "Designer" + }, + new + { + Id = 3, + LdapGroupName = "SCADA-Deploy-All", + Role = "Deployer" + }, + new + { + Id = 4, + LdapGroupName = "SCADA-Deploy-SiteA", + Role = "Deployer" + }, + new + { + Id = 5, + LdapGroupName = "SCADA-Viewers", + Role = "Viewer" + }); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.SiteScopeRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("LdapGroupMappingId") + .HasColumnType("int"); + + b.Property("SiteId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SiteId"); + + b.HasIndex("LdapGroupMappingId", "SiteId") + .IsUnique(); + + b.ToTable("SiteScopeRules"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.DataConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BackupConfiguration") + .HasColumnType("nvarchar(max)"); + + b.Property("FailoverRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(3); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PrimaryConfiguration") + .HasColumnType("nvarchar(max)"); + + b.Property("Protocol") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("SiteId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SiteId", "Name") + .IsUnique(); + + b.ToTable("DataConnections"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ArtifactRevisionHash") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ArtifactsDeployedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("GrpcNodeAAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("GrpcNodeBAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NodeAAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NodeBAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SiteIdentifier") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SiteIdentifier") + .IsUnique(); + + b.ToTable("Sites"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("FolderId") + .HasColumnType("int"); + + b.Property("IsDerived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OwnerCompositionId") + .HasColumnType("int"); + + b.Property("ParentTemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[IsDerived] = 0"); + + b.HasIndex("ParentTemplateId"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateAlarm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("IsInherited") + .HasColumnType("bit"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("IsMesReportable") + .HasColumnType("bit"); + + b.Property("LockedInDerived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OnTriggerScriptId") + .HasColumnType("int"); + + b.Property("PriorityLevel") + .HasColumnType("int"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.Property("TriggerConfiguration") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId", "Name") + .IsUnique(); + + b.ToTable("TemplateAlarms"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DataSourceReference") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ElementDataType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsInherited") + .HasColumnType("bit"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("LockedInDerived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId", "Name") + .IsUnique(); + + b.ToTable("TemplateAttributes"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateComposition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ComposedTemplateId") + .HasColumnType("int"); + + b.Property("InstanceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ComposedTemplateId"); + + b.HasIndex("TemplateId", "InstanceName") + .IsUnique(); + + b.ToTable("TemplateCompositions"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateFolder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParentFolderId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentFolderId", "Name") + .IsUnique() + .HasFilter("[ParentFolderId] IS NOT NULL"); + + b.ToTable("TemplateFolders"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateNativeAlarmSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConditionFilter") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ConnectionName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("IsInherited") + .HasColumnType("bit"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("IsMesReportable") + .HasColumnType("bit"); + + b.Property("LockedInDerived") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SourceReference") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId", "Name") + .IsUnique(); + + b.ToTable("TemplateNativeAlarmSources"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionTimeoutSeconds") + .HasColumnType("int"); + + b.Property("IsInherited") + .HasColumnType("bit"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("LockedInDerived") + .HasColumnType("bit"); + + b.Property("MinTimeBetweenRuns") + .HasColumnType("time"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ParameterDefinitions") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ReturnDefinition") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.Property("TriggerConfiguration") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("TriggerType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId", "Name") + .IsUnique(); + + b.ToTable("TemplateScripts"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Transport.BundleUnlockAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AttemptedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ClientKey") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("ClientKey", "AttemptedAtUtc") + .HasDatabaseName("IX_BundleUnlockAttempt_Client_Attempted"); + + b.ToTable("BundleUnlockAttempt", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities.AuditLogRow", b => + { + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("OccurredAtUtc") + .HasColumnType("datetime2"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("Actor") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)") + .HasColumnName("Category"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("DetailsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("uniqueidentifier") + .HasComputedColumnSql("CAST(JSON_VALUE(DetailsJson,'$.executionId') AS uniqueidentifier)", true); + + b.Property("IngestedAtUtc") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2(7)") + .HasComputedColumnSql("CAST(SWITCHOFFSET(CAST(JSON_VALUE(DetailsJson,'$.ingestedAtUtc') AS datetimeoffset), 0) AS datetime2(7))", false); + + b.Property("Kind") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)") + .HasComputedColumnSql("JSON_VALUE(DetailsJson,'$.kind')", true); + + b.Property("Outcome") + .IsRequired() + .HasMaxLength(16) + .IsUnicode(false) + .HasColumnType("varchar(16)"); + + b.Property("ParentExecutionId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("uniqueidentifier") + .HasComputedColumnSql("CAST(JSON_VALUE(DetailsJson,'$.parentExecutionId') AS uniqueidentifier)", true); + + b.Property("SourceNode") + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)"); + + b.Property("SourceSiteId") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(64) + .IsUnicode(false) + .HasColumnType("varchar(64)") + .HasComputedColumnSql("JSON_VALUE(DetailsJson,'$.sourceSiteId')", true); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(32) + .IsUnicode(false) + .HasColumnType("varchar(32)") + .HasComputedColumnSql("JSON_VALUE(DetailsJson,'$.status')", true); + + b.Property("Target") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("EventId", "OccurredAtUtc"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("IX_AuditLog_CorrelationId") + .HasFilter("[CorrelationId] IS NOT NULL"); + + b.HasIndex("ExecutionId") + .HasDatabaseName("IX_AuditLog_Execution"); + + b.HasIndex("OccurredAtUtc") + .IsDescending() + .HasDatabaseName("IX_AuditLog_OccurredAtUtc"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("OccurredAtUtc"), new[] { "Status" }); + + b.HasIndex("ParentExecutionId") + .HasDatabaseName("IX_AuditLog_ParentExecution"); + + b.HasIndex("Actor", "OccurredAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLog_Actor_Occurred"); + + b.HasIndex("Kind", "OccurredAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLog_Kind_Occurred"); + + b.HasIndex("SourceNode", "OccurredAtUtc") + .HasDatabaseName("IX_AuditLog_Node_Occurred"); + + b.HasIndex("SourceSiteId", "OccurredAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLog_Site_Occurred"); + + b.HasIndex("Target", "OccurredAtUtc") + .IsDescending(false, true) + .HasDatabaseName("IX_AuditLog_Target_Occurred") + .HasFilter("[Target] IS NOT NULL"); + + b.HasIndex("Channel", "Status", "OccurredAtUtc") + .IsDescending(false, false, true) + .HasDatabaseName("IX_AuditLog_Channel_Status_Occurred"); + + b.ToTable("AuditLog", (string)null); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeployedConfigSnapshot", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeploymentRecord", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.PendingDeployment", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemMethod", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemDefinition", null) + .WithMany() + .HasForeignKey("ExternalSystemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", null) + .WithMany("Children") + .HasForeignKey("ParentAreaId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", null) + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", null) + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", null) + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAlarmOverride", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany("AlarmOverrides") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAttributeOverride", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany("AttributeOverrides") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceConnectionBinding", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.DataConnection", null) + .WithMany() + .HasForeignKey("DataConnectionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany("ConnectionBindings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceNativeAlarmSourceOverride", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", null) + .WithMany("NativeAlarmSourceOverrides") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationRecipient", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationList", null) + .WithMany("Recipients") + .HasForeignKey("NotificationListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.SiteScopeRule", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.LdapGroupMapping", null) + .WithMany() + .HasForeignKey("LdapGroupMappingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", null) + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.DataConnection", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", null) + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateFolder", null) + .WithMany() + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany() + .HasForeignKey("ParentTemplateId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateAlarm", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany("Alarms") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateAttribute", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany("Attributes") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateComposition", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany() + .HasForeignKey("ComposedTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany("Compositions") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateFolder", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateFolder", null) + .WithMany() + .HasForeignKey("ParentFolderId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateNativeAlarmSource", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany("NativeAlarmSources") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateScript", b => + { + b.HasOne("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", null) + .WithMany("Scripts") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", b => + { + b.Navigation("AlarmOverrides"); + + b.Navigation("AttributeOverrides"); + + b.Navigation("ConnectionBindings"); + + b.Navigation("NativeAlarmSourceOverrides"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationList", b => + { + b.Navigation("Recipients"); + }); + + modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template", b => + { + b.Navigation("Alarms"); + + b.Navigation("Attributes"); + + b.Navigation("Compositions"); + + b.Navigation("NativeAlarmSources"); + + b.Navigation("Scripts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.cs new file mode 100644 index 00000000..095722f0 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20261109200211_AddNativeAlarmSourceMesReportable.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations +{ + /// + public partial class AddNativeAlarmSourceMesReportable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsMesReportable", + table: "TemplateNativeAlarmSources", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsMesReportableOverride", + table: "InstanceNativeAlarmSourceOverrides", + type: "bit", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsMesReportable", + table: "TemplateNativeAlarmSources"); + + migrationBuilder.DropColumn( + name: "IsMesReportableOverride", + table: "InstanceNativeAlarmSourceOverrides"); + } + } +} diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs index 6767d371..88fede6e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs @@ -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("InstanceId") .HasColumnType("int"); + b.Property("IsMesReportableOverride") + .HasColumnType("bit"); + b.Property("SourceCanonicalName") .IsRequired() .HasMaxLength(400) @@ -1673,6 +1676,9 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations b.Property("IsLocked") .HasColumnType("bit"); + b.Property("IsMesReportable") + .HasColumnType("bit"); + b.Property("LockedInDerived") .HasColumnType("bit"); diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 4d265dc2..08a4790b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -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. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs index 286e691b..a4244972 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs @@ -59,9 +59,16 @@ public class InstanceActor : ReceiveActor /// children are created and read back in , 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). /// private readonly Dictionary _alarmMesReportable = new(); + + /// + /// Native binding canonical name → ResolvedNativeAlarmSource.IsMesReportable (Gitea #59), + /// used to stamp the quiet-binding placeholder rows. Live condition rows are stamped by the + /// that owns the binding. + /// + private readonly Dictionary _nativeAlarmMesReportable = new(); private readonly Dictionary _scriptActors = new(); private readonly Dictionary _alarmActors = new(); private readonly Dictionary _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( diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs index 359f1268..70981d34 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs @@ -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 diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs index 7cea9049..e32d1bbc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs @@ -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) => diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs index c8d3118e..1fca35fb 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs @@ -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" }; } diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs index 4f30b4bb..9c8ad5f1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs @@ -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 /// public bool IsLocked { get; init; } /// + /// MES-reportable binding flag; null when false (the serializer drops nulls), so + /// pre-existing configurations hash identically. + /// + public bool? IsMesReportable { get; init; } + /// /// The source reference (OPC UA SourceNode/notifier nodeId, or MxAccess object/area). /// public string SourceReference { get; init; } = string.Empty; diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs index f4328729..ac79c963 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs @@ -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); diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs index 8702317a..7d394605 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs @@ -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); diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs index c2a7a36d..8f6792dd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs @@ -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) diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs index 9812fa54..f0771bf1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs @@ -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; diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs index 8a77c043..951f8090 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs @@ -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, diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs index 2128250c..a28af67c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs @@ -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) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs index c1bc01c2..20f0e2cb 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs @@ -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}")] diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs index dfa72c81..ae9f6bcd 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs @@ -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. + /// Verifies that native alarm sources and their overrides persist the MES-reportable flags. + /// A task that represents the asynchronous operation. + [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().SingleAsync(a => a.Id == src.Id)).IsMesReportable); + Assert.False((await _context.Set().SingleAsync(o => o.Id == ovr.Id)).IsMesReportableOverride); + } } public class SplitQueryBehaviourTests : IDisposable diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs index 7f4b823c..8c90853c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs @@ -4706,6 +4706,37 @@ public class ManagementActorTests : TestKit, IDisposable _templateRepo.ReceivedWithAnyArgs(1).SaveChangesAsync(default); } + /// Gitea #59: the instance override command carries IsMesReportableOverride onto the persisted row. + [Fact] + public void SetInstanceNativeAlarmSourceOverride_CarriesIsMesReportableOverride() + { + var pipeline = Substitute.For(); + var flattened = new FlattenedConfiguration + { + NativeAlarmSources = new List + { + new() { CanonicalName = "Pressure", IsLocked = false, Source = "Template" } + } + }; + pipeline.FlattenAndValidateAsync(1, Arg.Any(), Arg.Any()) + .Returns(Result.Success( + new FlatteningPipelineResult(flattened, "hash", ValidationResult.Success()))); + _services.AddScoped(_ => pipeline); + + InstanceNativeAlarmSourceOverride? added = null; + _templateRepo + .When(r => r.AddInstanceNativeAlarmSourceOverrideAsync(Arg.Any(), Arg.Any())) + .Do(ci => added = ci.Arg()); + + CreateActor().Tell(Envelope( + new SetInstanceNativeAlarmSourceOverrideCommand(1, "Pressure", null, null, null, IsMesReportableOverride: true), + "Deployer")); + + ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.NotNull(added); + Assert.True(added!.IsMesReportableOverride); + } + /// Verifies that set instance native alarm source override, when with design role, returns unauthorized. [Fact] public void SetInstanceNativeAlarmSourceOverride_WithDesignRole_ReturnsUnauthorized() diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/NativeAlarmSourceNameValidationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/NativeAlarmSourceNameValidationTests.cs index c4ebfcf3..90180b38 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/NativeAlarmSourceNameValidationTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/NativeAlarmSourceNameValidationTests.cs @@ -50,6 +50,25 @@ public class NativeAlarmSourceNameValidationTests : TestKit _repo.DidNotReceive().AddTemplateNativeAlarmSourceAsync(Arg.Any(), Arg.Any()); } + /// Gitea #59: the update command carries IsMesReportable onto the persisted binding. + [Fact] + public void Update_CarriesIsMesReportable() + { + TemplateNativeAlarmSource? updated = null; + _repo.When(r => r.UpdateTemplateNativeAlarmSourceAsync(Arg.Any(), Arg.Any())) + .Do(ci => updated = ci.Arg()); + _services.AddScoped(); + + 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(TimeSpan.FromSeconds(5)); + Assert.NotNull(updated); + Assert.True(updated!.IsMesReportable); + } + /// Renaming a source to an illegal actor name is rejected before any write. [Fact] public void Update_RenameToInvalidActorName_IsRejected() diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs index b7c6c787..49e05612 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs @@ -51,7 +51,7 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable NullLogger.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); } + /// Gitea #59: the quiet-binding placeholder row carries the binding's MES flag. + [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(); + + actor.Tell(new SubscribeDebugViewRequest("inst", "c")); + var snap = ExpectMsg(); + + Assert.Equal(flag, Assert.Single(snap.AlarmStates, a => a.IsConfiguredPlaceholder).IsMesReportable); + } + /// Verifies that a quiet native binding emits a placeholder in the snapshot. [Fact] public void Snapshot_QuietNativeBinding_EmitsPlaceholder() diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs index 500e5516..7558c007 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs @@ -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.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); } + /// Gitea #59: a binding flagged MES-reportable stamps every condition it mirrors; an unflagged one does not. + [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(); + + 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().IsMesReportable); + } + /// Verifies that a snapshot completing with a missing prior alarm emits a return to normal. [Fact] public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal() diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs index eff33db7..0a063d41 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs @@ -952,6 +952,31 @@ public class FlatteningServiceTests Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source); } + /// Gitea #59: the binding flag flattens from the template and a tri-state instance override wins (null inherits). + [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>(), + new Dictionary>(), + new Dictionary()); + + Assert.True(result.IsSuccess); + Assert.Equal(expected, result.Value.NativeAlarmSources[0].IsMesReportable); + } + /// Verifies that a locked native alarm source ignores an instance override. [Fact] public void Flatten_LockedNativeAlarmSource_IgnoresInstanceOverride() diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs index 4b64debf..5d535be5 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs @@ -413,6 +413,29 @@ public class RevisionHashServiceTests Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter)); } + /// Gitea #59: the binding's MES-reportable flag participates in the hash; false hashes as before the field existed. + [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 }] })); + } + /// Verifies that changing a native alarm source's locked flag changes the hash. [Fact] public void ComputeHash_NativeAlarmSourceIsLockedChange_ChangesHash() diff --git a/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs index 05177e89..5e02d61d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs @@ -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); + } + + /// Gitea #59: an instance native-source override round-trips its tri-state MES flag. + [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); } /// Verifies that old-form bundle JSON without the locked-in-derived field deserializes it as false.