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

This commit is contained in:
Friend2868
2023-03-08 10:14:46 +01:00
18 changed files with 553 additions and 94 deletions
+60 -61
View File
@@ -1,64 +1,63 @@
<Blazorise.ThemeProvider Theme="@theme"> @using Blazorise.Components
<Router AppAssembly="typeof(App).Assembly"> <Blazorise.ThemeProvider Theme="@theme">
<Found Context="routeData"> <Router AppAssembly="typeof(App).Assembly">
<RouteView RouteData="routeData" DefaultLayout="typeof(Elektrifikatsiya.Layouts.MainLayout)" /> <Found Context="routeData">
</Found> <RouteView RouteData="routeData" DefaultLayout="typeof(Elektrifikatsiya.Layouts.MainLayout)" />
<NotFound> </Found>
<p>Sorry, there's nothing at this address.</p> <NotFound>
</NotFound> <p>Sorry, there's nothing at this address.</p>
</Router> </NotFound>
<MessageProvider /> </Router>
<NotificationProvider /> <MessageProvider />
<PageProgressProvider /> <NotificationProvider />
<PageProgressProvider />
</Blazorise.ThemeProvider> </Blazorise.ThemeProvider>
@code { @code {
private Theme theme = new() private Theme theme = new()
{ {
BarOptions = new() BarOptions = new()
{ {
HorizontalHeight = "72px", HorizontalHeight = "72px",
DarkColors = new() DarkColors = new()
{ {
ItemColorOptions = new() ItemColorOptions = new()
{ {
ActiveBackgroundColor = "#1a237e", ActiveBackgroundColor = "#1a237e",
ActiveColor = "#1a237e", ActiveColor = "#1a237e",
HoverColor = "#1a237e", HoverColor = "#1a237e",
} }
}
} },
}, ColorOptions = new()
ColorOptions = new() {
{ Primary = "#1a237e",
Primary = "#1a237e", Secondary = "#A65529",
Secondary = "#A65529", Success = "#23C02E",
Success = "#23C02E", Info = "#9BD8FE",
Info = "#9BD8FE", Warning = "#F8B86C",
Warning = "#F8B86C", Danger = "#F95741",
Danger = "#F95741", Light = "#F0F0F0",
Light = "#F0F0F0", Dark = "#535353",
Dark = "#535353", },
}, BackgroundOptions = new()
BackgroundOptions = new() {
{ Primary = "#1a237e",
Primary = "#1a237e", Secondary = "#A65529",
Secondary = "#A65529", Success = "#23C02E",
Success = "#23C02E", Info = "#9BD8FE",
Info = "#9BD8FE", Warning = "#F8B86C",
Warning = "#F8B86C", Danger = "#F95741",
Danger = "#F95741", Light = "#F0F0F0",
Light = "#F0F0F0", Dark = "#535353",
Dark = "#535353", },
}, InputOptions = new()
InputOptions = new() {
{ CheckColor = "#1a237e",
CheckColor = "#1a237e", },
}, TextColorOptions = new()
TextColorOptions = new() {
{ Dark = "#000000",
Dark = "#000000", Primary = "#1a237e"
Primary = "#1a237e" }
} };
};
} }
@@ -6,5 +6,9 @@ namespace Elektrifikatsiya.Database
public class DeviceManagmentDatabaseContext : DbContext public class DeviceManagmentDatabaseContext : DbContext
{ {
public DbSet<Device> Devices { get; set; } public DbSet<Device> Devices { get; set; }
public DeviceManagmentDatabaseContext(DbContextOptions<DeviceManagmentDatabaseContext> options) : base(options)
{
}
} }
} }
@@ -17,6 +17,22 @@
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath> <DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Middleware\**" />
<Compile Remove="Model\**" />
<Content Remove="Middleware\**" />
<Content Remove="Model\**" />
<EmbeddedResource Remove="Middleware\**" />
<EmbeddedResource Remove="Model\**" />
<None Remove="Middleware\**" />
<None Remove="Model\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Models\NewFile.txt" />
<None Remove="Models\ShellyResponse" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazorise" Version="1.2.0" /> <PackageReference Include="Blazorise" Version="1.2.0" />
<PackageReference Include="Blazorise.Charts" Version="1.2.0" /> <PackageReference Include="Blazorise.Charts" Version="1.2.0" />
@@ -28,11 +44,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
</ItemGroup> <PackageReference Include="Tmds.MDns" Version="0.7.1" />
<ItemGroup>
<Folder Include="Middleware\" />
<Folder Include="Model\" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,8 +1,6 @@
@using Elektrifikatsiya.Components.Layout @using Elektrifikatsiya.Components.Layout
@inherits LayoutComponentBase @inherits LayoutComponentBase
<Layout Sider> <Layout Sider>
<LayoutSider> <LayoutSider>
<LayoutSiderContent> <LayoutSiderContent>
@@ -14,4 +12,4 @@
@Body @Body
</LayoutContent> </LayoutContent>
</Layout> </Layout>
</Layout> </Layout>
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net; using System.Net;
namespace Elektrifikatsiya.Models; namespace Elektrifikatsiya.Models;
@@ -6,21 +7,30 @@ namespace Elektrifikatsiya.Models;
public class Device public class Device
{ {
[Key] [Key]
public string Key { get; private set; } public string MacAddress { get; private set; }
[Required] [Required]
public string Name { get; private set; } = string.Empty; public string Name { get; set; }
[Required] [Required]
public IPAddress Address { get; private set; } public IPAddress IpAddress { get; set; }
[Required]
public User User { get; set; }
[NotMapped]
public int PowerUsage { get; set; } public int PowerUsage { get; set; }
public string User { get; set; }
public string Room { get; set; } public string Room { get; set; }
public Device(string key, string name, IPAddress address, int powerUsage, string user, string room) [NotMapped]
public bool Available { get; set; }
public Device(string macAddress, string name, IPAddress address, User user, string room)
{ {
Key = key; MacAddress = macAddress;
Name = name; Name = name;
Address = address; IpAddress = address;
PowerUsage = powerUsage;
User = user; User = user;
Room = room; Room = room;
} }
@@ -0,0 +1,11 @@
namespace Elektrifikatsiya.Models;
public class DeviceStatusChagedEventArgs : EventArgs
{
public string MacAddress { get; set; }
public DeviceStatusChagedEventArgs(string macAddress)
{
MacAddress = macAddress;
}
}
@@ -0,0 +1,73 @@
using System.Text.Json.Serialization;
namespace Elektrifikatsiya.Models;
public enum Status
{
Success,
Error
}
public enum ResultType
{
Matrix,
Vector,
Scalar,
String
}
public class PrometheusQueryResult
{
public Status Status { get; set; }
public string? ErrorType { get; set; }
public string? Error { get; set; }
public List<string>? Warnings { get; set; }
public PrometheusQueryResult(Status status, string? errorType, string? error, List<string>? warnings)
{
Status = status;
ErrorType = errorType;
Error = error;
Warnings = warnings;
}
}
internal class PrometheusDataWrapper
{
public ResultType ResultType { get; set; }
public List<PrometheusData> Result { get; set; }
public PrometheusDataWrapper(ResultType resultType, List<PrometheusData> result)
{
ResultType = resultType;
Result = result;
}
}
internal class PrometheusDataMetric
{
[JsonPropertyName("__name__")] public string Name { get; set; }
public string Job { get; set; }
public string Instance { get; set; }
public PrometheusDataMetric(string name, string job, string instance)
{
Name = name;
Job = job;
Instance = instance;
}
}
internal class PrometheusData
{
public PrometheusDataMetric Metric { get; set; }
public object Value { get; set; }
public PrometheusData(PrometheusDataMetric metric, object value)
{
Metric = metric;
Value = value;
}
}
@@ -0,0 +1,13 @@
namespace Elektrifikatsiya.Models;
public class ShellyResponse
{
public string Type { get; set; }
public string Mac { get; set; }
public ShellyResponse(string type, string mac)
{
Type = type;
Mac = mac;
}
}
@@ -4,6 +4,8 @@ using Blazorise.Icons.Material;
using Blazorise.Material; using Blazorise.Material;
using Elektrifikatsiya.Database; using Elektrifikatsiya.Database;
using Elektrifikatsiya.Services;
using Elektrifikatsiya.Services.Implementations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -12,7 +14,16 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddSingleton<IDeviceStatusService, DeviceStatusService>();
builder.Services.AddTransient<IDeviceManagmentService, DeviceManagmentService>();
builder.Services.AddDbContext<UserDatabaseContext>(options => options.UseSqlite("Data Source=./UserDatabase.sqlite")); builder.Services.AddDbContext<UserDatabaseContext>(options => options.UseSqlite("Data Source=./UserDatabase.sqlite"));
builder.Services.AddDbContext<DeviceManagmentDatabaseContext>((options) => options.UseSqlite("Data Source=./DeviceManagement.sqlite"));
builder.Services.AddBootstrapProviders();
builder.Services.AddBlazorise(options =>
{
options.Immediate = true;
});
AddBlazorise(builder.Services); AddBlazorise(builder.Services);
@@ -37,19 +48,16 @@ app.MapFallbackToPage("/_Host");
IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope(); IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
serviceScope.ServiceProvider.GetRequiredService<UserDatabaseContext>().Database.EnsureCreated(); serviceScope.ServiceProvider.GetRequiredService<UserDatabaseContext>().Database.EnsureCreated();
app.MapControllers();
AsyncServiceScope scope = app.Services.CreateAsyncScope();
scope.ServiceProvider.GetRequiredService<DeviceManagmentDatabaseContext>().Database.EnsureCreated();
app.Run(); app.Run();
void AddBlazorise(IServiceCollection services) void AddBlazorise(IServiceCollection services)
{ {
_ = services _ = services.AddBlazorise();
.AddBlazorise(); _ = services.AddMaterialProviders();
_ = services _ = services.AddMaterialIcons();
.AddMaterialProviders() }
.AddMaterialIcons();
}
builder.Services
.AddBlazorise(options =>
{
options.Immediate = true;
})
.AddBootstrapProviders();
@@ -15,7 +15,9 @@ public interface IAuthenticationService
/// <summary> /// <summary>
/// Registers a new user. /// Registers a new user.
/// </summary> /// </summary>
/// <param name="email">The E-mail address of the user.</param> /// <param name="name">The name address of the user.</param>
/// <param name="password">The password of the user.</param>
/// <param name="role">The default role of the user.</param>
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns> /// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
Task<Result> RegisterUserAsync(string name, string password, Role role); Task<Result> RegisterUserAsync(string name, string password, Role role);
@@ -23,7 +25,8 @@ public interface IAuthenticationService
/// <summary> /// <summary>
/// Logs a user in. /// Logs a user in.
/// </summary> /// </summary>
/// <param name="name">The E-mail address of the user.</param> /// <param name="name">The name of the user.</param>
/// <param name="password">The password of the user.</param>
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns> /// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
Task<Result> LoginUserAsync(string name, string password); Task<Result> LoginUserAsync(string name, string password);
@@ -36,14 +39,13 @@ public interface IAuthenticationService
/// <summary> /// <summary>
/// Deletes the current user. /// Deletes the current user.
/// </summary> /// </summary>
/// <param name="deletionToken">The deletion token.</param> /// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the operation was successful.</returns>
Task<Result> DeleteUserAsync(); Task<Result> DeleteUserAsync();
/// <summary> /// <summary>
/// Checks if a user exists. /// Checks if a user exists.
/// </summary> /// </summary>
/// <param name="name">The E-mail address of the user.</param> /// <param name="name">The name address of the user.</param>
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the user exists.</returns> /// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the user exists.</returns>
Task<Result<bool>> UserExistsAsync(string name); Task<Result<bool>> UserExistsAsync(string name);
@@ -0,0 +1,22 @@
using Elektrifikatsiya.Models;
using FluentResults;
using System.Net;
namespace Elektrifikatsiya.Services;
public interface IDeviceManagmentService
{
public Task<Result<Device>> Register(IPAddress ip, User user, string? name = null, string room = "default");
public Task<Result> Unregister(string macAdress);
public Result<Device> GetDevice(string macAdress);
public Result<List<Device>> GetDevices();
public Result<List<Device>> GetDevicesInRoom(string room);
public Result<List<Device>> GetDevicesOfUser(int userId);
}
@@ -0,0 +1,18 @@
using Elektrifikatsiya.Models;
using FluentResults;
namespace Elektrifikatsiya.Services;
public interface IDeviceStatusService
{
public event EventHandler<DeviceStatusChagedEventArgs> OnDeviceStatusChanged;
public Result<List<Device>> GetDevices();
public Result UpdateDeviceStatus(Device device);
public Result TrackDevice(Device device);
public Result UntrackDevice(string macAddress);
}
@@ -0,0 +1,125 @@
using Elektrifikatsiya.Database;
using Elektrifikatsiya.Models;
using FluentResults;
using System.Net;
using System.Net.NetworkInformation;
namespace Elektrifikatsiya.Services.Implementations;
public class DeviceManagmentService : IDeviceManagmentService
{
private readonly DeviceManagmentDatabaseContext deviceManagmentDatabaseContext;
private readonly IDeviceStatusService deviceStatusService;
private readonly HttpClient httpClient;
public DeviceManagmentService(DeviceManagmentDatabaseContext deviceManagmentDatabaseContext, IDeviceStatusService deviceStatusService, HttpClient httpClient)
{
this.deviceManagmentDatabaseContext = deviceManagmentDatabaseContext;
this.deviceStatusService = deviceStatusService;
this.httpClient = httpClient;
}
public Result<Device> GetDevice(string macAdress)
{
Result<List<Device>> getDevicesResult = deviceStatusService.GetDevices();
if (getDevicesResult.IsFailed)
{
return getDevicesResult.ToResult();
}
Device? device = getDevicesResult.Value.FirstOrDefault(d => d.MacAddress == macAdress);
if (device is null)
{
return Result.Fail("Device does not exist!");
}
return device;
}
public Result<List<Device>> GetDevices()
{
Result<List<Device>> getDevicesResult = deviceStatusService.GetDevices();
if (getDevicesResult.IsFailed)
{
return getDevicesResult.ToResult();
}
return getDevicesResult.Value.ToList();
}
public Result<List<Device>> GetDevicesInRoom(string room)
{
Result<List<Device>> getDevicesResult = deviceStatusService.GetDevices();
if (getDevicesResult.IsFailed)
{
return getDevicesResult.ToResult();
}
return getDevicesResult.Value.Where(d => d.Room == room).ToList();
}
public Result<List<Device>> GetDevicesOfUser(int userId)
{
Result<List<Device>> getDevicesResult = deviceStatusService.GetDevices();
if (getDevicesResult.IsFailed)
{
return getDevicesResult.ToResult();
}
return getDevicesResult.Value.Where(d => d.User.Id == userId).ToList();
}
public async Task<Result<Device>> Register(IPAddress ip, User user, string? name = null, string room = "default")
{
ShellyResponse? shellyResponse = await httpClient.GetFromJsonAsync<ShellyResponse>($"{ip}/shelly");
if(shellyResponse is null || shellyResponse.Type != "SHPLG-S")
{
return Result.Fail("Device is not a \"SHPLG-S\" or not reachable!");
}
string mac = shellyResponse.Mac;
if(PhysicalAddress.TryParse(mac, out _))
{
return Result.Fail("Invalid mac address!");
}
Device device = new Device(mac, name ?? mac, ip, user, room);
deviceManagmentDatabaseContext.Add(device);
Result saveDatabaseChangesResult = await Result.Try(async Task () => await deviceManagmentDatabaseContext.SaveChangesAsync());
Result trackDeviceResult = deviceStatusService.TrackDevice(device);
return Result.Merge(saveDatabaseChangesResult, trackDeviceResult).ToResult(device);
}
public async Task<Result> Unregister(string macAdress)
{
Result<Device> getDeviceResult = GetDevice(macAdress);
if (getDeviceResult.IsFailed)
{
return getDeviceResult.ToResult();
}
Result untrackDeviceResult = deviceStatusService.UntrackDevice(macAdress);
if (untrackDeviceResult.IsFailed)
{
return untrackDeviceResult;
}
deviceManagmentDatabaseContext.Remove(getDeviceResult.Value);
return await Result.Try(async Task () => await deviceManagmentDatabaseContext.SaveChangesAsync());
}
}
@@ -0,0 +1,53 @@
using Elektrifikatsiya.Models;
using FluentResults;
namespace Elektrifikatsiya.Services.Implementations;
public class DeviceStatusService : IDeviceStatusService
{
public event EventHandler<DeviceStatusChagedEventArgs>? OnDeviceStatusChanged;
private readonly Dictionary<string, Device> devices = new();
public Result<List<Device>> GetDevices()
{
return devices.Values.ToList();
}
public Result UpdateDeviceStatus(Device device)
{
Device? modDevice = devices.GetValueOrDefault(device.MacAddress);
if (modDevice is null)
{
return Result.Fail("Device not tracked!");
}
modDevice.PowerUsage = device.PowerUsage;
modDevice.Available = device.Available;
modDevice.IpAddress = device.IpAddress;
modDevice.Name = device.Name;
OnDeviceStatusChanged?.Invoke(this, new DeviceStatusChagedEventArgs(device.MacAddress));
return Result.Ok();
}
public Result TrackDevice(Device device)
{
if (devices.ContainsKey(device.MacAddress))
{
return Result.Fail("Device already tracked!");
}
devices[device.MacAddress] = device;
return Result.Ok();
}
public Result UntrackDevice(string macAddress)
{
return devices.Remove(macAddress) ? Result.Ok() : Result.Fail("Device is not tracked!");
}
}
@@ -0,0 +1,73 @@
using Elektrifikatsiya.Database;
using Elektrifikatsiya.Models;
using FluentResults;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace Elektrifikatsiya.Services.Implementations;
public class UpdateService : IHostedService, IDisposable
{
private readonly ILogger<UpdateService> logger;
private readonly IDeviceStatusService deviceStatusService;
private readonly DeviceManagmentDatabaseContext deviceManagmentDatabaseContext;
private Timer? timer = null;
public UpdateService(ILogger<UpdateService> logger, IDeviceStatusService deviceStatusService, DeviceManagmentDatabaseContext deviceManagmentDatabaseContext)
{
this.logger = logger;
this.deviceStatusService = deviceStatusService;
this.deviceManagmentDatabaseContext = deviceManagmentDatabaseContext;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Starting update service...");
foreach (Device device in await deviceManagmentDatabaseContext.Devices.AsNoTracking().ToListAsync(cancellationToken))
{
_ = deviceStatusService.TrackDevice(device);
}
timer = new Timer(async (_) => await Update(), null, TimeSpan.Zero, TimeSpan.FromSeconds(15));
logger.LogInformation("Update service started.");
}
private async Task Update()
{
Result<List<Device>> getDeviceStatusResult = deviceStatusService.GetDevices();
if (getDeviceStatusResult.IsFailed)
{
logger.LogError("Updating devices failed!");
}
foreach(Device device in getDeviceStatusResult.Value)
{
//TODO: Some update magic
}
}
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,25 @@
using System.Net;
using System.Net.Sockets;
using System.Text.Encodings.Web;
using System.Text.Json;
using Elektrifikatsiya.Models;
using FluentResults;
namespace Elektrifikatsiya.Utilities;
public class PrometheusQuery
{
private string connectionString;
private readonly HttpClient client = new();
public PrometheusQuery(string connectionString)
{
this.connectionString = connectionString;
client.BaseAddress = new Uri(connectionString);
}
public Task<PrometheusQueryResult?> Query(string query)
{
return client.GetFromJsonAsync<PrometheusQueryResult>($"/v1/query?{UrlEncoder.Create().Encode(query)}");
}
}
@@ -2,11 +2,24 @@
public static class TokenGenerator public static class TokenGenerator
{ {
/// <summary>
/// Creates secure token and uses an UID to make it unique.
/// </summary>
/// <param name="tokenType">The type of the token.</param>
/// <param name="uid">The UID of a user.</param>
/// <param name="length">The length of the token.</param>
/// <returns></returns>
public static string GenerateToken(string tokenType, int uid = 0, int length = 128) public static string GenerateToken(string tokenType, int uid = 0, int length = 128)
{ {
return tokenType + "-" + SecureStringGenerator.CreateCryptographicRandomString(length, uid); return tokenType + "-" + SecureStringGenerator.CreateCryptographicRandomString(length, uid);
} }
/// <summary>
/// Checks if a token is valid.
/// </summary>
/// <param name="token">The token to validate.</param>
/// <param name="tokenType">The type the token should have.</param>
/// <returns></returns>
public static bool ValidateToken(string? token, string tokenType) public static bool ValidateToken(string? token, string tokenType)
{ {
if (token is null) if (token is null)
Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB