diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/RestValueResultConfiguration.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/RestValueResultConfiguration.cs index 2b3382c9c..1622340f1 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/RestValueResultConfiguration.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/RestValueResultConfiguration.cs @@ -6,7 +6,10 @@ public class RestValueResultConfiguration { public int Id { get; set; } public string? NodePattern { get; set; } - public float CorrectionFactor { get; set; } + public string? XmlAttributeHeaderName { get; set; } + public string? XmlAttributeHeaderValue { get; set; } + public string? XmlAttributeValueName { get; set; } + public decimal CorrectionFactor { get; set; } public ValueUsage UsedFor { get; set; } public ValueOperator Operator { get; set; } diff --git a/TeslaSolarCharger.Services/ServiceCollectionExtensions.cs b/TeslaSolarCharger.Services/ServiceCollectionExtensions.cs index 7a1c4cf9c..c7f05a638 100644 --- a/TeslaSolarCharger.Services/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger.Services/ServiceCollectionExtensions.cs @@ -9,5 +9,6 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddServicesDependencies(this IServiceCollection services) => services .AddTransient() + .AddTransient() ; } diff --git a/TeslaSolarCharger.Services/Services/Contracts/IRestValueExecutionService.cs b/TeslaSolarCharger.Services/Services/Contracts/IRestValueExecutionService.cs new file mode 100644 index 000000000..201b46fc5 --- /dev/null +++ b/TeslaSolarCharger.Services/Services/Contracts/IRestValueExecutionService.cs @@ -0,0 +1,18 @@ +using TeslaSolarCharger.Shared.Dtos.RestValueConfiguration; + +namespace TeslaSolarCharger.Services.Services.Contracts; + +public interface IRestValueExecutionService +{ + /// + /// Get result for each configuration ID + /// + /// Rest Value configuration + /// Headers for REST request + /// Configurations to extract the values + /// Dictionary with with resultConfiguration as key and resulting value as Value + /// Throw if request results in not success status code + Task> GetResult(DtoRestValueConfiguration config, + List headers, + List resultConfigurations); +} diff --git a/TeslaSolarCharger.Services/Services/RestValueExecutionService.cs b/TeslaSolarCharger.Services/Services/RestValueExecutionService.cs new file mode 100644 index 000000000..9ef9e3a1b --- /dev/null +++ b/TeslaSolarCharger.Services/Services/RestValueExecutionService.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Xml; +using TeslaSolarCharger.Services.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos.RestValueConfiguration; +using TeslaSolarCharger.SharedModel.Enums; + + +[assembly: InternalsVisibleTo("TeslaSolarCharger.Tests")] +namespace TeslaSolarCharger.Services.Services; + +public class RestValueExecutionService( + ILogger logger) : IRestValueExecutionService +{ + /// + /// Get result for each configuration ID + /// + /// Rest Value configuration + /// Headers for REST request + /// Configurations to extract the values + /// Dictionary with with resultConfiguration as key and resulting value as Value + /// Throw if request results in not success status code + public async Task> GetResult(DtoRestValueConfiguration config, + List headers, + List resultConfigurations) + { + logger.LogTrace("{method}({@config}, {@headers}, {resultConfigurations})", nameof(GetResult), config, headers, resultConfigurations); + var client = new HttpClient(); + var request = new HttpRequestMessage(new HttpMethod(config.HttpMethod.ToString()), config.Url); + foreach (var header in headers) + { + request.Headers.Add(header.Key, header.Value); + } + var response = await client.SendAsync(request).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var contentString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Requesting JSON Result with url {requestUrl} did result in non success status code: {statusCode} {content}", config.Url, response.StatusCode, contentString); + throw new InvalidOperationException($"Requesting JSON Result with url {config.Url} did result in non success status code: {response.StatusCode} {contentString}"); + } + var results = new Dictionary(); + foreach (var resultConfig in resultConfigurations) + { + var contentString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + results.Add(resultConfig.Id, GetValue(contentString, config.NodePatternType, resultConfig)); + } + return results; + } + + internal decimal GetValue(string responseString, NodePatternType configNodePatternType, DtoRestValueResultConfiguration resultConfig) + { + logger.LogTrace("{method}({responseString}, {configNodePatternType}, {@resultConfig})", nameof(GetValue), responseString, configNodePatternType, resultConfig); + decimal rawValue; + switch (configNodePatternType) + { + case NodePatternType.Direct: + rawValue = decimal.Parse(responseString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + break; + case NodePatternType.Json: + var jsonTokenString = (JObject.Parse(responseString).SelectToken(resultConfig.NodePattern ?? throw new ArgumentNullException(nameof(resultConfig.NodePattern))) ?? + throw new InvalidOperationException("Could not find token by pattern")).Value() ?? "0"; + rawValue = decimal.Parse(jsonTokenString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + break; + case NodePatternType.Xml: + var xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(responseString); + var nodes = xmlDocument.SelectNodes(resultConfig.NodePattern ?? throw new ArgumentNullException(nameof(resultConfig.NodePattern))) ?? throw new InvalidOperationException("Could not find any nodes by pattern"); + var xmlTokenString = string.Empty; + switch (nodes.Count) + { + case < 1: + throw new InvalidOperationException($"Could not find any nodes with pattern {resultConfig.NodePattern}"); + case 1: + xmlTokenString = nodes[0]?.LastChild?.Value ?? "0"; + break; + case > 2: + for (var i = 0; i < nodes.Count; i++) + { + if (nodes[i]?.Attributes?[resultConfig.XmlAttributeHeaderName ?? throw new ArgumentNullException(nameof(resultConfig.XmlAttributeHeaderName))]?.Value == resultConfig.XmlAttributeHeaderValue) + { + xmlTokenString = nodes[i]?.Attributes?[resultConfig.XmlAttributeValueName ?? throw new ArgumentNullException(nameof(resultConfig.XmlAttributeValueName))]?.Value ?? "0"; + break; + } + } + break; + } + rawValue = decimal.Parse(xmlTokenString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + break; + default: + throw new InvalidOperationException($"NodePatternType {configNodePatternType} not supported"); + } + return MakeCalculationsOnRawValue(resultConfig.CorrectionFactor, resultConfig.Operator, rawValue); + } + + internal decimal MakeCalculationsOnRawValue(decimal correctionFactor, ValueOperator valueOperator, decimal rawValue) + { + rawValue = correctionFactor * rawValue; + switch (valueOperator) + { + case ValueOperator.Plus: + return rawValue; + case ValueOperator.Minus: + return -rawValue; + default: + throw new ArgumentOutOfRangeException(); + } + } +} diff --git a/TeslaSolarCharger.Tests/Data/DataGenerator.cs b/TeslaSolarCharger.Tests/Data/DataGenerator.cs index e80a04ae7..f31476074 100644 --- a/TeslaSolarCharger.Tests/Data/DataGenerator.cs +++ b/TeslaSolarCharger.Tests/Data/DataGenerator.cs @@ -1,11 +1,63 @@ -using TeslaSolarCharger.Model.EntityFramework; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Model.EntityFramework; +using TeslaSolarCharger.SharedModel.Enums; namespace TeslaSolarCharger.Tests.Data; public static class DataGenerator { - public static void InitContextData(this TeslaSolarChargerContext ctx) + public static string _httpLocalhostApiValues = "http://localhost:5000/api/values"; + public static NodePatternType _nodePatternType = NodePatternType.Json; + public static HttpVerb _httpMethod = HttpVerb.Get; + public static string _headerKey = "Authorization"; + public static string _headerValue = "Bearer asdf"; + public static string? _nodePattern = "$.data"; + public static decimal _correctionFactor = 1; + public static ValueUsage _valueUsage = ValueUsage.GridPower; + public static ValueOperator _valueOperator = ValueOperator.Plus; + + + public static TeslaSolarChargerContext InitSpotPrices(this TeslaSolarChargerContext context) + { + context.SpotPrices.Add(new SpotPrice() + { + StartDate = new DateTime(2023, 1, 22, 17, 0, 0), + EndDate = new DateTime(2023, 1, 22, 18, 0, 0), Price = new decimal(0.11) + }); + return context; + } + + public static TeslaSolarChargerContext InitRestValueConfigurations(this TeslaSolarChargerContext context) { - ctx.InitSpotPrices(); + context.RestValueConfigurations.Add(new RestValueConfiguration() + { + Url = _httpLocalhostApiValues, + NodePatternType = _nodePatternType, + HttpMethod = _httpMethod, + Headers = new List() + { + new RestValueConfigurationHeader() + { + Key = _headerKey, + Value = _headerValue, + }, + }, + RestValueResultConfigurations = new List() + { + new RestValueResultConfiguration() + { + NodePattern = _nodePattern, + CorrectionFactor = _correctionFactor, + UsedFor = _valueUsage, + Operator = _valueOperator, + }, + }, + }); + return context; } } diff --git a/TeslaSolarCharger.Tests/Data/SpotPriceDataGenerator.cs b/TeslaSolarCharger.Tests/Data/SpotPriceDataGenerator.cs deleted file mode 100644 index b21cf2f4a..000000000 --- a/TeslaSolarCharger.Tests/Data/SpotPriceDataGenerator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; -using TeslaSolarCharger.Model.EntityFramework; - -namespace TeslaSolarCharger.Tests.Data; - -public static class SpotPriceDataGenerator -{ - public static TeslaSolarChargerContext InitSpotPrices(this TeslaSolarChargerContext context) - { - context.SpotPrices.Add(new SpotPrice() - { - StartDate = new DateTime(2023, 1, 22, 17, 0, 0), - EndDate = new DateTime(2023, 1, 22, 18, 0, 0), Price = new decimal(0.11) - }); - return context; - } -} diff --git a/TeslaSolarCharger.Tests/Services/Services/RestValueConfigurationService.cs b/TeslaSolarCharger.Tests/Services/Services/RestValueConfigurationService.cs index d0d5188be..be768d7a3 100644 --- a/TeslaSolarCharger.Tests/Services/Services/RestValueConfigurationService.cs +++ b/TeslaSolarCharger.Tests/Services/Services/RestValueConfigurationService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.SharedModel.Enums; +using TeslaSolarCharger.Tests.Data; using Xunit; using Xunit.Abstractions; #pragma warning disable xUnit2013 @@ -14,34 +15,24 @@ namespace TeslaSolarCharger.Tests.Services.Services; [SuppressMessage("ReSharper", "UseConfigureAwaitFalse")] public class RestValueConfigurationService(ITestOutputHelper outputHelper) : TestBase(outputHelper) { - private string _httpLocalhostApiValues = "http://localhost:5000/api/values"; - private NodePatternType _nodePatternType = NodePatternType.Json; - private HttpVerb _httpMethod = HttpVerb.Get; - private string _headerKey = "Authorization"; - private string _headerValue = "Bearer asdf"; - private string? _nodePattern = "$.data"; - private float _correctionFactor = 1; - private ValueUsage _valueUsage = ValueUsage.GridPower; - private ValueOperator _valueOperator = ValueOperator.Plus; + [Fact] public async Task Can_Get_Rest_Configurations() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); Assert.NotEmpty(restValueConfigurations); Assert.Equal(1, restValueConfigurations.Count); var firstValue = restValueConfigurations.First(); - Assert.Equal(_httpLocalhostApiValues, firstValue.Url); - Assert.Equal(_nodePatternType, firstValue.NodePatternType); - Assert.Equal(_httpMethod, firstValue.HttpMethod); + Assert.Equal(DataGenerator._httpLocalhostApiValues, firstValue.Url); + Assert.Equal(DataGenerator._nodePatternType, firstValue.NodePatternType); + Assert.Equal(DataGenerator._httpMethod, firstValue.HttpMethod); } [Fact] public async Task Can_Update_Rest_Configurations() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); var firstValue = restValueConfigurations.First(); @@ -61,7 +52,6 @@ public async Task Can_Update_Rest_Configurations() [Fact] public async Task Can_Get_Rest_Configuration_Headers() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); var firstValue = restValueConfigurations.First(); @@ -69,14 +59,13 @@ public async Task Can_Get_Rest_Configuration_Headers() Assert.NotEmpty(headers); Assert.Equal(1, headers.Count); var firstHeader = headers.First(); - Assert.Equal(_headerKey, firstHeader.Key); - Assert.Equal(_headerValue, firstHeader.Value); + Assert.Equal(DataGenerator._headerKey, firstHeader.Key); + Assert.Equal(DataGenerator._headerValue, firstHeader.Value); } [Fact] public async Task Can_Update_Rest_Configuration_Headers() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); var firstValue = restValueConfigurations.First(); @@ -95,7 +84,6 @@ public async Task Can_Update_Rest_Configuration_Headers() [Fact] public async Task Can_Get_Rest_Result_Configurations() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); var firstValue = restValueConfigurations.First(); @@ -103,16 +91,15 @@ public async Task Can_Get_Rest_Result_Configurations() Assert.NotEmpty(values); Assert.Equal(1, values.Count); var firstHeader = values.First(); - Assert.Equal(_nodePattern, firstHeader.NodePattern); - Assert.Equal(_correctionFactor, firstHeader.CorrectionFactor); - Assert.Equal(_valueUsage, firstHeader.UsedFor); - Assert.Equal(_valueOperator, firstHeader.Operator); + Assert.Equal(DataGenerator._nodePattern, firstHeader.NodePattern); + Assert.Equal(DataGenerator._correctionFactor, firstHeader.CorrectionFactor); + Assert.Equal(DataGenerator._valueUsage, firstHeader.UsedFor); + Assert.Equal(DataGenerator._valueOperator, firstHeader.Operator); } [Fact] public async Task Can_Update_Rest_Result_Configurations() { - await GenerateDemoData(); var service = Mock.Create(); var restValueConfigurations = await service.GetAllRestValueConfigurations(); var firstValue = restValueConfigurations.First(); @@ -133,35 +120,4 @@ public async Task Can_Update_Rest_Result_Configurations() var id = await service.SaveResultConfiguration(firstValue.Id, firstHeader); Assert.Equal(firstHeader.Id, id); } - - private async Task GenerateDemoData() - { - Context.RestValueConfigurations.Add(new RestValueConfiguration() - { - Url = _httpLocalhostApiValues, - NodePatternType = _nodePatternType, - HttpMethod = _httpMethod, - Headers = new List() - { - new RestValueConfigurationHeader() - { - Key = _headerKey, - Value = _headerValue, - }, - }, - RestValueResultConfigurations = new List() - { - new RestValueResultConfiguration() - { - NodePattern = _nodePattern, - CorrectionFactor = _correctionFactor, - UsedFor = _valueUsage, - Operator = _valueOperator, - }, - }, - }); - await Context.SaveChangesAsync(); - Context.ChangeTracker.Entries().Where(e => e.State != EntityState.Detached).ToList() - .ForEach(entry => entry.State = EntityState.Detached); - } } diff --git a/TeslaSolarCharger.Tests/Services/Services/RestValueExecutionService.cs b/TeslaSolarCharger.Tests/Services/Services/RestValueExecutionService.cs new file mode 100644 index 000000000..b67733bc1 --- /dev/null +++ b/TeslaSolarCharger.Tests/Services/Services/RestValueExecutionService.cs @@ -0,0 +1,46 @@ +using TeslaSolarCharger.Shared.Dtos.RestValueConfiguration; +using TeslaSolarCharger.SharedModel.Enums; +using Xunit; +using Xunit.Abstractions; + +namespace TeslaSolarCharger.Tests.Services.Services; + +public class RestValueExecutionService(ITestOutputHelper outputHelper) : TestBase(outputHelper) +{ + [Fact] + public void Can_Extract_Json_Value() + { + var service = Mock.Create(); + var json = "{\r\n \"request\": {\r\n \"method\": \"get\",\r\n \"key\": \"asdf\"\r\n },\r\n \"code\": 0,\r\n \"type\": \"call\",\r\n \"data\": {\r\n \"value\": 14\r\n }\r\n}"; + var value = service.GetValue(json, NodePatternType.Json, new DtoRestValueResultConfiguration + { + Id = 1, + NodePattern = "$.data.value", + }); + Assert.Equal(14, value); + } + + [Fact] + public void Can_Extract_Xml_Value() + { + var service = Mock.Create(); + var xml = "\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n"; + var value = service.GetValue(xml, NodePatternType.Xml, new DtoRestValueResultConfiguration + { + Id = 1, + NodePattern = "Device/Measurements/Measurement", + XmlAttributeHeaderName = "Type", + XmlAttributeHeaderValue = "GridPower", + XmlAttributeValueName = "Value", + }); + Assert.Equal(18.7m, value); + } + + [Fact] + public void CanCalculateCorrectionFactor() + { + var service = Mock.Create(); + var value = service.MakeCalculationsOnRawValue(10, ValueOperator.Minus, 14); + Assert.Equal(-140, value); + } +} diff --git a/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj b/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj index ad969335b..758d3df3d 100644 --- a/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj +++ b/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj @@ -33,7 +33,6 @@ - diff --git a/TeslaSolarCharger.Tests/TestBase.cs b/TeslaSolarCharger.Tests/TestBase.cs index 299f1fe69..b92e3d87d 100644 --- a/TeslaSolarCharger.Tests/TestBase.cs +++ b/TeslaSolarCharger.Tests/TestBase.cs @@ -11,6 +11,7 @@ using Serilog; using Serilog.Core; using Serilog.Events; +using System.Linq; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.EntityFramework; using TeslaSolarCharger.Shared.Contracts; @@ -112,8 +113,10 @@ protected TestBase( _ctx = _fake.Provide(new TeslaSolarChargerContext(options)); _ctx.Database.EnsureCreated(); - //_ctx.InitContextData(); + _ctx.InitRestValueConfigurations(); _ctx.SaveChanges(); + _ctx.ChangeTracker.Entries().Where(e => e.State != EntityState.Detached).ToList() + .ForEach(entry => entry.State = EntityState.Detached); } private static (ILoggerFactory, LoggingLevelSwitch) GetOrCreateLoggerFactory( diff --git a/TeslaSolarCharger/Shared/Dtos/RestValueConfiguration/DtoRestValueResultConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/RestValueConfiguration/DtoRestValueResultConfiguration.cs index c166db360..bb8fcab45 100644 --- a/TeslaSolarCharger/Shared/Dtos/RestValueConfiguration/DtoRestValueResultConfiguration.cs +++ b/TeslaSolarCharger/Shared/Dtos/RestValueConfiguration/DtoRestValueResultConfiguration.cs @@ -6,7 +6,10 @@ public class DtoRestValueResultConfiguration { public int Id { get; set; } public string? NodePattern { get; set; } - public float CorrectionFactor { get; set; } + public string? XmlAttributeHeaderName { get; set; } + public string? XmlAttributeHeaderValue { get; set; } + public string? XmlAttributeValueName { get; set; } + public decimal CorrectionFactor { get; set; } = 1; public ValueUsage UsedFor { get; set; } public ValueOperator Operator { get; set; } }