Skip to content

Commit

Permalink
Merge pull request #100 from DougSchmidt-AI/feature/Issue-99-PointZil…
Browse files Browse the repository at this point in the history
…laSavePointsAsCsv

Issue-99 Add -SaveCsvPath option to allow saving of the extracted/gen…
  • Loading branch information
Doug Schmidt authored Nov 24, 2018
2 parents 1de159a + fea1276 commit cf8523e
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public static Guid GetTimeSeriesUniqueId(this IAquariusClient client, string ide
if (Guid.TryParse(identifier, out var uniqueId))
return uniqueId;

var timeSeriesDescription = client.GetTimeSeriesDescription(identifier);

return timeSeriesDescription.UniqueId;
}

public static TimeSeriesDescription GetTimeSeriesDescription(this IAquariusClient client, string identifier)
{
var location = TimeSeriesIdentifierParser.ParseLocationIdentifier(identifier);

var response = client.Publish.Get(new TimeSeriesDescriptionServiceRequest { LocationIdentifier = location });
Expand All @@ -22,7 +29,7 @@ public static Guid GetTimeSeriesUniqueId(this IAquariusClient client, string ide
if (timeSeriesDescription == null)
throw new ExpectedException($"Can't find '{identifier}' at location '{location}'");

return timeSeriesDescription.UniqueId;
return timeSeriesDescription;
}

public static TimeSeries GetTimeSeriesInfo(this IAquariusClient client, string identifier)
Expand Down
5 changes: 5 additions & 0 deletions TimeSeries/PublicApis/SdkExamples/PointZilla/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace PointZilla
{
public class Context
{
public string ExecutingFileVersion { get; set; }

public string Server { get; set; }
public string Username { get; set; } = "admin";
public string Password { get; set; } = "admin";
Expand All @@ -35,10 +37,13 @@ public class Context
public string ComputationPeriodIdentifier { get; set; }
public string SubLocationIdentifier { get; set; }
public List<ExtendedAttributeValue> ExtendedAttributeValues { get; set; } = new List<ExtendedAttributeValue>();
public TimeSeriesType? TimeSeriesType { get; set; }

public TimeSeriesIdentifier SourceTimeSeries { get; set; }
public Instant? SourceQueryFrom { get; set; }
public Instant? SourceQueryTo { get; set; }
public string SaveCsvPath { get; set; }
public bool StopAfterSavingCsv { get; set; }

public List<ReflectedTimeSeriesPoint> ManualPoints { get; set; } = new List<ReflectedTimeSeriesPoint>();

Expand Down
4 changes: 2 additions & 2 deletions TimeSeries/PublicApis/SdkExamples/PointZilla/CsvReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private ReflectedTimeSeriesPoint ParsePoint(string[] fields)
if (int.TryParse(text, out var grade))
gradeCode = grade;
});
ParseField(fields, Context.CsvQualifiersField, text => qualifiers = text.Split(QualifierDelimeters, StringSplitOptions.RemoveEmptyEntries).ToList());
ParseField(fields, Context.CsvQualifiersField, text => qualifiers = text.Split(QualifierDelimiters, StringSplitOptions.RemoveEmptyEntries).ToList());

if (time == null)
return null;
Expand All @@ -175,7 +175,7 @@ private ReflectedTimeSeriesPoint ParsePoint(string[] fields)
};
}

private static readonly char[] QualifierDelimeters = {','};
private static readonly char[] QualifierDelimiters = {','};

