diff --git a/src/PVOutput.Net/Builders/BatchNetStatusPostBuilder.cs b/src/PVOutput.Net/Builders/BatchNetStatusPostBuilder.cs new file mode 100644 index 0000000..8cf7c60 --- /dev/null +++ b/src/PVOutput.Net/Builders/BatchNetStatusPostBuilder.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dawn; +using PVOutput.Net.Objects; +using PVOutput.Net.Objects.Core; +using PVOutput.Net.Objects.Modules; + +namespace PVOutput.Net.Builders +{ + /// + /// Builder that creates batch outputs to post to PVOutput. + /// + public sealed class BatchNetStatusPostBuilder + { + internal BatchNetStatusPost _statusPost { get; set; } + + /// + /// Creates a new builder. + /// + public BatchNetStatusPostBuilder() + { + _statusPost = new BatchNetStatusPost(); + } + + /// + /// Sets the timestamp for the status. + /// + /// Timestamp. + /// The builder. + public BatchNetStatusPostBuilder SetTimeStamp(DateTime timestamp) + { + Guard.Argument(timestamp, nameof(timestamp)).IsNoFutureDate(); + + _statusPost.Timestamp = timestamp; + return this; + } + + /// + /// Sets the net power exported for the status. + /// + /// Net power exported consumed. + /// The builder. + public BatchNetStatusPostBuilder SetPowerExported(int powerExported) + { + Guard.Argument(powerExported, nameof(powerExported)).Min(0); + + _statusPost.PowerExported = powerExported; + return this; + } + + /// + /// Sets the net power imported for the status. + /// + /// Net power imported consumed. + /// The builder. + public BatchNetStatusPostBuilder SetPowerImported(int powerImported) + { + Guard.Argument(powerImported, nameof(powerImported)).Min(0); + + _statusPost.PowerImported = powerImported; + return this; + } + + /// + /// Resets the builder to it's default state. Ready to build a new status. + /// + public void Reset() => _statusPost = new BatchNetStatusPost(); + + /// + /// Uses information within the builder to return the built status. + /// + /// The status. + public IBatchNetStatusPost Build() + { + ValidateStatus(); + return _statusPost; + } + + /// + /// Uses information within the builder to return the built status. + /// Resets the builder to it's default state after building. + /// + /// The status. + public IBatchNetStatusPost BuildAndReset() + { + IBatchNetStatusPost result = Build(); + Reset(); + return result; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Exception messages are non translatable for now")] + private void ValidateStatus() + { + if (_statusPost.PowerExported == null + && _statusPost.PowerImported == null) + { + throw new InvalidOperationException("Status has no power values"); + } + } + } +} diff --git a/src/PVOutput.Net/Builders/StatusPostBuilder.cs b/src/PVOutput.Net/Builders/StatusPostBuilder.cs index 780ffa6..e6b0e9b 100644 --- a/src/PVOutput.Net/Builders/StatusPostBuilder.cs +++ b/src/PVOutput.Net/Builders/StatusPostBuilder.cs @@ -36,8 +36,7 @@ public StatusPostBuilder SetTimeStamp(DateTime timestamp) Guard.Argument(timestamp, nameof(timestamp)).IsNoFutureDate(); _statusPost.Timestamp = timestamp; - return this; - + return this; } /// diff --git a/src/PVOutput.Net/Modules/StatusService.cs b/src/PVOutput.Net/Modules/StatusService.cs index f947994..8953e22 100644 --- a/src/PVOutput.Net/Modules/StatusService.cs +++ b/src/PVOutput.Net/Modules/StatusService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Dawn; using PVOutput.Net.Builders; +using PVOutput.Net.Enums; using PVOutput.Net.Objects; using PVOutput.Net.Objects.Core; using PVOutput.Net.Requests.Handler; @@ -141,6 +142,50 @@ public Task> AddBatchStatusAsync(I return handler.ExecuteArrayRequestAsync(new AddBatchStatusRequest() { StatusPosts = statuses }, loggingScope, cancellationToken); } + /// + /// Adds multiple statuses to the owned system. + /// See the official API information. + /// Use the to create objects. + /// + /// The statuses to add. + /// Sets whether or not the provided data is cumulative. + /// A cancellation token for the request. + /// If the operation succeeded. + public Task> AddBatchStatusAsync(IEnumerable statuses, bool isCumulative, CancellationToken cancellationToken = default) + { + var loggingScope = new Dictionary() + { + [LoggingEvents.RequestId] = LoggingEvents.StatusService_AddBatchStatus, + [LoggingEvents.Parameter_CumulativeType] = isCumulative + }; + + Guard.Argument(statuses, nameof(statuses)).NotNull().NotEmpty(); + + var handler = new RequestHandler(Client); + return handler.ExecuteArrayRequestAsync(new AddBatchStatusRequest() { StatusPosts = statuses, Cumulative = isCumulative }, loggingScope, cancellationToken); + } + + /// + /// Adds multiple statuses to the owned system. + /// See the official API information. + /// Use the to create objects. + /// + /// The statuses to add. + /// A cancellation token for the request. + /// If the operation succeeded. + public Task> AddBatchNetStatusAsync(IEnumerable statuses, CancellationToken cancellationToken = default) + { + var loggingScope = new Dictionary() + { + [LoggingEvents.RequestId] = LoggingEvents.StatusService_AddNetBatchStatus + }; + + Guard.Argument(statuses, nameof(statuses)).NotNull().NotEmpty(); + + var handler = new RequestHandler(Client); + return handler.ExecuteArrayRequestAsync(new AddBatchNetStatusRequest() { StatusPosts = statuses }, loggingScope, cancellationToken); + } + /// /// Deletes a status on the specified moment. /// See the official API information. diff --git a/src/PVOutput.Net/Objects/Core/LoggingEvents.cs b/src/PVOutput.Net/Objects/Core/LoggingEvents.cs index e22f271..2f2e2ce 100644 --- a/src/PVOutput.Net/Objects/Core/LoggingEvents.cs +++ b/src/PVOutput.Net/Objects/Core/LoggingEvents.cs @@ -42,6 +42,7 @@ internal class LoggingEvents public const string Parameter_ApplicationId = "ApplicationId"; public const string Parameter_CallBackUrl = "CallBackUrl"; public const string Parameter_AlertType = "AlertType"; + public const string Parameter_CumulativeType = "CumulativeType"; /* * RequestHandler base events @@ -88,6 +89,7 @@ internal class LoggingEvents public static readonly EventId StatusService_AddStatus = new EventId(20804, "AddStatus"); public static readonly EventId StatusService_AddBatchStatus = new EventId(20805, "AddBatchStatus"); public static readonly EventId StatusService_DeleteStatus = new EventId(20806, "DeleteStatus"); + public static readonly EventId StatusService_AddNetBatchStatus = new EventId(20807, "AddNetBatchStatus"); public static readonly EventId SupplyService_GetSupply = new EventId(20901, "GetSupply"); public static readonly EventId SystemService_GetOwnSystem = new EventId(21001, "GetOwnSystem"); public static readonly EventId SystemService_GetOtherSystem = new EventId(21002, "GetOtherSystem"); diff --git a/src/PVOutput.Net/Objects/IBatchNetStatusPost.cs b/src/PVOutput.Net/Objects/IBatchNetStatusPost.cs new file mode 100644 index 0000000..8fb5de6 --- /dev/null +++ b/src/PVOutput.Net/Objects/IBatchNetStatusPost.cs @@ -0,0 +1,25 @@ +using System; + +namespace PVOutput.Net.Objects +{ + /// + /// A single net batch status used for posting multiple statuses. + /// + public interface IBatchNetStatusPost + { + /// + /// Timestamp for the recorded status. + /// + DateTime Timestamp { get; set; } + + /// + /// Total energy generated up to and including the timestamp. + /// + int? PowerExported { get; set; } + + /// + /// Actual power being generated at the moment of the timestamp. + /// + int? PowerImported { get; set; } + } +} diff --git a/src/PVOutput.Net/Objects/Modules/BatchNetStatusPost.cs b/src/PVOutput.Net/Objects/Modules/BatchNetStatusPost.cs new file mode 100644 index 0000000..161c88e --- /dev/null +++ b/src/PVOutput.Net/Objects/Modules/BatchNetStatusPost.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PVOutput.Net.Objects.Modules +{ + internal class BatchNetStatusPost : IBatchNetStatusPost + { + public DateTime Timestamp { get; set; } + public int? PowerExported { get; set; } + public int? PowerImported { get; set; } + } +} diff --git a/src/PVOutput.Net/Requests/Modules/AddBatchNetStatusRequest.cs b/src/PVOutput.Net/Requests/Modules/AddBatchNetStatusRequest.cs new file mode 100644 index 0000000..5ad7465 --- /dev/null +++ b/src/PVOutput.Net/Requests/Modules/AddBatchNetStatusRequest.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using PVOutput.Net.Objects.Core; +using PVOutput.Net.Objects; +using PVOutput.Net.Requests.Base; + +namespace PVOutput.Net.Requests.Modules +{ + internal class AddBatchNetStatusRequest : PostRequest + { + public IEnumerable StatusPosts { get; set; } + + public override HttpMethod Method => HttpMethod.Post; + + public override string UriTemplate => "addbatchstatus.jsp{?n,data}"; + + public override IDictionary GetUriPathParameters() => new Dictionary + { + ["n"] = 1, + ["data"] = FormatStatusPosts() + }; + + private string FormatStatusPosts() + { + var sb = new StringBuilder(); + + foreach (IBatchNetStatusPost status in StatusPosts) + { + sb.Append(FormatStatusPost(status)).Append(';'); + } + + return sb.ToString(); + } + + internal static string FormatStatusPost(IBatchNetStatusPost status) + { + var sb = new StringBuilder(); + sb.Append(FormatHelper.GetDateAsString(status.Timestamp)); + sb.Append(','); + sb.Append(FormatHelper.GetTimeAsString(status.Timestamp)); + sb.Append(",-1,"); // Skip single field as per documentation + sb.Append(status.PowerExported); + sb.Append(",-1,"); // Skip single field as per documentation + sb.Append(status.PowerImported); + return sb.ToString(); + } + } +} diff --git a/src/PVOutput.Net/Requests/Modules/AddBatchStatusRequest.cs b/src/PVOutput.Net/Requests/Modules/AddBatchStatusRequest.cs index 1e219e3..8f85531 100644 --- a/src/PVOutput.Net/Requests/Modules/AddBatchStatusRequest.cs +++ b/src/PVOutput.Net/Requests/Modules/AddBatchStatusRequest.cs @@ -13,9 +13,7 @@ internal class AddBatchStatusRequest : PostRequest { public IEnumerable StatusPosts { get; set; } - public bool Net { get; set; } - - public CumulativeStatusType Cumulative { get; set; } + public bool Cumulative { get; set; } public override HttpMethod Method => HttpMethod.Post; @@ -23,8 +21,8 @@ internal class AddBatchStatusRequest : PostRequest public override IDictionary GetUriPathParameters() => new Dictionary { - ["c1"] = Cumulative != CumulativeStatusType.None ? (int?)Cumulative : null, - ["n"] = Net ? 1 : 0, + ["c1"] = Cumulative ? 1 : 0, + ["n"] = 0, ["data"] = FormatStatusPosts() }; diff --git a/tests/PVOutput.Net.Tests/Modules/Status/AddBatchNetStatusRequestTests.cs b/tests/PVOutput.Net.Tests/Modules/Status/AddBatchNetStatusRequestTests.cs new file mode 100644 index 0000000..cb775ed --- /dev/null +++ b/tests/PVOutput.Net.Tests/Modules/Status/AddBatchNetStatusRequestTests.cs @@ -0,0 +1,46 @@ +using System; +using NUnit.Framework; +using PVOutput.Net.Requests.Modules; +using PVOutput.Net.Objects.Modules.Implementations; +using System.Threading.Tasks; +using PVOutput.Net.Objects; +using PVOutput.Net.Tests.Utils; +using System.Collections.Generic; +using System.Linq; +using RichardSzalay.MockHttp; +using PVOutput.Net.Builders; +using PVOutput.Net.Objects.Modules; + +namespace PVOutput.Net.Tests.Modules.Status +{ + public class AddBatchNetStatusRequestTests : BaseRequestsTest + { + private string[] GetSplitStatusPostLine(BatchNetStatusPost post) => AddBatchNetStatusRequest.FormatStatusPost(post).Split(','); + + [Test] + public void Parameter_Timestamp_CreatesCorrectUriParameters() + { + var post = new BatchNetStatusPost() { Timestamp = new DateTime(2020, 2, 1, 13, 12, 20) }; + + string[] postArray = GetSplitStatusPostLine(post); + Assert.That(postArray[0], Is.EqualTo("20200201")); + Assert.That(postArray[1], Is.EqualTo("13:12")); + } + + [Test] + public void Parameter_PowerExported_CreatesCorrectUriParameters() + { + var post = new BatchNetStatusPost() { PowerExported = 1111 }; + string[] postArray = GetSplitStatusPostLine(post); + Assert.That(postArray[3], Is.EqualTo("1111")); + } + + [Test] + public void Parameter_PowerImported_CreatesCorrectUriParameters() + { + var post = new BatchNetStatusPost() { PowerImported = 2222 }; + string[] postArray = GetSplitStatusPostLine(post); + Assert.That(postArray[5], Is.EqualTo("2222")); + } + } +} diff --git a/tests/PVOutput.Net.Tests/Modules/Status/BatchNetStatusBuilderTests.cs b/tests/PVOutput.Net.Tests/Modules/Status/BatchNetStatusBuilderTests.cs new file mode 100644 index 0000000..638ad53 --- /dev/null +++ b/tests/PVOutput.Net.Tests/Modules/Status/BatchNetStatusBuilderTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using PVOutput.Net.Builders; +using PVOutput.Net.Enums; +using PVOutput.Net.Objects; +using PVOutput.Net.Objects.Factories; +using PVOutput.Net.Objects.Modules; +using PVOutput.Net.Requests.Modules; +using PVOutput.Net.Tests.Utils; + +namespace PVOutput.Net.Tests.Modules.Status +{ + [TestFixture] + public class BatchNetStatusBuilderTests + { + [Test] + public void BatchNetStatusBuilder_WithTimeStamp_SetsTimeStamp() + { + var timeStamp = DateTime.Now; + var builder = new StatusPostBuilder().SetTimeStamp(timeStamp); + + Assert.That(builder._statusPost.Timestamp, Is.EqualTo(timeStamp)); + } + + [Test] + public void BatchNetStatusBuilder_WithFutureTimeStamp_Throws() + { + var timeStamp = DateTime.Now.AddDays(1); + var builder = new StatusPostBuilder(); + + Assert.Throws(() => + { + builder.SetTimeStamp(timeStamp); + }); + } + + [Test] + [TestCase(100)] + [TestCase(0)] + public void BatchNetStatusBuilder_WithPowerExported_SetsPowerExported(int powerExported) + { + var builder = new BatchNetStatusPostBuilder().SetPowerExported(powerExported); + + Assert.That(builder._statusPost.PowerExported, Is.EqualTo(powerExported)); + } + + [Test] + [TestCase(100)] + [TestCase(0)] + public void BatchNetStatusBuilder_WithPowerImported_SetsPowerImported(int powerImported) + { + var builder = new BatchNetStatusPostBuilder().SetPowerImported(powerImported); + + Assert.That(builder._statusPost.PowerImported, Is.EqualTo(powerImported)); + } + + [Test] + public void BatchNetStatusBuilder_AfterReset_HasNoStateLeft() + { + var builder = new BatchNetStatusPostBuilder().SetPowerExported(1000).SetTimeStamp(DateTime.Now); + IBatchNetStatusPost status = builder.Build(); + + builder.Reset(); + + Assert.That(builder._statusPost, Is.Not.SameAs(status)); + } + + + [Test] + public void BatchNetStatusBuilder_AfterBuildAndReset_HasNoStateLeft() + { + var builder = new BatchNetStatusPostBuilder().SetPowerImported(1000).SetTimeStamp(DateTime.Now); + IBatchNetStatusPost status = builder.BuildAndReset(); + + Assert.That(builder._statusPost, Is.Not.SameAs(status)); + } + + [Test] + public void BatchNetStatusBuilder_WithoutPowerOrConsumption_CannotBuild() + { + var builder = new BatchNetStatusPostBuilder().SetTimeStamp(DateTime.Now); + + Assert.Throws(() => + { + builder.Build(); + }); + } + } +} diff --git a/tests/PVOutput.Net.Tests/Modules/Status/StatusServiceTests.cs b/tests/PVOutput.Net.Tests/Modules/Status/StatusServiceTests.cs index 3655462..cd32f08 100644 --- a/tests/PVOutput.Net.Tests/Modules/Status/StatusServiceTests.cs +++ b/tests/PVOutput.Net.Tests/Modules/Status/StatusServiceTests.cs @@ -356,6 +356,58 @@ public async Task StatusService_AddBatchStatus_CallsCorrectUri() testProvider.VerifyNoOutstandingExpectation(); } + [Test] + public async Task StatusService_AddBatchCumulativeStatus_CallsCorrectUri() + { + var batchStatus = new StatusPostBuilder().SetTimeStamp(new DateTime(2020, 1, 1, 12, 22, 0)) + .SetGeneration(11000).SetConsumption(9000).Build(); + + PVOutputClient client = TestUtility.GetMockClient(out MockHttpMessageHandler testProvider); + testProvider.ExpectUriFromBase(ADDBATCHSTATUS_URL) + .WithQueryString("n=0&c1=1&data=20200101,12:22,11000,,9000,,,,,,,,,;") + .RespondPlainText(""); + + await client.Status.AddBatchStatusAsync(new[] { batchStatus }, true); + testProvider.VerifyNoOutstandingExpectation(); + } + + [Test] + public void StatusService_AddBatchNetStatus_WithNullStatuses_Throws() + { + PVOutputClient client = TestUtility.GetMockClient(out MockHttpMessageHandler testProvider); + + Assert.ThrowsAsync(async () => + { + _ = await client.Status.AddBatchNetStatusAsync(null); + }); + } + + [Test] + public void StatusService_AddBatchNetStatus_WithEmptyStatuses_Throws() + { + PVOutputClient client = TestUtility.GetMockClient(out MockHttpMessageHandler testProvider); + + Assert.ThrowsAsync(async () => + { + _ = await client.Status.AddBatchNetStatusAsync(new List()); + }); + } + + [Test] + public async Task StatusService_AddBatchNetStatus_CallsCorrectUri() + { + var batchStatus = new BatchNetStatusPostBuilder().SetTimeStamp(new DateTime(2020, 1, 1, 12, 22, 0)) + .SetPowerExported(11000).SetPowerImported(9000).Build(); + + PVOutputClient client = TestUtility.GetMockClient(out MockHttpMessageHandler testProvider); + testProvider.ExpectUriFromBase(ADDBATCHSTATUS_URL) + .WithQueryString("n=1&data=20200101,12:22,-1,11000,-1,9000;") + .RespondPlainText(""); + + await client.Status.AddBatchNetStatusAsync(new[] { batchStatus }); + testProvider.VerifyNoOutstandingExpectation(); + } + /* * Deserialisation tests below */