Squashed commit of the following:

commit a0adfc850d
Author: Stefan <[email protected]>
Date:   Thu Jun 1 15:28:39 2023 +0200

    Revert using Prom.Net

commit 08a5bd4772
Merge: a47c252 9d64698
Author: Stefan <[email protected]>
Date:   Thu Jun 1 15:27:48 2023 +0200

    Merge remote-tracking branch 'origin/feature-DeviceRegistration' into feature-DeviceRegistration

commit a47c2525e1
Author: Stefan <[email protected]>
Date:   Thu Jun 1 15:27:30 2023 +0200

    Revert using Prom.Net

commit 9d646988d1
Merge: 889fc5e 4ef3958
Author: Friend2868 <[email protected]>
Date:   Thu Jun 1 15:24:56 2023 +0200

    Merge branch 'feature-DeviceRegistration' of https://github.com/Stefan-5422/-T5-Elektrifikatsiya into feature-DeviceRegistration

commit 889fc5e3dd
Author: Friend2868 <[email protected]>
Date:   Thu Jun 1 15:24:21 2023 +0200

    Final2

commit 92a7538996
Merge: d5f5ec2 f04724e
Author: Friend2868 <[email protected]>
Date:   Thu Jun 1 15:23:59 2023 +0200

    Merge branch 'feature-DeviceRegistration' of https://github.com/Stefan-5422/-T5-Elektrifikatsiya into feature-DeviceRegistration

commit 4ef3958da3
Author: Stone_Red <[email protected]>
Date:   Thu Jun 1 15:23:10 2023 +0200

    Fix .gitignore

commit d5f5ec2d0e
Author: Friend2868 <[email protected]>
Date:   Thu Jun 1 15:21:37 2023 +0200

    Final

commit 236c2418c5
Author: Friend2868 <[email protected]>
Date:   Thu Jun 1 15:18:57 2023 +0200

    Update .gitignore

commit f04724e258
Merge: 9b2db9c f74cff3
Author: Stefan <[email protected]>
Date:   Thu Jun 1 15:15:57 2023 +0200

    Merge remote-tracking branch 'origin/feature-DeviceRegistration' into feature-DeviceRegistration

commit 9b2db9c97d
Author: Stefan <[email protected]>
Date:   Thu Jun 1 15:15:44 2023 +0200

    Changes 🐙

commit f74cff3d39
Author: Stone_Red <[email protected]>
Date:   Wed May 24 11:48:07 2023 +0200

    Update appsettings.Development.json

commit 2f79ccb22d
Author: Stone_Red <[email protected]>
Date:   Wed May 24 11:45:16 2023 +0200

    Update appsettings.Development.json

commit 104bd37bcb
Author: Stone_Red <[email protected]>
Date:   Wed May 24 11:44:58 2023 +0200

    Update appsettings.Development.json

commit bdac3581de
Author: Stefan <[email protected]>
Date:   Wed May 24 11:29:38 2023 +0200

    Revert test shid

commit 5b58318699
Merge: 5b342c1 b054b9c
Author: Stefan <[email protected]>
Date:   Wed May 24 11:28:20 2023 +0200

    Merge remote-tracking branch 'origin/feature-DeviceRegistration' into feature-DeviceRegistration

commit 5b342c1a8f
Author: Stefan <[email protected]>
Date:   Wed May 24 11:28:12 2023 +0200

    Add `ScheduledService` and email things ༼ つ ◕_◕ ༽つ

commit b054b9cfe3
Author: Friend2868 <[email protected]>
Date:   Wed May 24 11:16:18 2023 +0200

    EventService finish

commit 9520fa04b8
Author: Stefan <[email protected]>
Date:   Wed May 10 12:13:31 2023 +0200

    Email Inferstructure