private static void ParseField(string[] fields, int fieldIndex, Action<string> parseAction)
{
Expand Down
161 changes: 161 additions & 0 deletions TimeSeries/PublicApis/SdkExamples/PointZilla/CsvWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Aquarius.TimeSeries.Client.ServiceModels.Acquisition;
using NodaTime;
using NodaTime.Text;
using ServiceStack.Logging;

namespace PointZilla
{
public class CsvWriter
{
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

private Context Context { get; }

public CsvWriter(Context context)
{
Context = context;
}

public void WritePoints(List<ReflectedTimeSeriesPoint> points)
{
var timeSeriesIdentifier = CreateTimeSeriesIdentifier();

var csvPath = Directory.Exists(Context.SaveCsvPath)
? Path.Combine(Context.SaveCsvPath, SanitizeFilename($"{timeSeriesIdentifier.Identifier}.{CreatePeriod(Context.SourceQueryFrom, Context.SourceQueryTo)}.csv"))
: Context.SaveCsvPath;

Log.Info($"Saving {points.Count} extracted points to '{csvPath}' ...");

var dir = Path.GetDirectoryName(csvPath);

if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}

using (var writer = new StreamWriter(csvPath))
{
var offsetPattern = OffsetPattern.CreateWithInvariantCulture("m");
var utcOffsetText = $"UTC{offsetPattern.Format(Context.UtcOffset ?? Offset.Zero)}";
var period = CreatePeriod(Context.SourceQueryFrom ?? Instant.MinValue, Context.SourceQueryTo ?? Instant.MaxValue);

writer.WriteLine($"# {Path.GetFileName(csvPath)} generated by {Context.ExecutingFileVersion}");
writer.WriteLine($"#");
writer.WriteLine($"# Time series identifier: {timeSeriesIdentifier.Identifier}");
writer.WriteLine($"# Location: {timeSeriesIdentifier.LocationIdentifier}");
writer.WriteLine($"# UTC offset: ({utcOffsetText})");
writer.WriteLine($"# Value units: {Context.Unit}");
writer.WriteLine($"# Value parameter: {timeSeriesIdentifier.Parameter}");
writer.WriteLine($"# Interpolation type: {Context.InterpolationType}");
writer.WriteLine($"# Time series type: {Context.TimeSeriesType}");
writer.WriteLine($"#");
writer.WriteLine($"# Export options: Corrected signal from {period.StartText} to {period.EndText}");
writer.WriteLine($"#");
writer.WriteLine($"# CSV data starts at line 15.");
writer.WriteLine($"#");
writer.WriteLine($"ISO 8601 UTC, Value, Grade, Qualifiers");

foreach (var point in points)
{
var time = point.Time ?? Instant.MinValue;

writer.WriteLine($"{InstantPattern.ExtendedIsoPattern.Format(time)}, {point.Value:G12}, {point.GradeCode}, {FormatQualifiers(point.Qualifiers)}");
}
}
}

public static void SetPointZillaCsvFormat(Context context)
{
// Match PointZilla Export format above

// # CSV data starts at line 15.
// #
// ISO 8601 UTC, Value, Grade, Qualifiers
// 2015-12-04T00:01:00Z, 3.523200823975, 500,
// 2015-12-04T00:02:00Z, 3.525279357147, 500,

context.CsvTimeField = 1;
context.CsvValueField = 2;
context.CsvGradeField = 3;
context.CsvQualifiersField = 4;
context.CsvComment = "#";
context.CsvSkipRows = 0;
context.CsvTimeFormat = null;
context.CsvIgnoreInvalidRows = true;
context.CsvRealign = false;
}

private TimeSeriesIdentifier CreateTimeSeriesIdentifier()
{
if (Context.SourceTimeSeries != null)
return Context.SourceTimeSeries;

if (!string.IsNullOrEmpty(Context.TimeSeries))
return TimeSeriesIdentifierParser.ParseExtendedIdentifier(Context.TimeSeries);

string parameter;
var label = "Points";
var locationIdentifier = "PointZilla";

if (Context.Command == CommandType.DeleteAllPoints)
parameter ="Deleted";

else if (Context.ManualPoints.Any())
parameter = "ManuallyEntered";
else if (Context.CsvFiles.Any())
parameter = "OtherCsvFile";
else
parameter = Context.WaveformType.ToString();

return new TimeSeriesIdentifier
{
Parameter = parameter,
Label = label,
LocationIdentifier = locationIdentifier,
Identifier = $"{parameter}.{label}@{locationIdentifier}"
};
}

private static string CreatePeriod(Instant? startTime, Instant? endTime)
{
var start = startTime ?? Instant.MinValue;
var end = endTime ?? Instant.MaxValue;

if (start == Instant.MinValue && end == Instant.MaxValue)
return "EntireRecord";

var period = CreatePeriod(start, end);

return $"{period.StartText}.{period.EndText}";
}

private static (string StartText, string EndText) CreatePeriod(Instant start, Instant end)
{
return (
start == Instant.MinValue ? "StartOfRecord" : InstantPattern.ExtendedIsoPattern.Format(start),
end == Instant.MaxValue ? "EndOfRecord" : InstantPattern.ExtendedIsoPattern.Format(end)
);

}

private static string FormatQualifiers(List<string> qualifiers)
{
if (!qualifiers.Any())
return string.Empty;

if (qualifiers.Count == 1)
return qualifiers.First();

return $"\"{string.Join(",", qualifiers)}\"";
}

private static string SanitizeFilename(string s)
{
return Path.GetInvalidFileNameChars().Aggregate(s, (current, ch) => current.Replace(ch, '_'));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
using Aquarius.TimeSeries.Client;
using Aquarius.TimeSeries.Client.Helpers;
using Aquarius.TimeSeries.Client.ServiceModels.Acquisition;
using Aquarius.TimeSeries.Client.ServiceModels.Legacy.Publish3x;
using Aquarius.TimeSeries.Client.ServiceModels.Provisioning;
using Aquarius.TimeSeries.Client.ServiceModels.Publish;
using Get3xCorrectedData = Aquarius.TimeSeries.Client.ServiceModels.Legacy.Publish3x.TimeSeriesDataCorrectedServiceRequest;
using Get3xTimeSeriesDescription = Aquarius.TimeSeries.Client.ServiceModels.Legacy.Publish3x.TimeSeriesDescriptionServiceRequest;
using NodaTime;
using ServiceStack.Logging;
using InterpolationType = Aquarius.TimeSeries.Client.ServiceModels.Provisioning.InterpolationType;
using TimeRange = Aquarius.TimeSeries.Client.ServiceModels.Publish.TimeRange;
using TimeSeriesDataCorrectedServiceRequest = Aquarius.TimeSeries.Client.ServiceModels.Publish.TimeSeriesDataCorrectedServiceRequest;

namespace PointZilla
{
Expand Down Expand Up @@ -48,6 +50,8 @@ private List<ReflectedTimeSeriesPoint> LoadPointsFromNg(IAquariusClient client)
{
var timeSeriesInfo = client.GetTimeSeriesInfo(Context.SourceTimeSeries.Identifier);

Log.Info($"Loading points from '{timeSeriesInfo.Identifier}' ...");

var timeSeriesData = client.Publish.Get(new TimeSeriesDataCorrectedServiceRequest
{
TimeSeriesUniqueId = timeSeriesInfo.UniqueId,
Expand All @@ -74,7 +78,6 @@ private List<ReflectedTimeSeriesPoint> LoadPointsFromNg(IAquariusClient client)

SetTimeSeriesCreationProperties(
timeSeriesInfo,
timeSeriesInfo.UtcOffset,
timeSeriesData.Methods.LastOrDefault()?.MethodCode,
gapTolerance,
interpolationType);
Expand Down Expand Up @@ -102,7 +105,6 @@ private static IEnumerable<T> GetManyMetadata<TMetadata, T>(IEnumerable<TMetadat

private void SetTimeSeriesCreationProperties(
TimeSeries timeSeries,
Offset? utcOffset = null,
string method = null,
Duration? gapTolerance = null,
InterpolationType? interpolationType = null)
Expand All @@ -113,18 +115,17 @@ private void SetTimeSeriesCreationProperties(
if (interpolationType.HasValue && !Context.InterpolationType.HasValue)
Context.InterpolationType = interpolationType;

if (utcOffset.HasValue && !Context.UtcOffset.HasValue)
Context.UtcOffset = utcOffset.Value;

Context.Publish = timeSeries.Publish;
Context.Description = timeSeries.Description;

Context.UtcOffset = Context.UtcOffset ?? timeSeries.UtcOffset;
Context.Method = Context.Method ?? method;
Context.Unit = Context.Unit ?? timeSeries.Unit;
Context.Comment = Context.Comment ?? timeSeries.Comment;
Context.ComputationIdentifier = Context.ComputationIdentifier ?? timeSeries.ComputationIdentifier;
Context.ComputationPeriodIdentifier = Context.ComputationPeriodIdentifier ?? timeSeries.ComputationPeriodIdentifier;
Context.SubLocationIdentifier = Context.SubLocationIdentifier ?? timeSeries.SubLocationIdentifier;
Context.TimeSeriesType = Context.TimeSeriesType ?? timeSeries.TimeSeriesType;

foreach (var extendedAttributeValue in timeSeries.ExtendedAttributeValues)
{
Expand All @@ -135,6 +136,7 @@ private void SetTimeSeriesCreationProperties(
}
}


private List<ReflectedTimeSeriesPoint> LoadPointsFrom3X(IAquariusClient client)
{
var timeSeriesDescription = client.Publish.Get(new Get3xTimeSeriesDescription
Expand All @@ -148,12 +150,16 @@ private List<ReflectedTimeSeriesPoint> LoadPointsFrom3X(IAquariusClient client)
if (timeSeriesDescription == null)
throw new ExpectedException($"Can't find '{Context.SourceTimeSeries.Identifier}' time-series in location '{Context.SourceTimeSeries.LocationIdentifier}'.");

var points = client.Publish.Get(new Get3xCorrectedData
{
TimeSeriesIdentifier = Context.SourceTimeSeries.Identifier,
QueryFrom = Context.SourceQueryFrom?.ToDateTimeOffset(),
QueryTo = Context.SourceQueryTo?.ToDateTimeOffset()
})
Log.Info($"Loading points from '{timeSeriesDescription.Identifier}' ...");

var correctedData = client.Publish.Get(new Get3xCorrectedData
{
TimeSeriesIdentifier = Context.SourceTimeSeries.Identifier,
QueryFrom = Context.SourceQueryFrom?.ToDateTimeOffset(),
QueryTo = Context.SourceQueryTo?.ToDateTimeOffset()
});

var points = correctedData
.Points
.Select(p => new ReflectedTimeSeriesPoint
{
Expand All @@ -163,14 +169,27 @@ private List<ReflectedTimeSeriesPoint> LoadPointsFrom3X(IAquariusClient client)
})
.ToList();

SetTimeSeriesCreationProperties(new TimeSeries
// 3.X Publish API's TimeSeriesDescription is missing some info, so grab those pieces from elsewhere

// The time-range start will always be in the offset of the time-series, even when no points exist
var utcOffset = Offset.FromHoursAndMinutes(correctedData.TimeRange.StartTime.Offset.Hours, correctedData.TimeRange.StartTime.Offset.Minutes);

// We can infer the interpolationType from the last point (if one exists)
var interpolationType = Context.InterpolationType ?? (correctedData.Points.Any()
? (InterpolationType?)correctedData.Points.Last().Interpolation
: null);

var timeSeries = new TimeSeries
{
Identifier = Context.SourceTimeSeries.Identifier,
Parameter = timeSeriesDescription.Parameter,
Label = timeSeriesDescription.Label,
Unit = timeSeriesDescription.Unit,
Publish = timeSeriesDescription.Publish,
Description = timeSeriesDescription.Description,
Comment = timeSeriesDescription.Comment,
TimeSeriesType = KnownTimeSeriesTypes[timeSeriesDescription.TimeSeriesType],
UtcOffset = utcOffset,
ComputationIdentifier = timeSeriesDescription.ComputationIdentifier,
ComputationPeriodIdentifier = timeSeriesDescription.ComputationPeriodIdentifier,
SubLocationIdentifier = timeSeriesDescription.SubLocationIdentifier,
Expand All @@ -182,11 +201,27 @@ private List<ReflectedTimeSeriesPoint> LoadPointsFrom3X(IAquariusClient client)
Value = ea.Value.ToString()
})
.ToList()
});
};

SetTimeSeriesCreationProperties(timeSeries, interpolationType: interpolationType);

Log.Info($"Loaded {points.Count} points from {Context.SourceTimeSeries.Identifier}");

return points;
}

private static readonly Dictionary<AtomType, TimeSeriesType> KnownTimeSeriesTypes =
new Dictionary<AtomType, TimeSeriesType>
{
{ AtomType.TimeSeries_Basic, TimeSeriesType.ProcessorBasic},
{ AtomType.TimeSeries_Field_Visit, TimeSeriesType.External},
{ AtomType.TimeSeries_Composite, TimeSeriesType.ProcessorDerived},
{ AtomType.TimeSeries_Rating_Curve_Derived, TimeSeriesType.ProcessorDerived},
{ AtomType.TimeSeries_Calculated_Derived, TimeSeriesType.ProcessorDerived},
{ AtomType.TimeSeries_External, TimeSeriesType.External},
{ AtomType.TimeSeries_Statistical_Derived, TimeSeriesType.ProcessorDerived},
{ AtomType.TimeSeries_ProcessorBasic, TimeSeriesType.ProcessorBasic},
{ AtomType.TimeSeries_ProcessorDerived, TimeSeriesType.ProcessorDerived},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<Compile Include="Context.cs" />
<Compile Include="CreateMode.cs" />
<Compile Include="CsvReader.cs" />
<Compile Include="CsvWriter.cs" />
<Compile Include="ExpectedException.cs" />
<Compile Include="ExternalPointsReader.cs" />
<Compile Include="TimeSeriesCreator.cs" />
Expand Down
Loading

0 comments on commit cf8523e

Please sign in to comment.