Cause: - ResultsWriter evaluated selected test-dependent items without the actual TestID. - This caused values such as Test passed(), timestamps, flow, pressure, temperature, conductivity and error data to be empty or incorrect. Solution: 1. Fixed test-aware result evaluation - Uses published regular meter test results. - Passes mtr.Name() as TestID to item.Print(wm, testId). 2. Restored correct test result mapping - Test-dependent values are now resolved from the correct TestRslt / MeterTestRslt. 3. Increased revision - Updated revision to 3.9.3145.101.
1278 lines
39 KiB
C#
1278 lines
39 KiB
C#
///
|
|
/// Copyright (c) 2026 Sensus Slovensko a.s.
|
|
///
|
|
|
|
using Common;
|
|
using log4net;
|
|
using Results.Entities;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using TBF.Rig.Generic;
|
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter;
|
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Formatters;
|
|
using TBF.Rig.Output.DataStorage.UniDataStorageWriter.Interfaces;
|
|
using TBF.Rig.Sequences;
|
|
|
|
namespace TBF.Rig.Output.DB.ResultsWriter
|
|
{
|
|
/// <summary>
|
|
/// Writes evaluated water-meter results through UniDataStorageWriter.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Normal Insert mode preserves the original Caption/value behavior.
|
|
/// XML payload targets generate a clean XML document derived from the
|
|
/// configured customer reference XML. XML element and attribute names have
|
|
/// no predefined semantics in ResultsWriter. Every selected TBF result is
|
|
/// written only to the destination explicitly selected by the user.
|
|
/// The generated payload can either be sent to a Microsoft SQL stored
|
|
/// procedure or written directly to an XML file through
|
|
/// UniDataStorageWriter.
|
|
/// </remarks>
|
|
public class ResultsWriter :
|
|
ComponentBase,
|
|
IOperation,
|
|
GenericDevices.IResultsWriter,
|
|
Generic.IDevice
|
|
{
|
|
private const string RepeatPrefix = "repeat:";
|
|
|
|
private static readonly ILog log =
|
|
LogManager.GetLogger(
|
|
typeof(ResultsWriter));
|
|
|
|
private ResultsWriterCfg resultsWriterCfg;
|
|
private Batch batch;
|
|
private bool opCompleted;
|
|
private bool anyError;
|
|
|
|
private enum OpState
|
|
{
|
|
None,
|
|
WriteResultsScheduled,
|
|
WriteResultsRunning,
|
|
}
|
|
|
|
private OpState currentOpState;
|
|
|
|
private TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writer
|
|
dataStorageWriter;
|
|
|
|
public ResultsWriter()
|
|
{
|
|
}
|
|
|
|
public ResultsWriter(
|
|
IComponentCfg cfg,
|
|
IList<IComponent> components)
|
|
: base(cfg)
|
|
{
|
|
resultsWriterCfg =
|
|
cfg as ResultsWriterCfg;
|
|
|
|
if (resultsWriterCfg == null)
|
|
throw new ArgumentException(
|
|
"resultsWriterCfg");
|
|
|
|
currentOpState =
|
|
OpState.None;
|
|
|
|
log.Warn(
|
|
ToString());
|
|
|
|
//
|
|
// Do not require the parent to be already instantiated here.
|
|
// TBF component loading is sequential and the runtime component list
|
|
// passed to this constructor may not yet contain every configured
|
|
// component. Resolve immediately when possible and defer otherwise.
|
|
//
|
|
TryAssignParent(
|
|
components);
|
|
|
|
if (dataStorageWriter == null)
|
|
{
|
|
log.WarnFormat(
|
|
"Parent '{0}' is not available yet. Parent resolution will be deferred until ResultsWriter is used.",
|
|
resultsWriterCfg.ParentName);
|
|
}
|
|
}
|
|
|
|
public ResultsWriter(
|
|
IComponentCfg cfg)
|
|
: base(cfg)
|
|
{
|
|
resultsWriterCfg =
|
|
cfg as ResultsWriterCfg;
|
|
|
|
if (resultsWriterCfg == null)
|
|
throw new ArgumentException(
|
|
"resultsWriterCfg");
|
|
|
|
currentOpState =
|
|
OpState.None;
|
|
|
|
log.Warn(
|
|
ToString());
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return string.Format(
|
|
"{0}({1})",
|
|
ClassName,
|
|
Cfg.ToString(-1));
|
|
}
|
|
|
|
public override void Initialize()
|
|
{
|
|
}
|
|
|
|
public void RunDeviceBefore()
|
|
{
|
|
}
|
|
|
|
public void RunDeviceAfter()
|
|
{
|
|
}
|
|
|
|
public void StopDevice()
|
|
{
|
|
}
|
|
|
|
public void StopDevice2()
|
|
{
|
|
}
|
|
|
|
public IOperation ProcessResultsOp(
|
|
Batch batch)
|
|
{
|
|
if (!resultsWriterCfg.Enabled)
|
|
return null;
|
|
|
|
if (currentOpState ==
|
|
OpState.WriteResultsRunning)
|
|
{
|
|
throw new Exception(
|
|
"Sequence error");
|
|
}
|
|
|
|
this.batch =
|
|
batch;
|
|
|
|
currentOpState =
|
|
OpState.WriteResultsScheduled;
|
|
|
|
return this;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (currentOpState ==
|
|
OpState.WriteResultsScheduled)
|
|
{
|
|
currentOpState =
|
|
OpState.WriteResultsRunning;
|
|
}
|
|
|
|
opCompleted = false;
|
|
anyError = false;
|
|
}
|
|
|
|
public Event Run()
|
|
{
|
|
log.WarnFormat(
|
|
"{0} : Run() : currentOp = {1}",
|
|
Name,
|
|
currentOpState);
|
|
|
|
if (currentOpState !=
|
|
OpState.WriteResultsRunning)
|
|
{
|
|
return Event.None;
|
|
}
|
|
|
|
if (resultsWriterCfg.DebugLevel ==
|
|
DebugMode.Simulate)
|
|
{
|
|
return Event.ResultsWritten;
|
|
}
|
|
|
|
if (batch == null ||
|
|
batch.WaterMeters == null ||
|
|
batch.WaterMeters.Count == 0)
|
|
{
|
|
return Event.ResultsWritten;
|
|
}
|
|
|
|
if (opCompleted)
|
|
{
|
|
return anyError
|
|
? Event.ErrorProcessingResults
|
|
: Event.ResultsWritten;
|
|
}
|
|
|
|
opCompleted = true;
|
|
|
|
try
|
|
{
|
|
WriteBatchResults(
|
|
batch);
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
anyError = true;
|
|
|
|
log.ErrorFormat(
|
|
"Failed to write results by ResultsWriter: {0}",
|
|
exc);
|
|
}
|
|
|
|
return anyError
|
|
? Event.ErrorProcessingResults
|
|
: Event.ResultsWritten;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
currentOpState =
|
|
OpState.None;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes all enabled water meters in a batch.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// XML payload targets are generated once per batch. Every enabled water
|
|
/// meter contributes one logical repeat record to the same XML document.
|
|
///
|
|
/// Legacy Insert output keeps its existing behavior: every published
|
|
/// regular meter test is written with its concrete test ID context.
|
|
/// </remarks>
|
|
public void WriteBatchResults(
|
|
Batch batch)
|
|
{
|
|
if (batch == null)
|
|
throw new ArgumentNullException(
|
|
nameof(batch));
|
|
|
|
EnsureParentInitialized();
|
|
|
|
resultsWriterCfg.UpdateRuntimeModel();
|
|
|
|
WriterCfg storageCfg =
|
|
GetDataStorageWriterConfiguration();
|
|
|
|
if (storageCfg.UsesXmlPayload())
|
|
{
|
|
WriteXmlBatchResults(
|
|
batch,
|
|
storageCfg);
|
|
|
|
return;
|
|
}
|
|
|
|
if (storageCfg.WriteMode !=
|
|
WriteMode.Insert)
|
|
{
|
|
throw new NotSupportedException(
|
|
string.Format(
|
|
"ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
|
|
storageCfg.DataStorageType,
|
|
storageCfg.TechnologyType,
|
|
storageCfg.WriteMode));
|
|
}
|
|
|
|
foreach (WaterMeter wm
|
|
in batch.WaterMeters)
|
|
{
|
|
if (wm == null ||
|
|
wm.Disabled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Insert mode needs the same test context as the legacy TBF
|
|
// result writers. One row is generated for every published
|
|
// regular meter test and all selected items are evaluated
|
|
// with the actual mtr.Name() as test ID.
|
|
//
|
|
WriteInsertResults(
|
|
wm,
|
|
storageCfg);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates and writes one XML payload for the complete batch.
|
|
/// </summary>
|
|
private void WriteXmlBatchResults(
|
|
Batch batch,
|
|
WriterCfg storageCfg)
|
|
{
|
|
IList<WaterMeter> enabledMeters =
|
|
batch.WaterMeters
|
|
.Where(
|
|
wm =>
|
|
wm != null &&
|
|
!wm.Disabled)
|
|
.ToList();
|
|
|
|
if (enabledMeters.Count == 0)
|
|
return;
|
|
|
|
XmlPayloadRequestBuilder payloadBuilder =
|
|
new XmlPayloadRequestBuilder(
|
|
storageCfg);
|
|
|
|
PayloadGenerationRequest generationRequest =
|
|
BuildBatchPayloadGenerationRequest(
|
|
enabledMeters);
|
|
|
|
string payloadIdentifier =
|
|
BuildBatchPayloadIdentifier(
|
|
enabledMeters);
|
|
|
|
XmlPayloadBuildResult buildResult;
|
|
|
|
if (storageCfg.IsStoredProcedurePayloadTarget())
|
|
{
|
|
buildResult =
|
|
payloadBuilder.Build(
|
|
generationRequest,
|
|
payloadIdentifier);
|
|
}
|
|
else if (storageCfg.IsXmlFileTarget())
|
|
{
|
|
buildResult =
|
|
payloadBuilder.BuildFile(
|
|
generationRequest,
|
|
payloadIdentifier);
|
|
}
|
|
else
|
|
{
|
|
throw new NotSupportedException(
|
|
string.Format(
|
|
"Unsupported XML payload target '{0}' / '{1}' / '{2}'.",
|
|
storageCfg.DataStorageType,
|
|
storageCfg.TechnologyType,
|
|
storageCfg.WriteMode));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(
|
|
buildResult.ArchiveFilePath))
|
|
{
|
|
log.InfoFormat(
|
|
"XML payload archived to '{0}'.",
|
|
buildResult.ArchiveFilePath);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(
|
|
buildResult.OutputFileName))
|
|
{
|
|
log.InfoFormat(
|
|
"XML batch payload generated: {0}",
|
|
buildResult.OutputFileName);
|
|
}
|
|
|
|
WriterDiagnosticResult result =
|
|
dataStorageWriter.SetData(
|
|
buildResult.Request);
|
|
|
|
if (!result.Success)
|
|
{
|
|
throw new Exception(
|
|
result.Message);
|
|
}
|
|
|
|
log.WarnFormat(
|
|
"ResultsWriter wrote one XML payload for {0} enabled water meter(s). Target={1}, Technology={2}, Mode={3}.",
|
|
enabledMeters.Count,
|
|
storageCfg.DataStorageType,
|
|
storageCfg.TechnologyType,
|
|
storageCfg.WriteMode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a stable identifier used by the XML request builder for one batch.
|
|
/// </summary>
|
|
private string BuildBatchPayloadIdentifier(
|
|
IList<WaterMeter> enabledMeters)
|
|
{
|
|
if (enabledMeters == null ||
|
|
enabledMeters.Count == 0)
|
|
{
|
|
return "BATCH";
|
|
}
|
|
|
|
string firstSerial =
|
|
Convert.ToString(
|
|
enabledMeters[0].SerialNr);
|
|
|
|
if (string.IsNullOrWhiteSpace(
|
|
firstSerial))
|
|
{
|
|
return "BATCH";
|
|
}
|
|
|
|
return firstSerial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a dry-run XML payload without calling the database.
|
|
/// </summary>
|
|
/// <param name="simulationBatch">
|
|
/// Batch used to evaluate one-time TBF mappings and provide preview context.
|
|
/// </param>
|
|
/// <param name="simulatedTestCount">
|
|
/// Number of logical repeated result records generated for the preview.
|
|
/// </param>
|
|
public XmlPayloadBuildResult GeneratePreviewPayload(
|
|
Batch simulationBatch,
|
|
int simulatedTestCount)
|
|
{
|
|
if (simulationBatch == null)
|
|
throw new ArgumentNullException(
|
|
nameof(simulationBatch));
|
|
|
|
if (simulatedTestCount < 0)
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(simulatedTestCount));
|
|
|
|
EnsureParentInitialized();
|
|
|
|
resultsWriterCfg.UpdateRuntimeModel();
|
|
|
|
WriterCfg storageCfg =
|
|
GetDataStorageWriterConfiguration();
|
|
|
|
if (!storageCfg.UsesXmlPayload())
|
|
{
|
|
throw new InvalidOperationException(
|
|
"XML preview is available only for an XML payload target.");
|
|
}
|
|
|
|
WaterMeter wm =
|
|
simulationBatch.WaterMeters
|
|
.FirstOrDefault(
|
|
meter =>
|
|
meter != null &&
|
|
!meter.Disabled);
|
|
|
|
if (wm == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Simulation batch does not contain an enabled water meter.");
|
|
}
|
|
|
|
XmlPayloadRequestBuilder builder =
|
|
new XmlPayloadRequestBuilder(
|
|
storageCfg);
|
|
|
|
PayloadGenerationRequest request =
|
|
BuildPreviewGenerationRequest(
|
|
wm,
|
|
simulatedTestCount);
|
|
|
|
request.RepeatPrototypePath =
|
|
builder.GetDefaultRepeatPrototypePath();
|
|
|
|
return builder.BuildPreview(
|
|
request,
|
|
Convert.ToString(
|
|
wm.SerialNr));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes Insert rows for one water meter using the actual TBF test context.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// One row is written for every published regular meter test.
|
|
/// The actual test ID is passed to WMeterRsltItemSpec.Print().
|
|
/// </remarks>
|
|
private void WriteInsertResults(
|
|
WaterMeter wm,
|
|
WriterCfg storageCfg)
|
|
{
|
|
IList<MeterTestRslt> publishedTests =
|
|
wm.RegularMeterTestRslts()
|
|
.Where(
|
|
mtr =>
|
|
mtr != null &&
|
|
mtr.Publish() ==
|
|
Publish.Always)
|
|
.ToList();
|
|
|
|
//
|
|
// Preserve meter/batch data even if there is no published test.
|
|
//
|
|
if (publishedTests.Count == 0)
|
|
{
|
|
DataWriteRequest fallbackRequest =
|
|
BuildInsertRequest(
|
|
wm,
|
|
string.Empty);
|
|
|
|
WriteInsertRequest(
|
|
wm,
|
|
string.Empty,
|
|
fallbackRequest,
|
|
storageCfg);
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (MeterTestRslt mtr
|
|
in publishedTests)
|
|
{
|
|
string testId =
|
|
mtr.Name() ??
|
|
string.Empty;
|
|
|
|
DataWriteRequest request =
|
|
BuildInsertRequest(
|
|
wm,
|
|
testId);
|
|
|
|
WriteInsertRequest(
|
|
wm,
|
|
testId,
|
|
request,
|
|
storageCfg);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds one Insert request for one water meter and one concrete test.
|
|
/// </summary>
|
|
private DataWriteRequest BuildInsertRequest(
|
|
WaterMeter wm,
|
|
string testId)
|
|
{
|
|
DataWriteRequest request =
|
|
new DataWriteRequest
|
|
{
|
|
Mode = WriteMode.Insert
|
|
};
|
|
|
|
if (resultsWriterCfg.SelectedItems == null)
|
|
return request;
|
|
|
|
foreach (Results.WMeterRsltItemSpec item
|
|
in resultsWriterCfg.SelectedItems)
|
|
{
|
|
if (item == null ||
|
|
string.IsNullOrWhiteSpace(
|
|
item.Caption))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
request.InsertItems.Add(
|
|
new InsertWriteItem
|
|
{
|
|
ColumnName =
|
|
item.Caption,
|
|
|
|
Value =
|
|
item.Print(
|
|
wm,
|
|
testId)
|
|
});
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes one prepared Insert request.
|
|
/// </summary>
|
|
private void WriteInsertRequest(
|
|
WaterMeter wm,
|
|
string testId,
|
|
DataWriteRequest request,
|
|
WriterCfg storageCfg)
|
|
{
|
|
if (request == null ||
|
|
request.InsertItems.Count == 0)
|
|
{
|
|
log.WarnFormat(
|
|
"No values to write for WM position {0}",
|
|
wm.WMPosition);
|
|
|
|
return;
|
|
}
|
|
|
|
WriterDiagnosticResult result =
|
|
dataStorageWriter.SetData(
|
|
request);
|
|
|
|
if (!result.Success)
|
|
{
|
|
throw new Exception(
|
|
result.Message);
|
|
}
|
|
|
|
log.WarnFormat(
|
|
"ResultsWriter wrote WM position {0}, TestID='{1}'. Target={2}, Technology={3}, Mode={4}.",
|
|
wm.WMPosition,
|
|
testId,
|
|
storageCfg.DataStorageType,
|
|
storageCfg.TechnologyType,
|
|
storageCfg.WriteMode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates one runtime XML mapping request for the complete batch.
|
|
/// </summary>
|
|
/// <param name="enabledMeters">
|
|
/// Enabled water meters that must be represented in the generated payload.
|
|
/// </param>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Every enabled water meter creates one logical repeat record. All
|
|
/// destinations located inside the repeating XML prototype are populated
|
|
/// into that same record.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Result evaluation first uses the meter-level context. If that does not
|
|
/// produce a value, the same result item is evaluated against the actual
|
|
/// published measurement-test IDs in procedure order. This preserves
|
|
/// meter-level values while also allowing test-result variables to be
|
|
/// populated in the XML record.
|
|
/// </para>
|
|
/// </remarks>
|
|
private PayloadGenerationRequest BuildBatchPayloadGenerationRequest(
|
|
IList<WaterMeter> enabledMeters)
|
|
{
|
|
PayloadGenerationRequest request =
|
|
new PayloadGenerationRequest();
|
|
|
|
if (enabledMeters == null ||
|
|
enabledMeters.Count == 0 ||
|
|
resultsWriterCfg.SelectedItems == null)
|
|
{
|
|
return request;
|
|
}
|
|
|
|
List<Results.WMeterRsltItemSpec> singleItems =
|
|
resultsWriterCfg.SelectedItems
|
|
.Where(
|
|
item =>
|
|
item != null &&
|
|
!string.IsNullOrWhiteSpace(
|
|
item.Caption) &&
|
|
!IsRepeatDestination(
|
|
item.Caption))
|
|
.ToList();
|
|
|
|
List<Results.WMeterRsltItemSpec> repeatItems =
|
|
resultsWriterCfg.SelectedItems
|
|
.Where(
|
|
item =>
|
|
item != null &&
|
|
!string.IsNullOrWhiteSpace(
|
|
item.Caption) &&
|
|
IsRepeatDestination(
|
|
item.Caption))
|
|
.ToList();
|
|
|
|
//
|
|
// One-time XML destinations exist only once in the document.
|
|
// Evaluate them from the first enabled water meter. This preserves
|
|
// the existing one-time mapping model while repeat destinations are
|
|
// created once for every enabled water meter.
|
|
//
|
|
WaterMeter firstMeter =
|
|
enabledMeters[0];
|
|
|
|
for (int singleIndex = 0;
|
|
singleIndex < singleItems.Count;
|
|
singleIndex++)
|
|
{
|
|
Results.WMeterRsltItemSpec item =
|
|
singleItems[
|
|
singleIndex];
|
|
|
|
string sourceKey =
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"Batch.Single.{0}",
|
|
singleIndex);
|
|
|
|
request.SingleMappings.Add(
|
|
new PayloadMapping
|
|
{
|
|
SourceKey =
|
|
sourceKey,
|
|
|
|
DestinationPath =
|
|
item.Caption
|
|
});
|
|
|
|
request.SingleValues.SetValue(
|
|
sourceKey,
|
|
EvaluateXmlMappedValue(
|
|
firstMeter,
|
|
item));
|
|
}
|
|
|
|
//
|
|
// Every enabled water meter becomes one logical repeat record.
|
|
//
|
|
for (int meterIndex = 0;
|
|
meterIndex < enabledMeters.Count;
|
|
meterIndex++)
|
|
{
|
|
WaterMeter wm =
|
|
enabledMeters[
|
|
meterIndex];
|
|
|
|
PayloadRepeatRecord record =
|
|
new PayloadRepeatRecord();
|
|
|
|
for (int repeatIndex = 0;
|
|
repeatIndex < repeatItems.Count;
|
|
repeatIndex++)
|
|
{
|
|
Results.WMeterRsltItemSpec item =
|
|
repeatItems[
|
|
repeatIndex];
|
|
|
|
string sourceKey =
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"Batch.Repeat.{0}.{1}",
|
|
meterIndex,
|
|
repeatIndex);
|
|
|
|
AddRecordMapping(
|
|
record,
|
|
sourceKey,
|
|
item.Caption,
|
|
EvaluateXmlMappedValue(
|
|
wm,
|
|
item));
|
|
}
|
|
|
|
if (record.Mappings.Count > 0)
|
|
{
|
|
request.RepeatRecords.Add(
|
|
record);
|
|
}
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates one configured result item for XML output.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Meter/batch variables normally return a value directly from
|
|
/// Print(wm). Test-result variables require a concrete test ID. For those
|
|
/// variables the method evaluates the published regular meter tests in
|
|
/// measurement-procedure order and uses the first non-empty value.
|
|
///
|
|
/// This fallback is intentionally applied only when the meter-level
|
|
/// evaluation is empty.
|
|
/// </remarks>
|
|
private string EvaluateXmlMappedValue(
|
|
WaterMeter wm,
|
|
Results.WMeterRsltItemSpec item)
|
|
{
|
|
if (wm == null ||
|
|
item == null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
string meterValue =
|
|
item.Print(
|
|
wm);
|
|
|
|
if (!string.IsNullOrWhiteSpace(
|
|
meterValue))
|
|
{
|
|
return meterValue;
|
|
}
|
|
|
|
IList<MeterTestRslt> publishedTests =
|
|
wm.RegularMeterTestRslts()
|
|
.Where(
|
|
mtr =>
|
|
mtr != null &&
|
|
mtr.Publish() ==
|
|
Publish.Always)
|
|
.ToList();
|
|
|
|
string firstResolvedValue =
|
|
string.Empty;
|
|
|
|
string firstResolvedTestId =
|
|
string.Empty;
|
|
|
|
foreach (MeterTestRslt mtr
|
|
in publishedTests)
|
|
{
|
|
string testId =
|
|
mtr.Name() ??
|
|
string.Empty;
|
|
|
|
string testValue =
|
|
item.Print(
|
|
wm,
|
|
testId);
|
|
|
|
if (string.IsNullOrWhiteSpace(
|
|
testValue))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(
|
|
firstResolvedValue))
|
|
{
|
|
firstResolvedValue =
|
|
testValue;
|
|
|
|
firstResolvedTestId =
|
|
testId;
|
|
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// A single XML destination can hold only one scalar value.
|
|
// If more than one test produces a different value for the same
|
|
// mapped result, preserve deterministic procedure order and keep
|
|
// the first value, but make the ambiguity visible in the log.
|
|
//
|
|
if (!string.Equals(
|
|
firstResolvedValue,
|
|
testValue,
|
|
StringComparison.Ordinal))
|
|
{
|
|
log.WarnFormat(
|
|
"XML mapping '{0}' produced values in more than one test context for WM position {1}. Using first test '{2}'.",
|
|
item.Caption,
|
|
wm.WMPosition,
|
|
firstResolvedTestId);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return firstResolvedValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a preview request with an exact number of simulated repeat records.
|
|
/// </summary>
|
|
private PayloadGenerationRequest BuildPreviewGenerationRequest(
|
|
WaterMeter wm,
|
|
int simulatedTestCount)
|
|
{
|
|
PayloadGenerationRequest request =
|
|
new PayloadGenerationRequest();
|
|
|
|
List<Results.WMeterRsltItemSpec> repeatItems =
|
|
new List<Results.WMeterRsltItemSpec>();
|
|
|
|
int sourceIndex = 0;
|
|
|
|
if (resultsWriterCfg.SelectedItems != null)
|
|
{
|
|
foreach (Results.WMeterRsltItemSpec item
|
|
in resultsWriterCfg.SelectedItems)
|
|
{
|
|
if (item == null ||
|
|
string.IsNullOrWhiteSpace(
|
|
item.Caption))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsRepeatDestination(
|
|
item.Caption))
|
|
{
|
|
//
|
|
// A repeating mapping defines a destination inside the
|
|
// repeat prototype. The mapping itself must not create a
|
|
// separate repeat record.
|
|
//
|
|
repeatItems.Add(
|
|
item);
|
|
|
|
continue;
|
|
}
|
|
|
|
string sourceKey =
|
|
"Preview.Single." +
|
|
sourceIndex.ToString(
|
|
CultureInfo.InvariantCulture);
|
|
|
|
string previewValue =
|
|
item.Print(
|
|
wm);
|
|
|
|
//
|
|
// A minimal simulation batch does not contain every possible
|
|
// TBF result source. Use a clearly synthetic type-appropriate
|
|
// value when the evaluated preview value is unavailable.
|
|
//
|
|
if (string.IsNullOrWhiteSpace(
|
|
previewValue))
|
|
{
|
|
previewValue =
|
|
CreateUniversalSimulationValue(
|
|
item,
|
|
sourceIndex);
|
|
}
|
|
|
|
sourceIndex++;
|
|
|
|
request.SingleMappings.Add(
|
|
new PayloadMapping
|
|
{
|
|
SourceKey =
|
|
sourceKey,
|
|
|
|
DestinationPath =
|
|
item.Caption
|
|
});
|
|
|
|
request.SingleValues.SetValue(
|
|
sourceKey,
|
|
previewValue);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Create the requested number of logical repeat records. Every record
|
|
// receives all configured repeating mappings, so one simulated record
|
|
// is structurally equivalent to one generated runtime record.
|
|
//
|
|
for (int recordIndex = 0;
|
|
recordIndex < simulatedTestCount &&
|
|
repeatItems.Count > 0;
|
|
recordIndex++)
|
|
{
|
|
PayloadRepeatRecord record =
|
|
new PayloadRepeatRecord();
|
|
|
|
for (int repeatIndex = 0;
|
|
repeatIndex < repeatItems.Count;
|
|
repeatIndex++)
|
|
{
|
|
Results.WMeterRsltItemSpec item =
|
|
repeatItems[
|
|
repeatIndex];
|
|
|
|
string sourceKey =
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"Preview.Repeat.{0}.{1}",
|
|
recordIndex,
|
|
repeatIndex);
|
|
|
|
int simulationValueIndex =
|
|
recordIndex *
|
|
repeatItems.Count +
|
|
repeatIndex;
|
|
|
|
AddRecordMapping(
|
|
record,
|
|
sourceKey,
|
|
item.Caption,
|
|
CreateUniversalSimulationValue(
|
|
item,
|
|
simulationValueIndex));
|
|
}
|
|
|
|
request.RepeatRecords.Add(
|
|
record);
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
private void AddRecordMapping(
|
|
PayloadRepeatRecord record,
|
|
string sourceKey,
|
|
string destinationPath,
|
|
object value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(
|
|
destinationPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
record.Mappings.Add(
|
|
new PayloadMapping
|
|
{
|
|
SourceKey =
|
|
sourceKey,
|
|
|
|
DestinationPath =
|
|
destinationPath
|
|
});
|
|
|
|
record.Values.SetValue(
|
|
sourceKey,
|
|
value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a clearly synthetic preview value according to the logical
|
|
/// type of a configured TBF result item.
|
|
/// </summary>
|
|
/// <param name="item">
|
|
/// Result item whose logical type should be simulated.
|
|
/// </param>
|
|
/// <param name="simulationIndex">
|
|
/// Zero-based simulation value index.
|
|
/// </param>
|
|
/// <returns>
|
|
/// A non-empty, type-appropriate value suitable for XML preview output.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Preview values are intentionally deterministic and visibly synthetic.
|
|
/// They are used only when a preview source has no real simulation value,
|
|
/// or when repeated test records are created without real measured data.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The method uses the result category description instead of depending
|
|
/// on concrete enum member names. Physical quantity categories therefore
|
|
/// naturally fall back to a numeric simulation value.
|
|
/// </para>
|
|
/// </remarks>
|
|
private string CreateUniversalSimulationValue(
|
|
Results.WMeterRsltItemSpec item,
|
|
int simulationIndex)
|
|
{
|
|
if (item == null)
|
|
{
|
|
return string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"SIMULATION_{0}",
|
|
simulationIndex + 1);
|
|
}
|
|
|
|
string category =
|
|
Common.GetDescription.ToDescription(
|
|
item.Category);
|
|
|
|
string normalizedCategory =
|
|
(category ?? string.Empty)
|
|
.Trim()
|
|
.ToUpperInvariant();
|
|
|
|
//
|
|
// Boolean
|
|
//
|
|
if (normalizedCategory.Contains(
|
|
"BOOLEAN"))
|
|
{
|
|
return simulationIndex % 2 == 0
|
|
? "True"
|
|
: "False";
|
|
}
|
|
|
|
//
|
|
// Date and time
|
|
//
|
|
if (normalizedCategory.Contains(
|
|
"DATE") ||
|
|
normalizedCategory.Contains(
|
|
"TIME"))
|
|
{
|
|
return new DateTime(
|
|
2099,
|
|
1,
|
|
1,
|
|
12,
|
|
0,
|
|
0)
|
|
.AddSeconds(
|
|
simulationIndex + 1)
|
|
.ToString(
|
|
"yyyy-MM-dd HH:mm:ss.fff",
|
|
CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
//
|
|
// Explicit textual categories
|
|
//
|
|
if (normalizedCategory.Contains(
|
|
"STRING"))
|
|
{
|
|
return string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"SIMULATION_{0}",
|
|
simulationIndex + 1);
|
|
}
|
|
|
|
if (normalizedCategory.Contains(
|
|
"ENUMERATED"))
|
|
{
|
|
return string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"SIMULATION_ENUM_{0}",
|
|
simulationIndex + 1);
|
|
}
|
|
|
|
if (normalizedCategory.Contains(
|
|
"ERROR"))
|
|
{
|
|
return string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"SIMULATION_ERROR_{0}",
|
|
simulationIndex + 1);
|
|
}
|
|
|
|
//
|
|
// Flow, humidity, length, mass, number, pressure, pulses,
|
|
// temperature, volume and other physical quantities are represented
|
|
// by a valid invariant numeric value.
|
|
//
|
|
return (900000 +
|
|
simulationIndex + 1)
|
|
.ToString(
|
|
CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private bool IsRepeatDestination(
|
|
string destinationPath)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(
|
|
destinationPath) &&
|
|
destinationPath.StartsWith(
|
|
RepeatPrefix,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private WriterCfg GetDataStorageWriterConfiguration()
|
|
{
|
|
WriterCfg storageCfg =
|
|
dataStorageWriter.Cfg
|
|
as WriterCfg;
|
|
|
|
if (storageCfg == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Parent UniDataStorageWriter configuration is not a WriterCfg.");
|
|
}
|
|
|
|
return storageCfg;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the configured UniDataStorageWriter parent from the complete
|
|
/// runtime component collection.
|
|
/// </summary>
|
|
public void InitializeParent()
|
|
{
|
|
EnsureParentInitialized();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to resolve the configured parent from a supplied component list.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Parent-name comparison is intentionally trimmed and case-insensitive
|
|
/// because component names originate from persisted user configuration.
|
|
/// </remarks>
|
|
private void TryAssignParent(
|
|
IEnumerable<IComponent> components)
|
|
{
|
|
if (components == null ||
|
|
string.IsNullOrWhiteSpace(
|
|
resultsWriterCfg.ParentName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string configuredParentName =
|
|
resultsWriterCfg.ParentName.Trim();
|
|
|
|
IComponent parent =
|
|
components.FirstOrDefault(
|
|
component =>
|
|
component != null &&
|
|
string.Equals(
|
|
(component.Name ??
|
|
string.Empty).Trim(),
|
|
configuredParentName,
|
|
StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (parent == null)
|
|
return;
|
|
|
|
TBF.Rig.Output.DataStorage
|
|
.UniDataStorageWriter.Writer writer =
|
|
parent as
|
|
TBF.Rig.Output.DataStorage
|
|
.UniDataStorageWriter.Writer;
|
|
|
|
if (writer == null)
|
|
{
|
|
throw new Exception(
|
|
string.Format(
|
|
"Parent '{0}' was found, but its runtime type '{1}' is not UniDataStorageWriter.Writer.",
|
|
resultsWriterCfg.ParentName,
|
|
parent.GetType().FullName));
|
|
}
|
|
|
|
dataStorageWriter =
|
|
writer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the parent after all TBF components have had a chance to load.
|
|
/// </summary>
|
|
private void EnsureParentInitialized()
|
|
{
|
|
if (dataStorageWriter != null)
|
|
return;
|
|
|
|
if (string.IsNullOrWhiteSpace(
|
|
resultsWriterCfg.ParentName))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"UniDataStorageWriter parent is not configured.");
|
|
}
|
|
|
|
IComponent parent =
|
|
TbfComponents.FindComponent(
|
|
resultsWriterCfg.ParentName.Trim());
|
|
|
|
if (parent == null)
|
|
{
|
|
throw new Exception(
|
|
string.Format(
|
|
"Parent '{0}' was not found after component loading completed.",
|
|
resultsWriterCfg.ParentName));
|
|
}
|
|
|
|
dataStorageWriter =
|
|
parent as
|
|
TBF.Rig.Output.DataStorage
|
|
.UniDataStorageWriter.Writer;
|
|
|
|
if (dataStorageWriter == null)
|
|
{
|
|
throw new Exception(
|
|
string.Format(
|
|
"Parent '{0}' of runtime type '{1}' is not UniDataStorageWriter.Writer.",
|
|
resultsWriterCfg.ParentName,
|
|
parent.GetType().FullName));
|
|
}
|
|
}
|
|
}
|
|
} |