From a5ecb430773ef29d4a14e6b3dcf7b18d783b615f Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 13 Jun 2023 20:56:56 +0200 Subject: [PATCH] Implement `NotificationService` and `EnergyPriceService` --- .gitignore | 1 + .../Elektrifikatsiya/Pages/Dashboard.razor | 7 +- .../Elektrifikatsiya/Pages/Dashboard.razor.cs | 18 ++-- .../Elektrifikatsiya/Program.cs | 40 +++++---- .../Services/IEnergyPriceService.cs | 13 +++ .../Implementations/EnergyPriceService.cs | 86 +++++++++++++++++++ .../Implementations/NotifcationService.cs | 25 +++--- .../Utilities/PrometheusQuery.cs | 40 ++++----- .../appsettings.Development.json | 19 ---- 9 files changed, 167 insertions(+), 82 deletions(-) create mode 100644 src/Elektrifikatsiya/Elektrifikatsiya/Services/IEnergyPriceService.cs create mode 100644 src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/EnergyPriceService.cs delete mode 100644 src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json diff --git a/.gitignore b/.gitignore index 688c331..85d8065 100644 --- a/.gitignore +++ b/.gitignore @@ -353,3 +353,4 @@ MigrationBackup/ *.sqlite* src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json /src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json~origin_Development +/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor index 6269368..b5149d5 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor @@ -12,6 +12,7 @@ @inject IDeviceStatusService DeviceStatusService; @inject IDeviceManagmentService DeviceManagmentService; @inject IEventService EventService; +@inject IEnergyPriceService EnergyPriceService; @inject MainDatabaseContext MainDatabaseContext;
@@ -107,19 +108,19 @@
Daily Cost
- 2€ + @($"{EnergyPriceService.DailyEnergyCost:F2}")€
Monthly Cost
- 60€ + @($"{EnergyPriceService.MonthlyEnergyCost:F2}")€
Yearly Cost
- 720€ + @($"{EnergyPriceService.YearlyEnergyCost:F2}")€
diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs index f3e4caa..91b8687 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs @@ -1,22 +1,18 @@ using Blazorise.Charts; +using Elektrifikatsiya.Database; using Elektrifikatsiya.Models; using Elektrifikatsiya.Utilities; -using System.Diagnostics; -using System.Text; -using Elektrifikatsiya.Services.Implementations; -using Elektrifikatsiya.Database; - namespace Elektrifikatsiya.Pages; public partial class Dashboard { + private readonly List labels = new List(); + //TODO: insert new event here if plug produces one private List events = new List() { new Event("Text", "Text", DateTime.Today, null) }; - private List labels = new List(); - //code for graph private LineChart lineChart; @@ -36,11 +32,11 @@ public partial class Dashboard plugs = DeviceStatusService.GetDevices().ValueOrDefault ?? new List(); events = EventService.Events; - EventService.OnEventCalled += (_, e) => + EventService.OnEventCalled += (__, e) => { _ = InvokeAsync(StateHasChanged); }; - DeviceStatusService.OnDeviceStatusChanged += (_, e) => + DeviceStatusService.OnDeviceStatusChanged += (__, e) => { _ = InvokeAsync(StateHasChanged); }; @@ -61,14 +57,14 @@ public partial class Dashboard } plugnames = plugnames[0..^1]; - double energyPrice = MainDatabaseContext.EnergyPriceChanges.OrderByDescending(e =>e.DateTime).FirstOrDefault()?.EnergyPrice ?? 0; + double energyPrice = MainDatabaseContext.EnergyPriceChanges.OrderByDescending(e => e.DateTime).FirstOrDefault()?.EnergyPrice ?? 0; PrometheusDataWrapper? deviceData = (await promQueryer.Query($$"""sum(power{sensor=~"{{plugnames}}"})[1h:1m]"""))?.Data; //PrometheusDataWrapper? priceData = (await promQueryer.Query($$"""sum(sum_over_time(power{sensor=~"{{plugnames}}"}[1y])*{{energyPrice}})"""))?.Data; //Console.WriteLine(priceData.VectorTypeToTimestampFloatTuple()); - var r = new Random(DateTime.Now.Millisecond); + Random r = new Random(DateTime.Now.Millisecond); return deviceData?.MatrixTypeToTimestampFloatTuple().ValueOrDefault?.Select(x => { diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs index 1e0c08d..c2a81dc 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs @@ -10,6 +10,7 @@ using Elektrifikatsiya.Models; using Elektrifikatsiya.Services; using Elektrifikatsiya.Services.Implementations; using Elektrifikatsiya.Utilities; + using HiveMQtt.Client; using HiveMQtt.Client.Options; @@ -30,22 +31,24 @@ builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(s => s.GetRequiredService()); builder.Services.AddSingleton(s => s.GetRequiredService()); +builder.Services.AddSingleton(s => s.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton((provider)=> +builder.Services.AddSingleton((provider) => { - HiveMQClientOptions options = new() - { - Host = "localhost", - Port = 1883, - UseTLS = false, - }; + HiveMQClientOptions options = new() + { + Host = "localhost", + Port = 1883, + UseTLS = false, + }; - HiveMQClient client = new(options); - client.ConnectAsync().ConfigureAwait(false); - return client; + HiveMQClient client = new(options); + _ = client.ConnectAsync().ConfigureAwait(false); + return client; }); builder.Services.AddTransient(); builder.Services.AddScoped(); @@ -56,7 +59,7 @@ builder.Services.AddBootstrapProviders(); builder.Services.AddHttpClient(); builder.Services.AddBlazorise(options => { - options.Immediate = true; + options.Immediate = true; }); AddBlazorise(builder.Services); @@ -66,9 +69,9 @@ WebApplication app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - _ = app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - _ = app.UseHsts(); + _ = app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + _ = app.UseHsts(); } app.UseHttpsRedirection(); @@ -80,7 +83,6 @@ app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.MapControllers(); - app.Services.GetRequiredService>().Value.CompileTemplates(); IServiceScope serviceScope = app.Services.GetRequiredService().CreateScope(); @@ -92,14 +94,14 @@ IAuthenticationService authenticationService = serviceScope.ServiceProvider.GetR if (!mainDatabase.Users.Any()) { - _ = authenticationService.RegisterUserAsync("admin", "admin", Role.Admin); + _ = authenticationService.RegisterUserAsync("admin", "admin", Role.Admin); } app.Run(); static void AddBlazorise(IServiceCollection services) { - _ = services.AddBlazorise(); - _ = services.AddMaterialProviders(); - _ = services.AddMaterialIcons(); + _ = services.AddBlazorise(); + _ = services.AddMaterialProviders(); + _ = services.AddMaterialIcons(); } \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/IEnergyPriceService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IEnergyPriceService.cs new file mode 100644 index 0000000..179cce7 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IEnergyPriceService.cs @@ -0,0 +1,13 @@ +using Elektrifikatsiya.Models; +using Elektrifikatsiya.Utilities; + +namespace Elektrifikatsiya.Services; + +public interface IEnergyPriceService : IScheduledService +{ + public double DailyEnergyCost { get; } + public double MonthlyEnergyCost { get; } + public double YearlyEnergyCost { get; } + + public Task GetEnergyPrice(DateTime startime, DateTime endTime, List plugs, List changes); +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/EnergyPriceService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/EnergyPriceService.cs new file mode 100644 index 0000000..e81031c --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/EnergyPriceService.cs @@ -0,0 +1,86 @@ +using Elektrifikatsiya.Database; +using Elektrifikatsiya.Models; +using Elektrifikatsiya.Utilities; + +using System.Globalization; + +namespace Elektrifikatsiya.Services.Implementations; + +public class EnergyPriceService : IEnergyPriceService +{ + private readonly MainDatabaseContext mainDatabaseContext; + private readonly PrometheusQuery prometheusQuerier = new("http://127.0.0.1:9090"); + + public TimeSpan ExecutionRepeatDelay { get; } = TimeSpan.FromSeconds(5); + public DateTime FirstExecutionTime { get; } = DateTime.Now.AddSeconds(5); + public double MonthlyEnergyCost { get; private set; } + + public double YearlyEnergyCost { get; private set; } + + public double DailyEnergyCost { get; private set; } + + public EnergyPriceService(IServiceScopeFactory serviceScopeFactory) + { + mainDatabaseContext = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + } + + /// + /// Calculate the energy price for the given timespan + /// + /// The timestamp that has the greater value + /// The timestamp that has the lower value + /// + /// + /// + /// + public async Task GetEnergyPrice(DateTime startime, DateTime endTime, List plugs, List changes) + { + string plugnames = ""; + foreach (Device device in plugs) + { + plugnames += $"shellyplug-s-{device.MacAddress}/relay/0|"; + } + plugnames = plugnames[0..^1]; + + if (startime < endTime) + { + throw new ArgumentException("Read the xmlDoc"); + } + + changes = changes.OrderByDescending(x => x.DateTime).ToList(); + + double total = (await prometheusQuerier.Query($$"""sum_over_time(sum(power{sensor=~"{{plugnames}}"})[{{(int)Math.Ceiling((startime - changes.First().DateTime).TotalMinutes)}}m:1s] @ {{((DateTimeOffset)startime).ToUnixTimeSeconds()}}) * {{(changes.First().EnergyPrice / 3600).ToString(CultureInfo.InvariantCulture)}}"""))?.Data?.VectorTypeToTimestampFloatTuple().ValueOrDefault.Item2 ?? 0; + + for (int i = 0; i < changes.Count - 1; i++) + { + bool inMinutes = true; + int duration = (int)Math.Ceiling((changes[i].DateTime - changes[i + 1].DateTime).TotalMinutes); + if (duration > 100000) + { + inMinutes = false; + duration = (int)Math.Ceiling(duration / 1440.0); + } + long timestamp = ((DateTimeOffset)changes[i].DateTime).ToUnixTimeSeconds(); + + double price = changes[i + 1].EnergyPrice; + + total += (await prometheusQuerier.Query($$"""sum_over_time(sum(power{sensor=~"{{plugnames}}"})[{{duration}}{{(inMinutes ? "m" : "d")}}:1s] @ {{timestamp}}) * {{(price / 3600).ToString(CultureInfo.InvariantCulture)}}"""))?.Data?.VectorTypeToTimestampFloatTuple().ValueOrDefault.Item2 ?? 0; + if (changes[i].DateTime < endTime) + { + break; + } + } + + return total; + } + + public async void Update() + { + List plugs = mainDatabaseContext.Devices.ToList(); + List energyPriceChanges = mainDatabaseContext.EnergyPriceChanges.ToList(); + + DailyEnergyCost = await GetEnergyPrice(DateTime.Now, DateTime.Now.AddDays(-1), plugs, energyPriceChanges); + MonthlyEnergyCost = await GetEnergyPrice(DateTime.Now, DateTime.Now.AddMonths(-1), plugs, energyPriceChanges); + YearlyEnergyCost = await GetEnergyPrice(DateTime.Now, DateTime.Now.AddYears(-1), plugs, energyPriceChanges); + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/NotifcationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/NotifcationService.cs index 12c6ff0..b8ddd0c 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/NotifcationService.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/NotifcationService.cs @@ -1,24 +1,29 @@ -using Elektrifikatsiya.Utilities; +namespace Elektrifikatsiya.Services.Implementations; -namespace Elektrifikatsiya.Services.Implementations; - -public class NotifcationService : INotifcationService +public class NotifcationService : INotificationService { + private readonly IEmailService emailService; + private readonly IEnergyPriceService energyPriceService; + private readonly Microsoft.Extensions.Configuration.IConfiguration configuration; + private readonly ILogger logger; public TimeSpan ExecutionRepeatDelay => TimeSpan.FromDays(1); public DateTime FirstExecutionTime => DateTime.Now.AddDays(0); - private readonly IEmailService emailService; - private readonly ILogger logger; - public NotifcationService(ILogger logger, IEmailService emailService) + public NotifcationService(ILogger logger, IEmailService emailService, IEnergyPriceService energyPriceService, Microsoft.Extensions.Configuration.IConfiguration configuration) { this.logger = logger; this.emailService = emailService; + this.energyPriceService = energyPriceService; + this.configuration = configuration; } public async void Update() { - //logger.LogInformation("Sending Email"); - //await emailService.SendWithTemeplateAsync("manuel.strubegger@htl-saalfelden.at", "AAAA", "DailyNotification", templateParameters: new() { { "yesterday", "10" }, { "yearly", "100" }, { "link", "localhost:5565/yes" } }); - //logger.LogInformation("Done sending"); + logger.LogInformation("Sending Email"); + + string url = configuration.GetValue("Url") ?? "https://http.cat/images/404.jpg"; + + await emailService.SendWithTemeplateAsync("stone-red@stone-red.net", "Daily Notification", "DailyNotification", templateParameters: new() { { "yesterday", $"{energyPriceService.DailyEnergyCost:F2}" }, { "yearly", $"{energyPriceService.YearlyEnergyCost:F2}" }, { "link", url } }); + logger.LogInformation("Done sending"); } } \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/PrometheusQuery.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/PrometheusQuery.cs index c952d7f..975cd16 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/PrometheusQuery.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/PrometheusQuery.cs @@ -1,31 +1,31 @@ -using System.Net; -using System.Net.Sockets; +using Elektrifikatsiya.Models; + +using System.Diagnostics; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using Elektrifikatsiya.Models; -using FluentResults; namespace Elektrifikatsiya.Utilities; public class PrometheusQuery { - private readonly HttpClient client = new(); + private readonly HttpClient client = new(); - public PrometheusQuery(string connectionString) - { - client.BaseAddress = new Uri(connectionString); - } + public PrometheusQuery(string connectionString) + { + client.BaseAddress = new Uri(connectionString); + } - public Task Query(string query) - { - return client.GetFromJsonAsync($"/api/v1/query?query={UrlEncoder.Create().Encode(query)}", new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = true, - Converters = - { - new JsonStringEnumConverter() - } - }); - } + public Task Query(string query) + { + Debug.WriteLine($"{client.BaseAddress}/api/v1/query?query={query}"); + return client.GetFromJsonAsync($"/api/v1/query?query={UrlEncoder.Create().Encode(query)}", new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter() + } + }); + } } \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json b/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json deleted file mode 100644 index 617c1e4..0000000 --- a/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "DetailedErrors": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "EmailSettings": { - "User": "", - "Key": "", - "DefaultEmail": "SMTP_EMAIL_ADDRESS", - "Templates": { - "DailyNotification": "../assets/DailyNotif.html" - }, - "SmtpServer": "", - "SmtpPort": 0 - } -}