From d970f80278799f26e96d706416246e43ee394776 Mon Sep 17 00:00:00 2001 From: Chris Kaczor Date: Tue, 15 Oct 2019 19:41:52 -0400 Subject: [PATCH] Add power database support --- .../Service/Controllers/ReadingsController.cs | 11 --- Power/Service/Controllers/StatusController.cs | 28 ++++++ Power/Service/Data/Database.cs | 91 +++++++++++++++++++ Power/Service/Data/Resources/CreateStatus.sql | 2 + .../Resources/GetStatusHistoryGrouped.sql | 14 +++ Power/Service/Data/Resources/Schema.sql | 9 ++ Power/Service/Models/PowerStatus.cs | 2 + Power/Service/Models/PowerStatusGrouped.cs | 13 +++ Power/Service/PowerReader.cs | 9 +- Power/Service/ResourceReader.cs | 22 +++++ Power/Service/Service.csproj | 14 +++ Power/Service/Startup.cs | 8 +- 12 files changed, 209 insertions(+), 14 deletions(-) delete mode 100644 Power/Service/Controllers/ReadingsController.cs create mode 100644 Power/Service/Controllers/StatusController.cs create mode 100644 Power/Service/Data/Database.cs create mode 100644 Power/Service/Data/Resources/CreateStatus.sql create mode 100644 Power/Service/Data/Resources/GetStatusHistoryGrouped.sql create mode 100644 Power/Service/Data/Resources/Schema.sql create mode 100644 Power/Service/Models/PowerStatusGrouped.cs create mode 100644 Power/Service/ResourceReader.cs diff --git a/Power/Service/Controllers/ReadingsController.cs b/Power/Service/Controllers/ReadingsController.cs deleted file mode 100644 index dc48dc7..0000000 --- a/Power/Service/Controllers/ReadingsController.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace ChrisKaczor.HomeMonitor.Power.Service.Controllers -{ - [Route("[controller]")] - [ApiController] - public class ReadingsController : ControllerBase - { - - } -} \ No newline at end of file diff --git a/Power/Service/Controllers/StatusController.cs b/Power/Service/Controllers/StatusController.cs new file mode 100644 index 0000000..3407406 --- /dev/null +++ b/Power/Service/Controllers/StatusController.cs @@ -0,0 +1,28 @@ +using ChrisKaczor.HomeMonitor.Power.Service.Data; +using ChrisKaczor.HomeMonitor.Power.Service.Models; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Controllers +{ + [Route("[controller]")] + [ApiController] + public class StatusController : ControllerBase + { + private readonly Database _database; + + public StatusController(Database database) + { + _database = database; + } + + [HttpGet("history-grouped")] + public async Task>> GetHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2) + { + return (await _database.GetStatusHistoryGrouped(start, end, bucketMinutes)).ToList(); + } + } +} \ No newline at end of file diff --git a/Power/Service/Data/Database.cs b/Power/Service/Data/Database.cs new file mode 100644 index 0000000..304c132 --- /dev/null +++ b/Power/Service/Data/Database.cs @@ -0,0 +1,91 @@ +using ChrisKaczor.HomeMonitor.Power.Service.Models; +using Dapper; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Data +{ + public class Database + { + private readonly IConfiguration _configuration; + + public Database(IConfiguration configuration) + { + _configuration = configuration; + } + + public void EnsureDatabase() + { + var connectionStringBuilder = new SqlConnectionStringBuilder + { + DataSource = $"{_configuration["Power:Database:Host"]},{_configuration["Power:Database:Port"]}", + UserID = _configuration["Power:Database:User"], + Password = _configuration["Power:Database:Password"], + InitialCatalog = "master" + }; + + using var connection = new SqlConnection(connectionStringBuilder.ConnectionString); + + var command = new SqlCommand { Connection = connection }; + + connection.Open(); + + // Check to see if the database exists + command.CommandText = $"SELECT CAST(1 as bit) from sys.databases WHERE name='{_configuration["Power:Database:Name"]}'"; + var databaseExists = (bool?)command.ExecuteScalar(); + + // Create database if needed + if (!databaseExists.GetValueOrDefault(false)) + { + command.CommandText = $"CREATE DATABASE {_configuration["Power:Database:Name"]}"; + command.ExecuteNonQuery(); + } + + // Switch to the database now that we're sure it exists + connection.ChangeDatabase(_configuration["Power:Database:Name"]); + + var schema = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.Schema.sql"); + + // Make sure the database is up to date + command.CommandText = schema; + command.ExecuteNonQuery(); + } + + private SqlConnection CreateConnection() + { + var connectionStringBuilder = new SqlConnectionStringBuilder + { + DataSource = $"{_configuration["Power:Database:Host"]},{_configuration["Power:Database:Port"]}", + UserID = _configuration["Power:Database:User"], + Password = _configuration["Power:Database:Password"], + InitialCatalog = _configuration["Power:Database:Name"] + }; + + var connection = new SqlConnection(connectionStringBuilder.ConnectionString); + connection.Open(); + + return connection; + } + + public void StorePowerData(PowerStatus powerStatus) + { + using var connection = CreateConnection(); + + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.CreateStatus.sql"); + + connection.Query(query, powerStatus); + } + + public async Task> GetStatusHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes) + { + await using var connection = CreateConnection(); + + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.GetStatusHistoryGrouped.sql"); + + return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); + } + } +} \ No newline at end of file diff --git a/Power/Service/Data/Resources/CreateStatus.sql b/Power/Service/Data/Resources/CreateStatus.sql new file mode 100644 index 0000000..d4f9858 --- /dev/null +++ b/Power/Service/Data/Resources/CreateStatus.sql @@ -0,0 +1,2 @@ +INSERT INTO Status (Timestamp, Generation, Consumption) +VALUES (@Timestamp, @Generation, @Consumption) \ No newline at end of file diff --git a/Power/Service/Data/Resources/GetStatusHistoryGrouped.sql b/Power/Service/Data/Resources/GetStatusHistoryGrouped.sql new file mode 100644 index 0000000..8520df3 --- /dev/null +++ b/Power/Service/Data/Resources/GetStatusHistoryGrouped.sql @@ -0,0 +1,14 @@ +SELECT Bucket, + AVG(Generation) AS AverageGeneration, + AVG(Consumption) AS AverageConsumption +FROM ( + SELECT CAST(FORMAT(Timestamp, 'yyyy-MM-ddTHH:') + + RIGHT('00' + CAST(DATEPART(MINUTE, Timestamp) / @BucketMinutes * @BucketMinutes AS VARCHAR), 2) + + ':00+00:00' AS DATETIMEOFFSET) AS Bucket, + Generation, + Consumption + FROM Status + WHERE Timestamp BETWEEN @Start AND @End + ) AS Data +GROUP BY Bucket +ORDER BY Bucket \ No newline at end of file diff --git a/Power/Service/Data/Resources/Schema.sql b/Power/Service/Data/Resources/Schema.sql new file mode 100644 index 0000000..a712e48 --- /dev/null +++ b/Power/Service/Data/Resources/Schema.sql @@ -0,0 +1,9 @@ +IF NOT EXISTS(SELECT 1 FROM sys.tables WHERE name = 'Status') + CREATE TABLE Status + ( + Timestamp datetimeoffset NOT NULL + CONSTRAINT status_pk + PRIMARY KEY, + Generation int NOT NULL, + Consumption int NOT NULL + ); diff --git a/Power/Service/Models/PowerStatus.cs b/Power/Service/Models/PowerStatus.cs index aa6ae0e..2fd86d4 100644 --- a/Power/Service/Models/PowerStatus.cs +++ b/Power/Service/Models/PowerStatus.cs @@ -1,10 +1,12 @@ using JetBrains.Annotations; +using System; namespace ChrisKaczor.HomeMonitor.Power.Service.Models { [PublicAPI] public class PowerStatus { + public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow; public long Generation { get; set; } public long Consumption { get; set; } } diff --git a/Power/Service/Models/PowerStatusGrouped.cs b/Power/Service/Models/PowerStatusGrouped.cs new file mode 100644 index 0000000..f82a68c --- /dev/null +++ b/Power/Service/Models/PowerStatusGrouped.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using System; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Models +{ + [PublicAPI] + public class PowerStatusGrouped + { + public DateTimeOffset Bucket { get; set; } + public long AverageGeneration { get; set; } + public long AverageConsumption { get; set; } + } +} diff --git a/Power/Service/PowerReader.cs b/Power/Service/PowerReader.cs index f3bbbac..6af3ced 100644 --- a/Power/Service/PowerReader.cs +++ b/Power/Service/PowerReader.cs @@ -1,4 +1,5 @@ -using ChrisKaczor.HomeMonitor.Power.Service.Models; +using ChrisKaczor.HomeMonitor.Power.Service.Data; +using ChrisKaczor.HomeMonitor.Power.Service.Models; using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Configuration; @@ -16,13 +17,15 @@ namespace ChrisKaczor.HomeMonitor.Power.Service public class PowerReader : IHostedService { private readonly IConfiguration _configuration; + private readonly Database _database; private HubConnection _hubConnection; private Timer _readTimer; - public PowerReader(IConfiguration configuration) + public PowerReader(IConfiguration configuration, Database database) { _configuration = configuration; + _database = database; } public Task StartAsync(CancellationToken cancellationToken) @@ -54,6 +57,8 @@ namespace ChrisKaczor.HomeMonitor.Power.Service var status = new PowerStatus { Generation = generation.RealPower, Consumption = consumption.RealPower }; + _database.StorePowerData(status); + var json = JsonSerializer.Serialize(status); Console.WriteLine(json); diff --git a/Power/Service/ResourceReader.cs b/Power/Service/ResourceReader.cs new file mode 100644 index 0000000..d81ee8a --- /dev/null +++ b/Power/Service/ResourceReader.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Reflection; + +namespace ChrisKaczor.HomeMonitor.Power.Service +{ + public static class ResourceReader + { + public static string GetString(string resourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(resourceName); + + if (stream == null) + throw new Exception($"Resource {resourceName} not found in {assembly.FullName}. Valid resources are: {string.Join(", ", assembly.GetManifestResourceNames())}."); + + using var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + } +} \ No newline at end of file diff --git a/Power/Service/Service.csproj b/Power/Service/Service.csproj index c3abec8..d579c1a 100644 --- a/Power/Service/Service.csproj +++ b/Power/Service/Service.csproj @@ -8,8 +8,22 @@ + + + + + + + + + + + + + + diff --git a/Power/Service/Startup.cs b/Power/Service/Startup.cs index 70c35e2..3a1ecd8 100644 --- a/Power/Service/Startup.cs +++ b/Power/Service/Startup.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Builder; +using ChrisKaczor.HomeMonitor.Power.Service.Data; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.ResponseCompression; @@ -12,6 +13,8 @@ namespace ChrisKaczor.HomeMonitor.Power.Service { public void ConfigureServices(IServiceCollection services) { + services.AddTransient(); + services.AddHostedService(); services.Configure(options => @@ -35,6 +38,9 @@ namespace ChrisKaczor.HomeMonitor.Power.Service if (environment.IsDevelopment()) applicationBuilder.UseDeveloperExceptionPage(); + var database = applicationBuilder.ApplicationServices.GetService(); + database.EnsureDatabase(); + applicationBuilder.UseCors("CorsPolicy"); applicationBuilder.UseResponseCompression();