feat(management): IsMesReportable on alarm commands, IsMesReportableOverride on SetInstanceAlarmOverride
Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
@@ -34,13 +34,23 @@ public record SetInstanceAreaCommand(int InstanceId, int? AreaId);
|
||||
/// Sets (or upserts) a per-instance alarm override. For HiLo trigger types the
|
||||
/// <c>TriggerConfigurationOverride</c> JSON is merged setpoint-by-setpoint with
|
||||
/// the inherited config; for binary trigger types it replaces the whole config.
|
||||
/// Either field is optional — pass null to leave it unchanged.
|
||||
/// Each override field is optional — null means the instance inherits the template value
|
||||
/// for that field (the row is replaced whole on every call, not merged).
|
||||
/// </summary>
|
||||
/// <param name="InstanceId">ID of the instance the override applies to.</param>
|
||||
/// <param name="AlarmCanonicalName">Canonical (path-qualified for composed members) alarm name.</param>
|
||||
/// <param name="TriggerConfigurationOverride">Trigger configuration JSON override, or null.</param>
|
||||
/// <param name="PriorityLevelOverride">Priority level override, or null.</param>
|
||||
/// <param name="IsMesReportableOverride">
|
||||
/// Per-instance override of the template alarm's MES-reportable flag. Additive trailing
|
||||
/// optional; null (the default) inherits the template value, true/false forces it.
|
||||
/// </param>
|
||||
public record SetInstanceAlarmOverrideCommand(
|
||||
int InstanceId,
|
||||
string AlarmCanonicalName,
|
||||
string? TriggerConfigurationOverride,
|
||||
int? PriorityLevelOverride);
|
||||
int? PriorityLevelOverride,
|
||||
bool? IsMesReportableOverride = null);
|
||||
|
||||
public record DeleteInstanceAlarmOverrideCommand(
|
||||
int InstanceId,
|
||||
|
||||
@@ -42,8 +42,11 @@ public record ResyncInheritedMembersCommand(int TemplateId);
|
||||
public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null);
|
||||
public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null);
|
||||
public record DeleteTemplateAttributeCommand(int AttributeId);
|
||||
public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked);
|
||||
public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked);
|
||||
// IsMesReportable is an additive trailing optional (default false = "not reported to MES"):
|
||||
// it mirrors TemplateAlarm.IsMesReportable and marks the alarm for inclusion in the MES
|
||||
// alarm-status feed. Additive-only: never reorder the existing parameters.
|
||||
public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false);
|
||||
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);
|
||||
|
||||
@@ -1175,7 +1175,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.SetAlarmOverrideAsync(
|
||||
cmd.InstanceId, cmd.AlarmCanonicalName,
|
||||
cmd.TriggerConfigurationOverride, cmd.PriorityLevelOverride,
|
||||
user.Username);
|
||||
user.Username, cmd.IsMesReportableOverride);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
@@ -3179,7 +3179,8 @@ public class ManagementActor : ReceiveActor
|
||||
PriorityLevel = cmd.PriorityLevel,
|
||||
Description = cmd.Description,
|
||||
TriggerConfiguration = cmd.TriggerConfiguration,
|
||||
IsLocked = cmd.IsLocked
|
||||
IsLocked = cmd.IsLocked,
|
||||
IsMesReportable = cmd.IsMesReportable
|
||||
};
|
||||
var result = await svc.AddAlarmAsync(cmd.TemplateId, alarm, user);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
@@ -3194,7 +3195,8 @@ public class ManagementActor : ReceiveActor
|
||||
PriorityLevel = cmd.PriorityLevel,
|
||||
Description = cmd.Description,
|
||||
TriggerConfiguration = cmd.TriggerConfiguration,
|
||||
IsLocked = cmd.IsLocked
|
||||
IsLocked = cmd.IsLocked,
|
||||
IsMesReportable = cmd.IsMesReportable
|
||||
};
|
||||
var result = await svc.UpdateAlarmAsync(cmd.AlarmId, alarm, user);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
|
||||
@@ -1014,6 +1014,112 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("Valid values:", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddTemplateAlarm_CarriesIsMesReportable()
|
||||
{
|
||||
// The MES-reportable flag rides AddTemplateAlarmCommand as a trailing
|
||||
// optional (additive-only wire contract) and must reach the TemplateAlarm
|
||||
// the handler hands to TemplateService — verified at the repository
|
||||
// boundary, which is where the entity is persisted.
|
||||
var template = new Template("T1") { Id = 1 };
|
||||
_templateRepo.GetTemplateByIdAsync(1, Arg.Any<CancellationToken>()).Returns(template);
|
||||
_templateRepo.GetAllTemplatesForAnalysisAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Template> { template });
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Template> { template });
|
||||
|
||||
TemplateAlarm? added = null;
|
||||
_templateRepo
|
||||
.When(r => r.AddTemplateAlarmAsync(Arg.Any<TemplateAlarm>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => added = ci.Arg<TemplateAlarm>());
|
||||
_services.AddScoped<TemplateService>();
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new AddTemplateAlarmCommand(1, "HighTemp", "HiLo", 500, null, null, false, IsMesReportable: true),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(added);
|
||||
Assert.True(added!.IsMesReportable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTemplateAlarm_CarriesIsMesReportable()
|
||||
{
|
||||
var template = new Template("T1") { Id = 1 };
|
||||
var existing = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 5,
|
||||
TemplateId = 1,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
PriorityLevel = 100,
|
||||
IsMesReportable = false
|
||||
};
|
||||
_templateRepo.GetTemplateAlarmByIdAsync(5, Arg.Any<CancellationToken>()).Returns(existing);
|
||||
_templateRepo.GetTemplateByIdAsync(1, Arg.Any<CancellationToken>()).Returns(template);
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Template> { template });
|
||||
|
||||
TemplateAlarm? updated = null;
|
||||
_templateRepo
|
||||
.When(r => r.UpdateTemplateAlarmAsync(Arg.Any<TemplateAlarm>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => updated = ci.Arg<TemplateAlarm>());
|
||||
_services.AddScoped<TemplateService>();
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new UpdateTemplateAlarmCommand(5, "HighTemp", "HiLo", 500, null, null, false, IsMesReportable: true),
|
||||
"Designer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(updated);
|
||||
Assert.True(updated!.IsMesReportable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetInstanceAlarmOverride_PassesIsMesReportableOverride()
|
||||
{
|
||||
// false is the interesting case: it is distinct from the null "inherit"
|
||||
// default, so a dropped argument would read as "inherit" rather than
|
||||
// "explicitly not reportable".
|
||||
var template = new Template("T1") { Id = 1 };
|
||||
template.Alarms.Add(new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 5,
|
||||
TemplateId = 1,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
PriorityLevel = 100
|
||||
});
|
||||
var instance = new Instance("Inst1") { Id = 10, TemplateId = 1, SiteId = 1 };
|
||||
|
||||
_templateRepo.GetInstanceByIdAsync(10, Arg.Any<CancellationToken>()).Returns(instance);
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Template> { template });
|
||||
_templateRepo.GetAlarmOverrideAsync(10, "HighTemp", Arg.Any<CancellationToken>())
|
||||
.Returns((InstanceAlarmOverride?)null);
|
||||
|
||||
InstanceAlarmOverride? persisted = null;
|
||||
_templateRepo
|
||||
.When(r => r.AddInstanceAlarmOverrideAsync(Arg.Any<InstanceAlarmOverride>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => persisted = ci.Arg<InstanceAlarmOverride>());
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(
|
||||
new SetInstanceAlarmOverrideCommand(10, "HighTemp", null, null, IsMesReportableOverride: false),
|
||||
"Deployer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotNull(persisted);
|
||||
Assert.False(persisted!.IsMesReportableOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateAttribute_ValidScalar_PersistsValueAndKeepsFixedFields()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user