diff --git a/.gitignore b/.gitignore index a63bd5c..4492e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ build/ bin/ +.vscode/ +!.vscode/settings.json +!.vscode/extensions.json + # ---> C++ # Prerequisites *.d diff --git a/include/electricity/server/api.hpp b/include/electricity/server/api.hpp deleted file mode 100644 index 128676e..0000000 --- a/include/electricity/server/api.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -#include -#include - -namespace Server::Api -{ - void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite); - - void SetupRouting(Pistache::Rest::Router & router); -} \ No newline at end of file diff --git a/include/electricity/server/configuration.hpp b/include/electricity/server/configuration.hpp deleted file mode 100644 index b98cc62..0000000 --- a/include/electricity/server/configuration.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#include -#include -#include - -namespace Server -{ - class Configuration { - private: - std::string logDirectory; - std::string serverDomain; - std::string lastExternalIp; - std::chrono::time_point lastIpCheckTimePoint; - std::mutex externalIpRefreshMutex; - - Configuration(); - Configuration(Configuration & other) = delete; - Configuration(Configuration && other) = delete; - Configuration & operator=(Configuration & other) = delete; - Configuration & operator=(Configuration && other) = delete; - - void RefreshExternalIp(); - bool ExternalIpRequiresRefresh() const; - - public: - void Setup(std::string & electricityLogDirectory, std::string const & serverDomain); - - std::string const & GetLogDirectory() const; - std::string const & GetExternalServerIp(); - - static Configuration & Get(); - }; -} \ No newline at end of file diff --git a/include/electricity/server/database.hpp b/include/electricity/server/database.hpp deleted file mode 100644 index ccef7e5..0000000 --- a/include/electricity/server/database.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#include -#include - -namespace Server::Database -{ - std::string GetDetailedJsonOf(Util::Date const & date); - std::string GetSummaryJsonOf(Util::Date const & startDate, Util::Date const & stopDate); -} \ No newline at end of file diff --git a/src/Electricity.Api/.gitignore b/src/Electricity.Api/.gitignore new file mode 100644 index 0000000..d86ba9f --- /dev/null +++ b/src/Electricity.Api/.gitignore @@ -0,0 +1,2 @@ +obj/ +bin/ \ No newline at end of file diff --git a/src/Electricity.Api/Constants.cs b/src/Electricity.Api/Constants.cs new file mode 100644 index 0000000..da12725 --- /dev/null +++ b/src/Electricity.Api/Constants.cs @@ -0,0 +1,6 @@ +namespace Electricity.Api; + +internal static class Constants +{ + public const string isoDateFormat = "yyyy-MM-dd"; +} \ No newline at end of file diff --git a/src/Electricity.Api/Controllers/ElectricityLogController.cs b/src/Electricity.Api/Controllers/ElectricityLogController.cs new file mode 100644 index 0000000..3403ed5 --- /dev/null +++ b/src/Electricity.Api/Controllers/ElectricityLogController.cs @@ -0,0 +1,45 @@ +using System.Net.Mime; +using Electricity.Api.Models; +using Electricity.Api.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Electricity.Api.Controllers; + +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Produces(MediaTypeNames.Application.Json)] +public class ElectricityLogController : ControllerBase +{ + private readonly ElectricityService electricityService; + private readonly ILogger logger; + + public ElectricityLogController( + ElectricityService electricityService, + ILogger logger) + { + this.electricityService = electricityService; + this.logger = logger; + } + + [HttpGet()] + [Route("/day")] + public async Task> Get([FromQuery] string? start, [FromQuery] string? stop) + { + if (string.IsNullOrWhiteSpace(start) || !DateOnly.TryParseExact(start, Constants.isoDateFormat, out _)) + { + return BadRequest(); + } + + if (string.IsNullOrWhiteSpace(stop)) + { + return await electricityService.GetDetailsFor(start); + } + + if (!DateOnly.TryParseExact(stop, Constants.isoDateFormat, out _) || string.Compare(start, stop) > 0) + { + return BadRequest(); + } + + return await electricityService.GetSummariesFor(start, stop); + } +} diff --git a/src/Electricity.Api/DatabaseContext.cs b/src/Electricity.Api/DatabaseContext.cs new file mode 100644 index 0000000..f2e7eae --- /dev/null +++ b/src/Electricity.Api/DatabaseContext.cs @@ -0,0 +1,45 @@ +using Electricity.Api.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Electricity.Api +{ + public partial class DatabaseContext : DbContext + { + public DatabaseContext() + { + } + + public DatabaseContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet ElectricityLogs { get; set; } = null!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlite("Data Source=test.db"); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + + entity.ToTable("ElectricityLog"); + + entity.HasIndex(e => e.Date, "idx_Date"); + + entity.HasIndex(e => e.TimeUtc, "idx_TimeUtc"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} diff --git a/src/Electricity.Api/Electricity.Api.csproj b/src/Electricity.Api/Electricity.Api.csproj new file mode 100644 index 0000000..062f4f5 --- /dev/null +++ b/src/Electricity.Api/Electricity.Api.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Electricity.Api/Entities/ElectricityLog.cs b/src/Electricity.Api/Entities/ElectricityLog.cs new file mode 100644 index 0000000..d5427f5 --- /dev/null +++ b/src/Electricity.Api/Entities/ElectricityLog.cs @@ -0,0 +1,16 @@ +namespace Electricity.Api.Entities +{ + public partial class ElectricityLog + { + public string Date { get; set; } = null!; + public string TimeUtc { get; set; } = null!; + public double CurrentPowerUsage { get; set; } + public double TotalPowerConsumptionDay { get; set; } + public double TotalPowerConsumptionNight { get; set; } + public double CurrentPowerReturn { get; set; } + public double TotalPowerReturnDay { get; set; } + public double TotalPowerReturnNight { get; set; } + public long DayTarifEnabled { get; set; } + public double GasConsumptionInCubicMeters { get; set; } + } +} diff --git a/src/Electricity.Api/Models/Day.cs b/src/Electricity.Api/Models/Day.cs new file mode 100644 index 0000000..de86ef6 --- /dev/null +++ b/src/Electricity.Api/Models/Day.cs @@ -0,0 +1,36 @@ +namespace Electricity.Api.Models; + +public record Day(uint DateTime, double TotalPowerUse, double TotalPowerReturn, double TotalGasUse) +{ + public static Day Empty(DateOnly date) + { + return new Day( + ConvertToEpoch(date.ToDateTime(new TimeOnly(0, 0), DateTimeKind.Utc)), + 0, 0, 0); + } + + public static Day FromEntity(Entities.ElectricityLog entity) + { + return new Day( + DateTime: ConvertToEpoch(ParseEntityDateTime(entity.Date, entity.TimeUtc)), + TotalPowerUse: entity.TotalPowerConsumptionDay + entity.TotalPowerConsumptionNight, + TotalPowerReturn: entity.TotalPowerReturnDay + entity.TotalPowerReturnNight, + TotalGasUse: entity.GasConsumptionInCubicMeters + ); + } + + private static DateTime ParseEntityDateTime(string date, string timeUtc) + { + return System.DateTime + .Parse( + $"{date}T{timeUtc}Z", + null, + System.Globalization.DateTimeStyles.AssumeUniversal) + .ToUniversalTime(); + } + + private static uint ConvertToEpoch(DateTime dateTime) + { + return (uint)Math.Round((dateTime - System.DateTime.UnixEpoch).TotalSeconds); + } +}; \ No newline at end of file diff --git a/src/Electricity.Api/Program.cs b/src/Electricity.Api/Program.cs new file mode 100644 index 0000000..272f94d --- /dev/null +++ b/src/Electricity.Api/Program.cs @@ -0,0 +1,59 @@ +using System.CommandLine; +using Electricity.Api; +using Electricity.Api.Services; +using Microsoft.EntityFrameworkCore; + +var rootCommand = new RootCommand("ElectricityServer is a small REST API to access the electricity log database"); +var connectionStringArgument = new Option( + name: "--connection-string", + description: "Filepath to the Sqlite3 database file (*.db)"); +rootCommand.AddOption(connectionStringArgument); + +var result = rootCommand.Parse(args); + +if (result.Errors.Any()) +{ + foreach (var error in result.Errors) + { + Console.WriteLine(error.Message); + } + return 1; +} + +var sqlite3DatabaseFilePath = result.GetValueForOption(connectionStringArgument); +if (!File.Exists(sqlite3DatabaseFilePath)) +{ + Console.WriteLine($"Sqlite3 database <{sqlite3DatabaseFilePath}> does not exist or is inaccessible"); + return 1; +} + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext((DbContextOptionsBuilder builder) => +{ + builder.UseSqlite($"Data Source={sqlite3DatabaseFilePath}"); +}); + +builder.Services.AddControllers() + .AddJsonOptions(config => + { + config.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + config.JsonSerializerOptions.WriteIndented = true; + }); +builder.Services.AddSwaggerGen(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + +return 0; \ No newline at end of file diff --git a/src/Electricity.Api/Properties/launchSettings.json b/src/Electricity.Api/Properties/launchSettings.json new file mode 100644 index 0000000..8181013 --- /dev/null +++ b/src/Electricity.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44706", + "sslPort": 44335 + } + }, + "profiles": { + "Electricity.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7290;http://localhost:5093", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Electricity.Api/Services/ElectricityService.cs b/src/Electricity.Api/Services/ElectricityService.cs new file mode 100644 index 0000000..7693e87 --- /dev/null +++ b/src/Electricity.Api/Services/ElectricityService.cs @@ -0,0 +1,66 @@ +using Electricity.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace Electricity.Api.Services; + +public class ElectricityService +{ + private readonly DatabaseContext databaseContext; + + public ElectricityService(DatabaseContext databaseContext) + { + this.databaseContext = databaseContext; + } + + public async Task GetDetailsFor(string date) + { + return (await databaseContext.ElectricityLogs + .Where(l => l.Date == date) + .OrderBy(l => l.TimeUtc) + .ToArrayAsync()) + .Select(Day.FromEntity) + .ToArray(); + } + + public async Task GetSummariesFor(string from, string to) + { + var list = new List(); + var current = DateOnly.ParseExact(from, Constants.isoDateFormat); + var limit = DateOnly.ParseExact(to, Constants.isoDateFormat); + do + { + list.Add(await GetSummaryFor(current)); + current = current.AddDays(1); + } while (current <= limit); + + return list.ToArray(); + } + + private async Task GetSummaryFor(DateOnly date) + { + var databaseDate = date.ToString(Constants.isoDateFormat); + var baseQuery = databaseContext.ElectricityLogs + .Where(l => l.Date == databaseDate) + .OrderBy(l => l.TimeUtc); + var first = await baseQuery.FirstOrDefaultAsync(); + if (first == null) + { + return Day.Empty(date); + } + var last = await baseQuery.LastAsync(); + + var firstAsModel = Day.FromEntity(first); + if (last.TimeUtc == first.TimeUtc) + { + return Day.Empty(date); + } + + var lastAsModel = Day.FromEntity(last); + return new Day( + lastAsModel.DateTime, + lastAsModel.TotalPowerUse - firstAsModel.TotalPowerUse, + lastAsModel.TotalPowerReturn - firstAsModel.TotalPowerReturn, + lastAsModel.TotalGasUse - firstAsModel.TotalGasUse + ); + } +} \ No newline at end of file diff --git a/src/Electricity.Api/appsettings.Development.json b/src/Electricity.Api/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/src/Electricity.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Electricity.Api/appsettings.json b/src/Electricity.Api/appsettings.json new file mode 100644 index 0000000..4d56694 --- /dev/null +++ b/src/Electricity.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Electricity.Api/publish.sh b/src/Electricity.Api/publish.sh new file mode 100755 index 0000000..b536630 --- /dev/null +++ b/src/Electricity.Api/publish.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +dotnet publish -c Release -r linux-arm --no-self-contained diff --git a/src/electricity-server/main.cpp b/src/electricity-server/main.cpp deleted file mode 100644 index 84ac38f..0000000 --- a/src/electricity-server/main.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include -#include -#include - -int main(int argc, char ** argv) -{ - TCLAP::CmdLine cmd( - "electricity-server is a small Pistache based HTTP content server with a REST API to access the solar log database", - ' ', - "1.0.0"); - - TCLAP::ValueArg - listeningPortArg("p", "listening-port", "TCP listening port number", true, 0u, "TCP listening port number"); - cmd.add(listeningPortArg); - - TCLAP::ValueArg logDirectoryPath( - "d", - "data-directory", - "Absolute path pointing to the logging directory", - true, - "", - "Absolute path pointing to the logging directory"); - cmd.add(logDirectoryPath); - - TCLAP::ValueArg serverDomain( - "s", - "server-domain", - "Domain this server is hosted on", - true, - "", - "Domain this server is hosted on"); - cmd.add(serverDomain); - - cmd.parse(argc, argv); - - auto & config = Server::Configuration::Get(); - config.Setup(logDirectoryPath.getValue(), serverDomain.getValue()); - - Pistache::Address address(Pistache::Ipv4::any(), listeningPortArg.getValue()); - Pistache::Http::Endpoint server(address); - - auto options = Pistache::Http::Endpoint::options().threads(2); - server.init(options); - - Pistache::Rest::Router router; - Server::Api::SetupRouting(router); - server.setHandler(router.handler()); - - server.serve(); -} \ No newline at end of file diff --git a/src/electricity-server/server/api.cpp b/src/electricity-server/server/api.cpp deleted file mode 100644 index c1de18a..0000000 --- a/src/electricity-server/server/api.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Server::Api -{ - using Pistache::Http::Mime::Subtype; - using Pistache::Http::Mime::Type; - - // Only allow serving to ourselves, all behind the same NAT address - bool IsRequesteeAllowed(Pistache::Http::Request const & request) - { - Configuration & config = Configuration::Get(); - auto const & serverAddress = config.GetExternalServerIp(); - - auto realIpHeader = request.headers().tryGetRaw("X-Real-IP"); - if(realIpHeader.isEmpty()) - { - spdlog::error("Blocking request without X-Real-IP header"); - return false; - } - - if(realIpHeader.unsafeGet().value() != serverAddress) - { - spdlog::info( - "Blocking request {} due to host mismatch (expected {})", - realIpHeader.unsafeGet().value(), - serverAddress); - return false; - } - - return true; - } - - void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite) - { - spdlog::info( - "{} {} {}", - Pistache::Http::methodString(request.method()), - request.resource(), - request.query().as_str()); - - if(!IsRequesteeAllowed(request)) - { - responseWrite.send(Pistache::Http::Code::Unauthorized); - return; - } - - auto const startQuery = request.query().get("start"); - if(startQuery.isEmpty()) - { - responseWrite.send(Pistache::Http::Code::Bad_Request); - return; - } - - Util::Date const startDate(startQuery.unsafeGet()); - if(!startDate.IsValid()) - { - responseWrite.send(Pistache::Http::Code::Bad_Request); - return; - } - - auto const stopQuery = request.query().get("stop"); - if(stopQuery.isEmpty()) - { - responseWrite.send( - Pistache::Http::Code::Ok, - Database::GetDetailedJsonOf(startDate), - MIME(Application, Json)); - return; - } - - Util::Date const stopDate(stopQuery.unsafeGet()); - if(!stopDate.IsValid() || stopDate.IsBefore(startDate)) - { - responseWrite.send(Pistache::Http::Code::Bad_Request); - return; - } - - responseWrite.send( - Pistache::Http::Code::Ok, - Database::GetSummaryJsonOf(startDate, stopDate), - MIME(Application, Json)); - } - - void SetupRouting(Pistache::Rest::Router & router) - { - Pistache::Rest::Routes::Get(router, "/day", Pistache::Rest::Routes::bind(&GetDay)); - } -} \ No newline at end of file diff --git a/src/electricity-server/server/configuration.cpp b/src/electricity-server/server/configuration.cpp deleted file mode 100644 index 3ffcba2..0000000 --- a/src/electricity-server/server/configuration.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -namespace Server -{ - Configuration::Configuration() : logDirectory() { } - - void Configuration::RefreshExternalIp() - { - addrinfo hints; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; - hints.ai_protocol = 0; - hints.ai_canonname = nullptr; - hints.ai_addr = nullptr; - hints.ai_next = nullptr; - - addrinfo * serverInfo; - auto const result = getaddrinfo(serverDomain.c_str(), "http", &hints, &serverInfo); - if(result) - { - spdlog::error( - "Error {} when parsing domain {} to determine external IP address", - gai_strerror(result), - serverDomain); - lastExternalIp.clear(); - return; - } - - char addressBuffer[INET_ADDRSTRLEN]; - sockaddr_in * socketAddress = reinterpret_cast(serverInfo->ai_addr); - if(inet_ntop(AF_INET, &socketAddress->sin_addr, addressBuffer, INET_ADDRSTRLEN) != nullptr) - { - lastIpCheckTimePoint = std::chrono::steady_clock::now(); - lastExternalIp = std::string(addressBuffer); - spdlog::info("External IP address {} cached", lastExternalIp); - } - else - { - auto const error = errno; - spdlog::error("Error {} returned by inet_ntop when trying to determine external IP address", error); - lastExternalIp.clear(); - } - - freeaddrinfo(serverInfo); - return; - } - - bool Configuration::ExternalIpRequiresRefresh() const - { - if(!std::regex_match(lastExternalIp, std::regex("^([0-9]+\\.){3}[0-9]+$"))) - { - return true; - } - - auto timeSinceLastRefresh = std::chrono::steady_clock::now() - lastIpCheckTimePoint; - return timeSinceLastRefresh >= std::chrono::minutes(5); - } - - void Configuration::Setup(std::string & electricityLogDirectory, std::string const & _serverDomain) - { - logDirectory = electricityLogDirectory; - if(electricityLogDirectory.size() > 0 && electricityLogDirectory[electricityLogDirectory.size() - 1] != '/') - { - logDirectory += '/'; - } - - serverDomain = _serverDomain; - } - - std::string const & Configuration::GetLogDirectory() const { return logDirectory; } - - std::string const & Configuration::GetExternalServerIp() - { - if(ExternalIpRequiresRefresh()) - { - std::lock_guard lock(externalIpRefreshMutex); - if(ExternalIpRequiresRefresh()) - { - RefreshExternalIp(); - } - } - - return lastExternalIp; - } - - Configuration & Configuration::Get() - { - static Configuration c; - return c; - } -} \ No newline at end of file diff --git a/src/electricity-server/server/database.cpp b/src/electricity-server/server/database.cpp deleted file mode 100644 index 48bdf0a..0000000 --- a/src/electricity-server/server/database.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -namespace Server::Database -{ - struct Record - { - private: - bool isValid; - - public: - long epoch; - double currentPowerUsage; - double totalPowerUsageDay; - double totalPowerUsageNight; - double currentPowerReturn; - double totalPowerReturnDay; - double totalPowerReturnNight; - double totalGasUsage; - - bool IsValid() const { return isValid; } - - void AppendAsJson(std::stringstream & ss) const - { - ss << "{\"dateTime\":" << epoch << ",\"totalPowerUse\":" << totalPowerUsageDay + totalPowerUsageNight - << ",\"totalPowerReturn\":" << totalPowerReturnDay + totalPowerReturnNight - << ",\"totalGasUse\":" << totalGasUsage << '}'; - } - - Record() - : isValid(false), currentPowerUsage(0.0), totalPowerUsageDay(0.0), totalPowerUsageNight(0.0), - currentPowerReturn(0.0), totalPowerReturnDay(0.0), totalPowerReturnNight(0.0), totalGasUsage(0.0) - { } - - Record(std::string const & line) - : isValid(false), currentPowerUsage(0.0), totalPowerUsageDay(0.0), totalPowerUsageNight(0.0), - currentPowerReturn(0.0), totalPowerReturnDay(0.0), totalPowerReturnNight(0.0), totalGasUsage(0.0) - { - std::vector values; - - char const delimiter = ','; - std::size_t previousIndex = 0; - while(true) - { - auto const commaIndex = line.find(delimiter, previousIndex + 1); - if(commaIndex != std::string::npos) - { - values.push_back(line.substr(previousIndex, commaIndex - previousIndex)); - previousIndex = commaIndex + 1; - } - else - { - break; - } - } - - if(previousIndex < line.size()) - { - values.push_back(line.substr(previousIndex)); - } - - if(values.size() != 10) - { - return; - } - - Util::Date const date(values[0]); - if(!date.IsValid()) - { - return; - } - int hours, minutes, seconds; - if(std::sscanf(values[0].substr(11).c_str(), "%d:%d:%d", &hours, &minutes, &seconds) != 3) - { - return; - } - - /* Fields are separated by comma's and represent: - 1 datetime - 2 currentPowerUsage (kw) - 3 totalPowerConsumptionDay (kwh) - 4 totalPowerConsumptionNight (kwh) - 5 currentPowerReturn (kw) - 6 totalPowerReturnedDay (kwh) - 7 totalPowerReturnedNight (kwh) - 8 tarif type (unused) - 9 gas timestamp (unused) - 10 gas consumption (M^3) - */ - epoch = date.ToEpoch() + hours * 3600 + minutes * 60 + seconds - 3600; - currentPowerUsage = std::atof(values[1].c_str()); - totalPowerUsageDay = std::atof(values[2].c_str()); - totalPowerUsageNight = std::atof(values[3].c_str()); - currentPowerReturn = std::atof(values[4].c_str()); - totalPowerReturnDay = std::atof(values[5].c_str()); - totalPowerReturnNight = std::atof(values[6].c_str()); - totalGasUsage = std::atof(values[9].c_str()); - - isValid = true; - } - }; - - std::string GetFileName(Util::Date const & date) - { - std::stringstream filePath; - filePath << Configuration::Get().GetLogDirectory() << date.Year() << '/' << std::setw(2) << std::setfill('0') - << date.Month() << '_' << std::setw(2) << date.Day() << ".txt"; - - return filePath.str(); - } - - std::vector GetFileContents(Util::Date const & date) - { - std::ifstream logFile(GetFileName(date)); - if(!logFile.is_open()) - { - return std::vector(); - } - - std::vector retval; - std::string line; - while(std::getline(logFile, line)) - { - Record record(line); - if(record.IsValid()) - { - retval.push_back(record); - } - } - logFile.close(); - - return retval; - } - - std::string GetDetailedJsonOf(Util::Date const & date) - { - auto const records = GetFileContents(date); - std::stringstream json; - json << '['; - - bool first = true; - for(std::size_t i = 0; i < records.size(); ++i) - { - if(!first) - { - json << ','; - } - first = false; - - records[i].AppendAsJson(json); - } - - json << ']'; - return json.str(); - } - - bool GetSummaryJsonOf(Util::Date const & date, bool const prependComma, std::stringstream & json) - { - if(prependComma) - { - json << ','; - } - - auto const records = GetFileContents(date); - if(records.size() < 2) - { - Record record; - record.epoch = date.ToEpoch(); - - record.AppendAsJson(json); - - return false; - } - - auto const firstRecord = records[0]; - auto lastRecord = records[records.size() - 1]; - lastRecord.totalPowerUsageDay -= firstRecord.totalPowerUsageDay; - lastRecord.totalPowerUsageNight -= firstRecord.totalPowerUsageNight; - lastRecord.totalPowerReturnDay -= firstRecord.totalPowerReturnDay; - lastRecord.totalPowerReturnNight -= firstRecord.totalPowerReturnNight; - lastRecord.totalGasUsage -= firstRecord.totalGasUsage; - - lastRecord.AppendAsJson(json); - - return true; - } - - std::string GetSummaryJsonOf(Util::Date const & startDate, Util::Date const & stopDate) - { - std::stringstream json; - json << '['; - - long const dayInSeconds = 24 * 60 * 60; - long const start = startDate.ToEpoch(); - long const stop = stopDate.ToEpoch(); - bool first = true; - for(long current = start; current <= stop; current += dayInSeconds) - { - GetSummaryJsonOf(Util::Date(current), !first, json); - first = false; - } - - json << ']'; - return json.str(); - } -} \ No newline at end of file diff --git a/systemd/electricity-server.service b/systemd/electricity-server.service index c932627..488dd0f 100644 --- a/systemd/electricity-server.service +++ b/systemd/electricity-server.service @@ -1,13 +1,15 @@ [Unit] -Description=A pistache based HTTP server serving the Electricity API +Description=A .NET Core application providing the Electricity REST API Requires=network.target After=network.target [Service] Type=simple -ExecStart=/usr/local/bin/electricity-server -d /mnt/data0/log/electricity -p 3002 -s electricity.valkendaal.duckdns.org +Environment=ASPNETCORE_URLS=http://*:5000 +ExecStart=/home/pi/.dotnet/dotnet /home/pi/bin/Electricity.Api/Electricity.Api.dll --connection-string /home/pi/logs/electricity.logs Restart=always RestartSec=30 +User=pi [Install] WantedBy=multi-user.target \ No newline at end of file