This commit is contained in:
Stefan
2023-06-01 15:33:18 +02:00
parent 0364016766
commit 1028cbfb40
34 changed files with 529 additions and 166 deletions
+1
View File
@@ -351,3 +351,4 @@ MigrationBackup/
# Project specific # Project specific
*.sqlite* *.sqlite*
src/Elektrifikatsiya/Elektrifikatsiya/appsettings.Development.json
@@ -6,11 +6,13 @@ namespace Elektrifikatsiya.Database;
public class MainDatabaseContext : DbContext public class MainDatabaseContext : DbContext
{ {
public MainDatabaseContext(DbContextOptions<MainDatabaseContext> dbContextOptions) : base(dbContextOptions) public DbSet<Device> Devices { get; set; }
{
}
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public DbSet<Device> Devices { get; set; }
public DbSet<Event> Events { get; set; } public DbSet<EnergyPriceChange> EnergyPriceChanges { get; set; }
public MainDatabaseContext(DbContextOptions<MainDatabaseContext> dbContextOptions) : base(dbContextOptions)
{
}
} }
@@ -42,7 +42,9 @@
<PackageReference Include="Blazorise.Icons.Material" Version="1.2.0" /> <PackageReference Include="Blazorise.Icons.Material" Version="1.2.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FluentResults" Version="3.15.2" /> <PackageReference Include="FluentResults" Version="3.15.2" />
<PackageReference Include="Handlebars.Net" Version="2.1.4" />
<PackageReference Include="HiveMQtt" Version="0.1.10" /> <PackageReference Include="HiveMQtt" Version="0.1.10" />
<PackageReference Include="MailKit" Version="4.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
@@ -50,6 +52,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Mjml.Net" Version="1.22.0" />
<PackageReference Include="Tmds.MDns" Version="0.7.1" /> <PackageReference Include="Tmds.MDns" Version="0.7.1" />
</ItemGroup> </ItemGroup>
@@ -1,10 +1,10 @@
namespace Elektrifikatsiya.Models; namespace Elektrifikatsiya.Models;
public class DeviceStatusChagedEventArgs : EventArgs public class DeviceStatusChangedEventArgs : EventArgs
{ {
public string MacAddress { get; set; } public string MacAddress { get; set; }
public DeviceStatusChagedEventArgs(string macAddress) public DeviceStatusChangedEventArgs(string macAddress)
{ {
MacAddress = macAddress; MacAddress = macAddress;
} }
@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace Elektrifikatsiya.Models;
public class EnergyPriceChange
{
[Key]
public DateTime DateTime { get; set; }
public double EnergyPrice { get; set; }
public EnergyPriceChange(DateTime dateTime, double energyPrice)
{
DateTime = dateTime;
EnergyPrice = energyPrice;
}
public EnergyPriceChange(double energyPrice)
{
EnergyPrice = energyPrice;
DateTime = DateTime.Now;
}
}
@@ -11,14 +11,15 @@ namespace Elektrifikatsiya.Models
public string EventName { get; set; } public string EventName { get; set; }
public string Description { get; set; } public string Description { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public Device Plug { get; set; }
//if Plug Class implemented -> Property for which plug this event is from public Event() { }
public Event(string eventName, string description, DateTime date, Device plug)
public Event(string eventName, string description, DateTime date)
{ {
EventName = eventName; EventName = eventName;
Description = description; Description = description;
Date = date; Date = date;
Plug = plug;
} }
} }
} }
@@ -0,0 +1,11 @@
namespace Elektrifikatsiya.Models
{
public class EventServiceEventArgs : EventArgs
{
public Event NewEvent { get; set; } = new Event();
public EventServiceEventArgs(Event newEvent)
{
NewEvent = newEvent;
}
}
}
@@ -5,29 +5,30 @@ namespace Elektrifikatsiya.Models;
public class User public class User
{ {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Required]
[Key] public List<Device> Devices { get; set; } = new();
public int Id { get; private set; }
[Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Name { get; private set; } [Key]
public int Id { get; private set; }
[Required] public DateTime LastLoginDate { get; set; }
public string PasswordHash { get; private set; }
[Required] [Required]
public Role Role { get; set; } public string Name { get; private set; }
public string? SessionToken { get; set; } [Required]
public DateTime LastLoginDate { get; set; } public string PasswordHash { get; private set; }
[Required] [Required]
public List<Device> Devices { get; set; } = new(); public Role Role { get; set; }
public User(string name, string passwordHash, Role role) public string? SessionToken { get; set; }
{
Name = name; public User(string name, string passwordHash, Role role)
PasswordHash = passwordHash; {
Role = role; Name = name;
} PasswordHash = passwordHash;
Role = role;
}
} }
@@ -1,5 +1,6 @@
@page "/" @page "/"
@using Blazorise.Components; @using Blazorise.Components;
@using Elektrifikatsiya.Database;
@using Elektrifikatsiya.Models; @using Elektrifikatsiya.Models;
@using System.Net @using System.Net
@using Elektrifikatsiya.Services @using Elektrifikatsiya.Services
@@ -10,6 +11,8 @@
@inject IDeviceStatusService DeviceStatusService; @inject IDeviceStatusService DeviceStatusService;
@inject IDeviceManagmentService DeviceManagmentService; @inject IDeviceManagmentService DeviceManagmentService;
@inject IEventService EventService;
@inject MainDatabaseContext MainDatabaseContext;
<Div> <Div>
<Row Padding="Padding.Is3"> <Row Padding="Padding.Is3">
@@ -5,29 +5,23 @@ using Elektrifikatsiya.Utilities;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using Elektrifikatsiya.Services.Implementations;
using Elektrifikatsiya.Database;
namespace Elektrifikatsiya.Pages; namespace Elektrifikatsiya.Pages;
public partial class Dashboard public partial class Dashboard
{ {
//TODO: insert new event here if plug produces one //TODO: insert new event here if plug produces one
private readonly List<Event> events = new List<Event>() { new Event("Placeholder", "Placeholder", DateTime.Now), new Event("Placeholder", "Placeholder", DateTime.Now), new Event("Placeholder", "Placeholder", DateTime.Now) }; private List<Event> events = new List<Event>() { new Event("Text", "Text", DateTime.Today, null) };
//TODO: insert new Device here if user adds one private List<string> labels = new List<string>();
private List<Device> plugs = new List<Device>();
//code for graph //code for graph
private LineChart<double> lineChart; private LineChart<double> lineChart;
protected override void OnInitialized() //TODO: insert new Device here if user adds one
{ private List<Device> plugs = new List<Device>();
plugs = DeviceStatusService.GetDevices().ValueOrDefault ?? new List<Device>();
DeviceStatusService.OnDeviceStatusChanged += (_, e) =>
{
_ = InvokeAsync(StateHasChanged);
};
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@@ -37,10 +31,63 @@ public partial class Dashboard
} }
} }
private async Task Switch(Device device) protected override void OnInitialized()
{ {
device.Enabled = !device.Enabled; plugs = DeviceStatusService.GetDevices().ValueOrDefault ?? new List<Device>();
_ = await DeviceManagmentService.UpdateDevice(device); events = EventService.Events;
EventService.OnEventCalled += (_, e) =>
{
_ = InvokeAsync(StateHasChanged);
};
DeviceStatusService.OnDeviceStatusChanged += (_, e) =>
{
_ = InvokeAsync(StateHasChanged);
};
}
private async Task<List<double>> GetData()
{
if (plugs.Count == 0)
{
return new();
}
PrometheusQuery promQueryer = new PrometheusQuery("http://localhost:9090");
string plugnames = "";
foreach (Device device in plugs)
{
plugnames += $"shellyplug-s-{device.MacAddress}/relay/0|";
}
plugnames = plugnames[0..^1];
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);
return deviceData?.MatrixTypeToTimestampFloatTuple().ValueOrDefault?.Select(x =>
{
labels.Add(DateTimeOffset.FromUnixTimeSeconds(long.Parse($"1{x.Item1}0")).UtcDateTime.ToLocalTime().ToShortTimeString());
return x.Item2;
}).ToList() ?? new List<double>();
}
//TODO: insert dataset of current and last voltage usages
private async Task<LineChartDataset<double>> GetLineChartDataset()
{
return new LineChartDataset<double>
{
Label = "Wattage",
Data = await GetData(),
Fill = true,
PointRadius = 3,
CubicInterpolationMode = "monotone",
};
} }
private async Task HandleRedraw() private async Task HandleRedraw()
@@ -50,48 +97,9 @@ public partial class Dashboard
await lineChart.AddLabelsDatasetsAndUpdate(labels, await GetLineChartDataset()); await lineChart.AddLabelsDatasetsAndUpdate(labels, await GetLineChartDataset());
} }
//TODO: insert dataset of current and last voltage usages private async Task Switch(Device device)
private async Task<LineChartDataset<double>> GetLineChartDataset()
{ {
return new LineChartDataset<double> device.Enabled = !device.Enabled;
{ _ = await DeviceManagmentService.UpdateDevice(device);
Label = "Wattage",
Data = await DeviceData(),
Fill = true,
PointRadius = 3,
CubicInterpolationMode = "monotone",
};
}
private readonly List<string> labels = new List<string>();
private async Task<List<double>> DeviceData()
{
PrometheusQuery promQueryer = new PrometheusQuery("http://localhost:9090");
StringBuilder builder = new();
foreach (Device device in plugs)
{
_ = builder.Append($"shellyplug-s-{device.MacAddress}/relay/0|");
}
if (builder.Length <= 1)
{
return new();
}
string plugnames = builder.ToString()[..^1];
Debug.WriteLine($$"""sum(power{sensor=~"{{plugnames}}"})[1h:1m]""");
PrometheusDataWrapper? deviceData = (await promQueryer.Query($$"""sum(power{sensor=~"{{plugnames}}"})[1h:1m]"""))?.Data;
Random r = new Random(DateTime.Now.Millisecond);
return deviceData?.MatrixTypeToTimestampFloatTuple().ValueOrDefault?.Select(x =>
{
labels.Add(DateTimeOffset.FromUnixTimeSeconds(long.Parse($"1{x.Item1}0")).UtcDateTime.ToLocalTime().ToShortTimeString());
return x.Item2;
}).ToList() ?? new List<double>();
} }
} }
@@ -1,4 +1,19 @@
@using System.Diagnostics; @using System.Diagnostics;
@using Elektrifikatsiya.Layouts; @using Elektrifikatsiya.Layouts;
@using Elektrifikatsiya.Services;
@inject IAuthenticationService AuthenticationService;
@inject NavigationManager NavigationManager
@page "/login" @page "/login"
@layout LoginLayout; @layout LoginLayout;
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
bool isAuthenticated = (await AuthenticationService.IsAuthenticated()).ValueOrDefault;
if (isAuthenticated)
{
NavigationManager.NavigateTo("/");
}
}
}
@@ -1,5 +1,6 @@
@page "/Settings" @page "/Settings"
@using Blazorise.Components; @using Blazorise.Components;
@using Elektrifikatsiya.Database;
@using Elektrifikatsiya.Models; @using Elektrifikatsiya.Models;
@using System.Net @using System.Net
@using System.Text.Json @using System.Text.Json
@@ -9,6 +10,7 @@
@inject IDeviceManagmentService DeviceManagmentService @inject IDeviceManagmentService DeviceManagmentService
@inject IMessageService MessageService @inject IMessageService MessageService
@inject MainDatabaseContext MainDatabaseContext
<Div> <Div>
<Row Style="width: 100%; height: 100%" Margin="Margin.Is3.FromTop.OnMobile" Padding="Padding.Is4.FromStart.OnMobile"> <Row Style="width: 100%; height: 100%" Margin="Margin.Is3.FromTop.OnMobile" Padding="Padding.Is4.FromStart.OnMobile">
@@ -90,21 +92,7 @@
Here you can change the name of your plug Here you can change the name of your plug
</Paragraph> </Paragraph>
</FieldLabel> </FieldLabel>
<TextEdit @bind-Text="@DeviceCopy.Name" Placeholder="Enter Name" /> <TextEdit MaxLength="30" @bind-Text="@DeviceCopy.Name" Placeholder="Enter Name" />
</Field>
<br />
<br />
<Field>
<FieldLabel>
<h5>
Change Max Output
</h5>
<Paragraph>
Here you can limit how much power your plug uses
</Paragraph>
</FieldLabel>
<TextEdit Placeholder="Enter Max Output" />
<!--TODO: What is this?-->
</Field> </Field>
<br /> <br />
<br /> <br />
@@ -117,7 +105,7 @@
Here you select or change the room the plug belongs to. Here you select or change the room the plug belongs to.
</Paragraph> </Paragraph>
</FieldLabel> </FieldLabel>
<TextEdit @bind-text="@DeviceCopy.Room" Placeholder="Enter the name of the room here"/> <TextEdit MaxLength="30" @bind-text="@DeviceCopy.Room" Placeholder="Enter the name of the room here" />
</Field> </Field>
</Div> </Div>
} }
@@ -138,7 +126,7 @@
Here you enter your electricity price. This is gonna calculate the overall Price of your System Here you enter your electricity price. This is gonna calculate the overall Price of your System
</Paragraph> </Paragraph>
</FieldLabel> </FieldLabel>
<TextEdit Placeholder="Enter Electricity Price" /> <NumericEdit @bind-Value="electricityPrice" MaxLength="8" Placeholder="Enter Electricity Price" />
</Field> </Field>
</Div> </Div>
<Column ColumnSize="ColumnSize.IsFull" TextAlignment="TextAlignment.End"> <Column ColumnSize="ColumnSize.IsFull" TextAlignment="TextAlignment.End">
@@ -153,7 +141,9 @@
@code { @code {
List<Device> plugs = new List<Device>(); List<Device> plugs = new List<Device>();
private bool hideButtonSettings = true; bool hideButtonSettings = true;
double electricityPrice = 0;
Device? SelectedDevice = null; Device? SelectedDevice = null;
Device? DeviceCopy = null; Device? DeviceCopy = null;
@@ -166,6 +156,8 @@
{ {
plugs = getDevicesResult.Value; plugs = getDevicesResult.Value;
} }
electricityPrice = MainDatabaseContext.EnergyPriceChanges.OrderByDescending(e=>e.DateTime).FirstOrDefault()?.EnergyPrice ?? 0;
} }
private void Toggle(Device device) private void Toggle(Device device)
@@ -175,16 +167,19 @@
DeviceCopy = device.CopyDevice(); DeviceCopy = device.CopyDevice();
} }
private void OnSave() private async void OnSave()
{ {
if (hideButtonSettings || DeviceCopy is null || SelectedDevice is null) if (hideButtonSettings)
{ {
MainDatabaseContext.EnergyPriceChanges.Add(new EnergyPriceChange(electricityPrice));
await MainDatabaseContext.SaveChangesAsync();
} }
else else if(DeviceCopy is not null && SelectedDevice is not null)
{ {
SelectedDevice.OverwiteDevice(DeviceCopy); SelectedDevice.OverwiteDevice(DeviceCopy);
DeviceManagmentService.UpdateDevice(SelectedDevice); await DeviceManagmentService.UpdateDevice(SelectedDevice);
} }
} }
@@ -1,3 +1,5 @@
global using INotificationService = Elektrifikatsiya.Services.INotifcationService;
using Blazorise; using Blazorise;
using Blazorise.Bootstrap; using Blazorise.Bootstrap;
using Blazorise.Icons.Material; using Blazorise.Icons.Material;
@@ -7,20 +9,31 @@ using Elektrifikatsiya.Database;
using Elektrifikatsiya.Models; using Elektrifikatsiya.Models;
using Elektrifikatsiya.Services; using Elektrifikatsiya.Services;
using Elektrifikatsiya.Services.Implementations; using Elektrifikatsiya.Services.Implementations;
using Elektrifikatsiya.Utilities;
using HiveMQtt.Client; using HiveMQtt.Client;
using HiveMQtt.Client.Options; using HiveMQtt.Client.Options;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Read configuration
builder.Services.Configure<EmailSettings>(builder.Configuration.GetRequiredSection("EmailSettings"));
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddOptions();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<UpdateService>(); builder.Services.AddHostedService<ServiceScheduler>();
builder.Services.AddSingleton<IUpdateService, UpdateService>();
builder.Services.AddSingleton<IDeviceStatusService, DeviceStatusService>(); builder.Services.AddSingleton<IDeviceStatusService, DeviceStatusService>();
builder.Services.AddSingleton<INotificationService, NotifcationService>();
builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<IUpdateService>());
builder.Services.AddSingleton<IScheduledService>(s => s.GetRequiredService<INotificationService>());
builder.Services.AddSingleton<IEmailService, EmailService>();
builder.Services.AddSingleton<IEventService, EventService>();
builder.Services.AddSingleton<IHiveMQClient, HiveMQClient>((provider)=> builder.Services.AddSingleton<IHiveMQClient, HiveMQClient>((provider)=>
{ {
HiveMQClientOptions options = new() HiveMQClientOptions options = new()
@@ -30,7 +43,7 @@ builder.Services.AddSingleton<IHiveMQClient, HiveMQClient>((provider)=>
UseTLS = false, UseTLS = false,
}; };
HiveMQClient client = new(options); HiveMQClient client = new(options);
client.ConnectAsync().ConfigureAwait(false); client.ConnectAsync().ConfigureAwait(false);
return client; return client;
}); });
@@ -67,6 +80,9 @@ app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); app.MapFallbackToPage("/_Host");
app.MapControllers(); app.MapControllers();
app.Services.GetRequiredService<IOptions<EmailSettings>>().Value.CompileTemplates();
IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope(); IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
MainDatabaseContext mainDatabase = serviceScope.ServiceProvider.GetRequiredService<MainDatabaseContext>(); MainDatabaseContext mainDatabase = serviceScope.ServiceProvider.GetRequiredService<MainDatabaseContext>();
@@ -81,7 +97,7 @@ if (!mainDatabase.Users.Any())
app.Run(); app.Run();
void AddBlazorise(IServiceCollection services) static void AddBlazorise(IServiceCollection services)
{ {
_ = services.AddBlazorise(); _ = services.AddBlazorise();
_ = services.AddMaterialProviders(); _ = services.AddMaterialProviders();
@@ -6,7 +6,7 @@ namespace Elektrifikatsiya.Services;
public interface IDeviceStatusService public interface IDeviceStatusService
{ {
public event EventHandler<DeviceStatusChagedEventArgs> OnDeviceStatusChanged; public event EventHandler<DeviceStatusChangedEventArgs> OnDeviceStatusChanged;
public Result<List<Device>> GetDevices(); public Result<List<Device>> GetDevices();
@@ -0,0 +1,8 @@
namespace Elektrifikatsiya.Services;
public interface IEmailService
{
Task SendAsync(string to, string subject, string html, string? from = null);
Task SendWithTemeplateAsync(string to, string subject, string templateKey, string? from = null, Dictionary<string, string>? templateParameters = null);
}
@@ -0,0 +1,10 @@
using Elektrifikatsiya.Models;
namespace Elektrifikatsiya.Services
{
public interface IEventService
{
public List<Event> Events { get; }
public event EventHandler<EventServiceEventArgs> OnEventCalled;
}
}
@@ -0,0 +1,8 @@
using Elektrifikatsiya.Utilities;
namespace Elektrifikatsiya.Services;
public interface INotifcationService : IScheduledService
{
}
@@ -0,0 +1,8 @@
using Elektrifikatsiya.Utilities;
namespace Elektrifikatsiya.Services;
public interface IUpdateService : IScheduledService
{
}
@@ -173,6 +173,8 @@ public class DeviceManagmentService : IDeviceManagmentService
PublishResult res = await hiveMQClient.PublishAsync($"shellies/shellyplug-s-{device.MacAddress}/relay/0/command", device.Enabled ? "on" : "off"); PublishResult res = await hiveMQClient.PublishAsync($"shellies/shellyplug-s-{device.MacAddress}/relay/0/command", device.Enabled ? "on" : "off");
device.User = mainDatabaseContext.Users.First(u => u.Id == device.User.Id);
_ = mainDatabaseContext.Update(device); _ = mainDatabaseContext.Update(device);
return await Result.Try(Task () => mainDatabaseContext.SaveChangesAsync()); return await Result.Try(Task () => mainDatabaseContext.SaveChangesAsync());
@@ -1,5 +1,4 @@
using Elektrifikatsiya.Models; using Elektrifikatsiya.Models;
using FluentResults; using FluentResults;
namespace Elektrifikatsiya.Services.Implementations; namespace Elektrifikatsiya.Services.Implementations;
@@ -7,15 +6,14 @@ namespace Elektrifikatsiya.Services.Implementations;
public class DeviceStatusService : IDeviceStatusService public class DeviceStatusService : IDeviceStatusService
{ {
private readonly ILogger<DeviceStatusService> logger; private readonly ILogger<DeviceStatusService> logger;
public event EventHandler<DeviceStatusChangedEventArgs>? OnDeviceStatusChanged;
public event EventHandler<DeviceStatusChagedEventArgs>? OnDeviceStatusChanged;
private readonly Dictionary<string, Device> devices = new(); private readonly Dictionary<string, Device> devices = new();
public DeviceStatusService(ILogger<DeviceStatusService> logger) public DeviceStatusService(ILogger<DeviceStatusService> logger)
{ {
this.logger = logger; this.logger = logger;
} }
public Result<List<Device>> GetDevices() public Result<List<Device>> GetDevices()
{ {
@@ -36,7 +34,7 @@ public class DeviceStatusService : IDeviceStatusService
modDevice.IpAddress = device.IpAddress; modDevice.IpAddress = device.IpAddress;
modDevice.Name = device.Name; modDevice.Name = device.Name;
OnDeviceStatusChanged?.Invoke(this, new DeviceStatusChagedEventArgs(device.MacAddress)); OnDeviceStatusChanged?.Invoke(this, new DeviceStatusChangedEventArgs(device.MacAddress));
return Result.Ok(); return Result.Ok();
} }
@@ -0,0 +1,43 @@
using Elektrifikatsiya.Utilities;
using HandlebarsDotNet;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Text;
namespace Elektrifikatsiya.Services.Implementations
{
public class EmailService : IEmailService
{
private readonly EmailSettings emailSettings;
public EmailService(IOptions<EmailSettings> emailSettings)
{
this.emailSettings = emailSettings.Value;
}
public async Task SendAsync(string to, string subject, string html, string? from = null)
{
from ??= emailSettings.DefaultEmail;
MimeMessage message = new MimeMessage();
message.From.Add(MailboxAddress.Parse(from));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = subject;
message.Body = new TextPart(TextFormat.Html) { Text = html };
using SmtpClient smtp = new SmtpClient();
await smtp.ConnectAsync(emailSettings.SmtpServer, emailSettings.SmtpPort, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(emailSettings.User, emailSettings.Key);
await smtp.SendAsync(message);
await smtp.DisconnectAsync(true);
}
public Task SendWithTemeplateAsync(string to, string subject, string templateKey, string? from = null, Dictionary<string, string>? templateParameters = null)
{
string content = emailSettings.CompiledTemplates[templateKey](templateParameters ?? new());
return SendAsync(to, subject, content, from);
}
}
}
@@ -0,0 +1,55 @@
using Elektrifikatsiya.Models;
using FluentResults;
using System.Reflection;
namespace Elektrifikatsiya.Services.Implementations
{
public class EventService : IEventService
{
private readonly ILogger<EventService> logger;
public event EventHandler<EventServiceEventArgs> OnEventCalled;
public List<Event> Events { get; } = new();
public EventService(ILogger<EventService> logger, IDeviceStatusService deviceStatusService, IDeviceManagmentService deviceManagmentService)
{
this.logger = logger;
List<Device> deviceList = deviceManagmentService.GetDevices().ValueOrDefault.Select(x=>x.CopyDevice()).ToList();
deviceStatusService.OnDeviceStatusChanged += (_, e) =>
{
bool newDevice = true;
Device currentDevice = deviceManagmentService.GetDevice(e.MacAddress).ValueOrDefault;
for (int i = 0; i < deviceList.Count; i++)
{
if (deviceList[i].MacAddress == currentDevice.MacAddress)
{
PropertyInfo[] properties = typeof(Device).GetProperties();
foreach(PropertyInfo property in properties) {
var currentvalue = property.GetValue(deviceList[i], null);
var newvalue = property.GetValue(currentDevice, null);
if(newvalue is not null && !newvalue.ToString().Equals(currentvalue.ToString()))
{
string eventName = $"Plug {deviceList[i].Name} changed: {property.Name}";
string description = $"Changed {property.Name} of plug [{currentvalue}] to [{newvalue}]";
DateTime dateTime = DateTime.Now;
Event newEvent = new Event(eventName, description, dateTime, currentDevice);
Events.Insert(0,newEvent);
OnEventCalled?.Invoke(this, new EventServiceEventArgs(newEvent));
}
}
deviceList[i].OverwiteDevice(currentDevice);
newDevice = false; break;
}
}
if (newDevice)
{
deviceList.Add(deviceManagmentService.GetDevice(e.MacAddress).ValueOrDefault);
}
};
}
}
}
@@ -0,0 +1,24 @@
using Elektrifikatsiya.Utilities;
namespace Elektrifikatsiya.Services.Implementations;
public class NotifcationService : INotifcationService
{
public TimeSpan ExecutionRepeatDelay => TimeSpan.FromDays(1);
public DateTime FirstExecutionTime => DateTime.Now.AddDays(0);
private readonly IEmailService emailService;
private readonly ILogger<NotifcationService> logger;
public NotifcationService(ILogger<NotifcationService> logger, IEmailService emailService)
{
this.logger = logger;
this.emailService = emailService;
}
public async void Update()
{
//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");
}
}
@@ -0,0 +1,61 @@
using Elektrifikatsiya.Utilities;
namespace Elektrifikatsiya.Services.Implementations;
public class ServiceScheduler : IHostedService
{
private readonly ILogger<ServiceScheduler> logger;
private readonly IEnumerable<IScheduledService> scheduledServices;
private readonly Dictionary<IScheduledService, DateTime> lastExectued;
private Timer timer = null!;
public ServiceScheduler(ILogger<ServiceScheduler> logger, IServiceProvider serviceProvider)
{
this.logger = logger;
scheduledServices = serviceProvider.GetServices<IScheduledService>();
lastExectued = scheduledServices.ToDictionary(x => x, x => x.FirstExecutionTime);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Starting service scheduler...");
timer = new Timer(async (_) => await Update(), null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
logger.LogInformation("Service scheduler started.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Stopping service scheduler.");
timer.Dispose();
logger.LogInformation("Stopped service scheduler.");
return Task.CompletedTask;
}
private async Task Update()
{
TimeSpan s = lastExectued.Min(x => (x.Value + x.Key.ExecutionRepeatDelay)) - DateTime.Now;
if (s < TimeSpan.Zero)
{
s = TimeSpan.FromSeconds(1);
}
timer.Change(s, Timeout.InfiniteTimeSpan);
foreach (IScheduledService scheduledService in scheduledServices.Where(s => s.FirstExecutionTime < DateTime.Now))
{
if (lastExectued[scheduledService] + scheduledService.ExecutionRepeatDelay <= DateTime.Now)
{
lastExectued[scheduledService] = DateTime.Now;
scheduledService.Update();
}
}
}
}
@@ -8,38 +8,29 @@ using Microsoft.EntityFrameworkCore;
namespace Elektrifikatsiya.Services.Implementations; namespace Elektrifikatsiya.Services.Implementations;
public class UpdateService : IHostedService, IDisposable public class UpdateService : IUpdateService
{ {
private readonly ILogger<UpdateService> logger; private readonly ILogger<UpdateService> logger;
private readonly IDeviceStatusService deviceStatusService; private readonly IDeviceStatusService deviceStatusService;
private readonly IServiceScopeFactory serviceScopeFactory;
private Timer? timer = null;
public UpdateService(ILogger<UpdateService> logger, IDeviceStatusService deviceStatusService, IServiceScopeFactory serviceScopeFactory) public UpdateService(ILogger<UpdateService> logger, IDeviceStatusService deviceStatusService, IServiceScopeFactory serviceScopeFactory)
{ {
this.logger = logger; this.logger = logger;
this.deviceStatusService = deviceStatusService; this.deviceStatusService = deviceStatusService;
this.serviceScopeFactory = serviceScopeFactory;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
IServiceScope serviceScope = serviceScopeFactory.CreateScope(); IServiceScope serviceScope = serviceScopeFactory.CreateScope();
MainDatabaseContext mainDatabaseContext = serviceScope.ServiceProvider.GetRequiredService<MainDatabaseContext>(); MainDatabaseContext mainDatabaseContext = serviceScope.ServiceProvider.GetRequiredService<MainDatabaseContext>();
logger.LogInformation("Starting update service..."); foreach (Device device in mainDatabaseContext.Devices.Include(d => d.User).AsNoTracking().ToList())
foreach (Device device in await mainDatabaseContext.Devices.Include(d => d.User).AsNoTracking().ToListAsync(cancellationToken))
{ {
_ = deviceStatusService.TrackDevice(device); _ = deviceStatusService.TrackDevice(device);
} }
timer = new Timer(async (_) => await Update(), null, TimeSpan.Zero, TimeSpan.FromSeconds(15));
logger.LogInformation("Update service started.");
} }
private async Task Update() public DateTime FirstExecutionTime { get; } = DateTime.Now.AddSeconds(15);
public TimeSpan ExecutionRepeatDelay { get; } = TimeSpan.FromSeconds(15);
public async void Update()
{ {
Result<List<Device>> getDeviceStatusResult = deviceStatusService.GetDevices(); Result<List<Device>> getDeviceStatusResult = deviceStatusService.GetDevices();
@@ -61,26 +52,4 @@ public class UpdateService : IHostedService, IDisposable
} }
} }
} }
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Stopping update service.");
_ = timer?.Change(Timeout.Infinite, 0);
logger.LogInformation("Stopped update service.");
return Task.CompletedTask;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
timer?.Dispose();
}
} }
@@ -0,0 +1,23 @@
using HandlebarsDotNet;
namespace Elektrifikatsiya.Utilities;
public class EmailSettings
{
public string User { get; set; }
public string DefaultEmail { get; set; }
public string Key { get; set; }
public Dictionary<string, string> Templates { get; set; }
public string SmtpServer { get; set; }
public int SmtpPort { get; set; }
public Dictionary<string, HandlebarsTemplate<object, object>> CompiledTemplates = new();
public void CompileTemplates()
{
foreach ((string templateName, string templatePath) in Templates)
{
CompiledTemplates.Add(templateName, Handlebars.Compile(File.ReadAllText(templatePath)));
}
}
}
@@ -0,0 +1,9 @@
namespace Elektrifikatsiya.Utilities;
public interface IScheduledService
{
DateTime FirstExecutionTime { get; }
TimeSpan ExecutionRepeatDelay { get; }
void Update();
}
@@ -1,9 +0,0 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
<mjml>
<mj-head>
<mj-attributes>
<mj-class name="primary" background-color="#1a237e"/>
</mj-attributes>
<mj-style>
@font-face {
font-family: Nunito;
src: url(https://fonts.gstatic.com/s/nunito/v16/XRXV3I6Li01BKofINeaB.woff2)
format('truetype');
}
</mj-style>
</mj-head>
<mj-body background-color="white">
<mj-wrapper padding-right="20px" padding-left="20px" padding-top="7%" >
<mj-section border-right="2px solid #B0B0B0" border-top="2px solid #B0B0B0" border-left="2px solid #B0B0B0" border-radius="33px">
</mj-section>
<mj-section padding-bottom="0px" padding-top="0px" border-left="2px solid #B0B0B0" border-right="2px solid #B0B0B0">
<mj-column>
<mj-text align="center" font-family="Nunito, Sans-Serif" font-weight="bold" font-size="30px">
Daily Notification
</mj-text>
</mj-column>
</mj-section>
<mj-section padding-bottom="0px" border-left="2px solid #B0B0B0" border-right="2px solid #B0B0B0">
<mj-column padding-left="5%" padding-right="5%">
<mj-text font-family="Nunito, Sans-Serif" align="center" font-size="18px">
Energy Consumption yesterday: {{yesterday}}
</mj-text>
</mj-column>
<mj-column>
<mj-text font-family="Nunito, Sans-Serif" algin="center" font-size="18px">
Energy Consumption since the start of the year: {{yearly}}
</mj-text>
</mj-column>
</mj-section>
<mj-section padding-top="0px" border-right="2px solid #B0B0B0" border-left="2px solid #B0B0B0" border-bottom="2px solid #B0B0B0" border-radius="33px">
<mj-column>
<mj-button font-size="16px" mj-class="primary" font-weight="bold" font-family="Nunito, Sans-Serif">
<mj-text>
<a href="{{link}}" target="_blank" style="color:white; text-decoration:none;">Goto Dashboard</a>
</mj-text>
</mj-button>
</mj-column>
</mj-section>
</mj-wrapper>
</mj-body>
</mjml>