Implement NotificationService and EnergyPriceService

This commit is contained in:
Stone_Red
2023-06-13 21:02:50 +02:00
parent 5b8915d7b4
commit a5ecb43077
9 changed files with 167 additions and 82 deletions
+1
View File
@@ -353,3 +353,4 @@ MigrationBackup/
*.sqlite* *.sqlite*
src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json
/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json~origin_Development /src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json~origin_Development
/src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json
@@ -12,6 +12,7 @@
@inject IDeviceStatusService DeviceStatusService; @inject IDeviceStatusService DeviceStatusService;
@inject IDeviceManagmentService DeviceManagmentService; @inject IDeviceManagmentService DeviceManagmentService;
@inject IEventService EventService; @inject IEventService EventService;
@inject IEnergyPriceService EnergyPriceService;
@inject MainDatabaseContext MainDatabaseContext; @inject MainDatabaseContext MainDatabaseContext;
<Div> <Div>
@@ -107,19 +108,19 @@
<Div Flex="Flex.JustifyContent.Between" Width="Width.Is100"> <Div Flex="Flex.JustifyContent.Between" Width="Width.Is100">
<Heading Size="HeadingSize.Is6">Daily Cost</Heading> <Heading Size="HeadingSize.Is6">Daily Cost</Heading>
</Div> </Div>
<Paragraph Margin="Margin.Is1.FromBottom">2€</Paragraph> <Paragraph Margin="Margin.Is1.FromBottom">@($"{EnergyPriceService.DailyEnergyCost:F2}")€</Paragraph>
</ListGroupItem> </ListGroupItem>
<ListGroupItem> <ListGroupItem>
<Div Flex="Flex.JustifyContent.Between" Width="Width.Is100"> <Div Flex="Flex.JustifyContent.Between" Width="Width.Is100">
<Heading Size="HeadingSize.Is6">Monthly Cost</Heading> <Heading Size="HeadingSize.Is6">Monthly Cost</Heading>
</Div> </Div>
<Paragraph Margin="Margin.Is1.FromBottom">60€</Paragraph> <Paragraph Margin="Margin.Is1.FromBottom">@($"{EnergyPriceService.MonthlyEnergyCost:F2}")€</Paragraph>
</ListGroupItem> </ListGroupItem>
<ListGroupItem> <ListGroupItem>
<Div Flex="Flex.JustifyContent.Between" Width="Width.Is100"> <Div Flex="Flex.JustifyContent.Between" Width="Width.Is100">
<Heading Size="HeadingSize.Is6">Yearly Cost</Heading> <Heading Size="HeadingSize.Is6">Yearly Cost</Heading>
</Div> </Div>
<Paragraph Margin="Margin.Is1.FromBottom">720€</Paragraph> <Paragraph Margin="Margin.Is1.FromBottom">@($"{EnergyPriceService.YearlyEnergyCost:F2}")€</Paragraph>
</ListGroupItem> </ListGroupItem>
</ListGroup> </ListGroup>
@@ -1,22 +1,18 @@
using Blazorise.Charts; using Blazorise.Charts;
using Elektrifikatsiya.Database;
using Elektrifikatsiya.Models; using Elektrifikatsiya.Models;
using Elektrifikatsiya.Utilities; using Elektrifikatsiya.Utilities;
using System.Diagnostics;
using System.Text;
using Elektrifikatsiya.Services.Implementations;
using Elektrifikatsiya.Database;
namespace Elektrifikatsiya.Pages; namespace Elektrifikatsiya.Pages;
public partial class Dashboard public partial class Dashboard
{ {
private readonly List<string> labels = new List<string>();
//TODO: insert new event here if plug produces one //TODO: insert new event here if plug produces one
private List<Event> events = new List<Event>() { new Event("Text", "Text", DateTime.Today, null) }; private List<Event> events = new List<Event>() { new Event("Text", "Text", DateTime.Today, null) };
private List<string> labels = new List<string>();
//code for graph //code for graph
private LineChart<double> lineChart; private LineChart<double> lineChart;
@@ -36,11 +32,11 @@ public partial class Dashboard
plugs = DeviceStatusService.GetDevices().ValueOrDefault ?? new List<Device>(); plugs = DeviceStatusService.GetDevices().ValueOrDefault ?? new List<Device>();
events = EventService.Events; events = EventService.Events;
EventService.OnEventCalled += (_, e) => EventService.OnEventCalled += (__, e) =>
{ {
_ = InvokeAsync(StateHasChanged); _ = InvokeAsync(StateHasChanged);
}; };
DeviceStatusService.OnDeviceStatusChanged += (_, e) => DeviceStatusService.OnDeviceStatusChanged += (__, e) =>
{ {
_ = InvokeAsync(StateHasChanged); _ = InvokeAsync(StateHasChanged);
}; };
@@ -61,14 +57,14 @@ public partial class Dashboard
} }
plugnames = plugnames[0..^1]; 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? 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; //PrometheusDataWrapper? priceData = (await promQueryer.Query($$"""sum(sum_over_time(power{sensor=~"{{plugnames}}"}[1y])*{{energyPrice}})"""))?.Data;
//Console.WriteLine(priceData.VectorTypeToTimestampFloatTuple()); //Console.WriteLine(priceData.VectorTypeToTimestampFloatTuple());
var r = new Random(DateTime.Now.Millisecond); Random r = new Random(DateTime.Now.Millisecond);
return deviceData?.MatrixTypeToTimestampFloatTuple().ValueOrDefault?.Select(x => return deviceData?.MatrixTypeToTimestampFloatTuple().ValueOrDefault?.Select(x =>
{ {
@@ -10,6 +10,7 @@ using Elektrifikatsiya.Models;
using Elektrifikatsiya.Services; using Elektrifikatsiya.Services;
using Elektrifikatsiya.Services.Implementations; using Elektrifikatsiya.Services.Implementations;
using Elektrifikatsiya.Utilities; using Elektrifikatsiya.Utilities;
using HiveMQtt.Client; using HiveMQtt.Client;
using HiveMQtt.Client.Options; using HiveMQtt.Client.Options;
@@ -30,22 +31,24 @@ builder.Services.AddHostedService<ServiceScheduler>();
builder.Services.AddSingleton<IUpdateService, UpdateService>(); builder.Services.AddSingleton<IUpdateService, UpdateService>();
builder.Services.AddSingleton<IDeviceStatusService, DeviceStatusService>(); builder.Services.AddSingleton<IDeviceStatusService, DeviceStatusService>();
builder.Services.AddSingleton<INotificationService, NotifcationService>(); builder.Services.AddSingleton<INotificationService, NotifcationService>();
builder.Services.AddSingleton<IEnergyPriceService, EnergyPriceService>();
builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<IUpdateService>()); builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<IUpdateService>());
builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<INotificationService>()); builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<INotificationService>());
builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<IEnergyPriceService>());
builder.Services.AddSingleton<IEmailService, EmailService>(); builder.Services.AddSingleton<IEmailService, EmailService>();
builder.Services.AddSingleton<IEventService, EventService>(); builder.Services.AddSingleton<IEventService, EventService>();
builder.Services.AddSingleton<IHiveMQClient, HiveMQClient>((provider)=> builder.Services.AddSingleton<IHiveMQClient, HiveMQClient>((provider) =>
{ {
HiveMQClientOptions options = new() HiveMQClientOptions options = new()
{ {
Host = "localhost", Host = "localhost",
Port = 1883, Port = 1883,
UseTLS = false, UseTLS = false,
}; };
HiveMQClient client = new(options); HiveMQClient client = new(options);
client.ConnectAsync().ConfigureAwait(false); _ = client.ConnectAsync().ConfigureAwait(false);
return client; return client;
}); });
builder.Services.AddTransient<IDeviceManagmentService, DeviceManagmentService>(); builder.Services.AddTransient<IDeviceManagmentService, DeviceManagmentService>();
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>(); builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
@@ -56,7 +59,7 @@ builder.Services.AddBootstrapProviders();
builder.Services.AddHttpClient<IDeviceManagmentService, DeviceManagmentService>(); builder.Services.AddHttpClient<IDeviceManagmentService, DeviceManagmentService>();
builder.Services.AddBlazorise(options => builder.Services.AddBlazorise(options =>
{ {
options.Immediate = true; options.Immediate = true;
}); });
AddBlazorise(builder.Services); AddBlazorise(builder.Services);
@@ -66,9 +69,9 @@ WebApplication app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
_ = app.UseExceptionHandler("/Error"); _ = 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. // 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.UseHsts();
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();
@@ -80,7 +83,6 @@ app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); app.MapFallbackToPage("/_Host");
app.MapControllers(); app.MapControllers();
app.Services.GetRequiredService<IOptions<EmailSettings>>().Value.CompileTemplates(); app.Services.GetRequiredService<IOptions<EmailSettings>>().Value.CompileTemplates();
IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope(); IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
@@ -92,14 +94,14 @@ IAuthenticationService authenticationService = serviceScope.ServiceProvider.GetR
if (!mainDatabase.Users.Any()) if (!mainDatabase.Users.Any())
{ {
_ = authenticationService.RegisterUserAsync("admin", "admin", Role.Admin); _ = authenticationService.RegisterUserAsync("admin", "admin", Role.Admin);
} }
app.Run(); app.Run();
static void AddBlazorise(IServiceCollection services) static void AddBlazorise(IServiceCollection services)
{ {
_ = services.AddBlazorise(); _ = services.AddBlazorise();
_ = services.AddMaterialProviders(); _ = services.AddMaterialProviders();
_ = services.AddMaterialIcons(); _ = services.AddMaterialIcons();
} }
@@ -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<double> GetEnergyPrice(DateTime startime, DateTime endTime, List<Device> plugs, List<EnergyPriceChange> changes);
}
@@ -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<MainDatabaseContext>();
}
/// <summary>
/// Calculate the energy price for the given timespan
/// </summary>
/// <param name="startime">The timestamp that has the greater value</param>
/// <param name="endTime">The timestamp that has the lower value</param>
/// <param name="plugs"></param>
/// <param name="changes"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<double> GetEnergyPrice(DateTime startime, DateTime endTime, List<Device> plugs, List<EnergyPriceChange> 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<Device> plugs = mainDatabaseContext.Devices.ToList();
List<EnergyPriceChange> 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);
}
}
@@ -1,24 +1,29 @@
using Elektrifikatsiya.Utilities; namespace Elektrifikatsiya.Services.Implementations;
namespace Elektrifikatsiya.Services.Implementations; public class NotifcationService : INotificationService
public class NotifcationService : INotifcationService
{ {
private readonly IEmailService emailService;
private readonly IEnergyPriceService energyPriceService;
private readonly Microsoft.Extensions.Configuration.IConfiguration configuration;
private readonly ILogger<NotifcationService> logger;
public TimeSpan ExecutionRepeatDelay => TimeSpan.FromDays(1); public TimeSpan ExecutionRepeatDelay => TimeSpan.FromDays(1);
public DateTime FirstExecutionTime => DateTime.Now.AddDays(0); public DateTime FirstExecutionTime => DateTime.Now.AddDays(0);
private readonly IEmailService emailService;
private readonly ILogger<NotifcationService> logger;
public NotifcationService(ILogger<NotifcationService> logger, IEmailService emailService) public NotifcationService(ILogger<NotifcationService> logger, IEmailService emailService, IEnergyPriceService energyPriceService, Microsoft.Extensions.Configuration.IConfiguration configuration)
{ {
this.logger = logger; this.logger = logger;
this.emailService = emailService; this.emailService = emailService;
this.energyPriceService = energyPriceService;
this.configuration = configuration;
} }
public async void Update() public async void Update()
{ {
//logger.LogInformation("Sending Email"); logger.LogInformation("Sending Email");
//await emailService.SendWithTemeplateAsync("[email protected]", "AAAA", "DailyNotification", templateParameters: new() { { "yesterday", "10" }, { "yearly", "100" }, { "link", "localhost:5565/yes" } });
//logger.LogInformation("Done sending"); string url = configuration.GetValue<string>("Url") ?? "https://http.cat/images/404.jpg";
await emailService.SendWithTemeplateAsync("[email protected]", "Daily Notification", "DailyNotification", templateParameters: new() { { "yesterday", $"{energyPriceService.DailyEnergyCost:F2}" }, { "yearly", $"{energyPriceService.YearlyEnergyCost:F2}" }, { "link", url } });
logger.LogInformation("Done sending");
} }
} }
@@ -1,31 +1,31 @@
using System.Net; using Elektrifikatsiya.Models;
using System.Net.Sockets;
using System.Diagnostics;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Elektrifikatsiya.Models;
using FluentResults;
namespace Elektrifikatsiya.Utilities; namespace Elektrifikatsiya.Utilities;
public class PrometheusQuery public class PrometheusQuery
{ {
private readonly HttpClient client = new(); private readonly HttpClient client = new();
public PrometheusQuery(string connectionString) public PrometheusQuery(string connectionString)
{ {
client.BaseAddress = new Uri(connectionString); client.BaseAddress = new Uri(connectionString);
} }
public Task<PrometheusQueryResult?> Query(string query) public Task<PrometheusQueryResult?> Query(string query)
{ {
return client.GetFromJsonAsync<PrometheusQueryResult>($"/api/v1/query?query={UrlEncoder.Create().Encode(query)}", new JsonSerializerOptions() Debug.WriteLine($"{client.BaseAddress}/api/v1/query?query={query}");
{ return client.GetFromJsonAsync<PrometheusQueryResult>($"/api/v1/query?query={UrlEncoder.Create().Encode(query)}", new JsonSerializerOptions()
PropertyNameCaseInsensitive = true, {
Converters = PropertyNameCaseInsensitive = true,
{ Converters =
new JsonStringEnumConverter() {
} new JsonStringEnumConverter()
}); }
} });
}
} }
@@ -1,19 +0,0 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"EmailSettings": {
"User": "<SMTP_USER>",
"Key": "<SMTP_KEY>",
"DefaultEmail": "SMTP_EMAIL_ADDRESS",
"Templates": {
"DailyNotification": "../assets/DailyNotif.html"
},
"SmtpServer": "<SMTP_SERVER>",
"SmtpPort": 0
}
}