Rewrite electricity server to .NET web api
This commit is contained in:
2
src/Electricity.Api/.gitignore
vendored
Normal file
2
src/Electricity.Api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
obj/
|
||||
bin/
|
||||
6
src/Electricity.Api/Constants.cs
Normal file
6
src/Electricity.Api/Constants.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Electricity.Api;
|
||||
|
||||
internal static class Constants
|
||||
{
|
||||
public const string isoDateFormat = "yyyy-MM-dd";
|
||||
}
|
||||
45
src/Electricity.Api/Controllers/ElectricityLogController.cs
Normal file
45
src/Electricity.Api/Controllers/ElectricityLogController.cs
Normal file
@@ -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<ElectricityLogController> logger;
|
||||
|
||||
public ElectricityLogController(
|
||||
ElectricityService electricityService,
|
||||
ILogger<ElectricityLogController> logger)
|
||||
{
|
||||
this.electricityService = electricityService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet()]
|
||||
[Route("/day")]
|
||||
public async Task<ActionResult<Day[]>> 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);
|
||||
}
|
||||
}
|
||||
45
src/Electricity.Api/DatabaseContext.cs
Normal file
45
src/Electricity.Api/DatabaseContext.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Electricity.Api.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Electricity.Api
|
||||
{
|
||||
public partial class DatabaseContext : DbContext
|
||||
{
|
||||
public DatabaseContext()
|
||||
{
|
||||
}
|
||||
|
||||
public DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual DbSet<ElectricityLog> 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<ElectricityLog>(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);
|
||||
}
|
||||
}
|
||||
20
src/Electricity.Api/Electricity.Api.csproj
Normal file
20
src/Electricity.Api/Electricity.Api.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
16
src/Electricity.Api/Entities/ElectricityLog.cs
Normal file
16
src/Electricity.Api/Entities/ElectricityLog.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
36
src/Electricity.Api/Models/Day.cs
Normal file
36
src/Electricity.Api/Models/Day.cs
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
59
src/Electricity.Api/Program.cs
Normal file
59
src/Electricity.Api/Program.cs
Normal file
@@ -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<string>(
|
||||
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<DatabaseContext>((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<ElectricityService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
return 0;
|
||||
31
src/Electricity.Api/Properties/launchSettings.json
Normal file
31
src/Electricity.Api/Properties/launchSettings.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/Electricity.Api/Services/ElectricityService.cs
Normal file
66
src/Electricity.Api/Services/ElectricityService.cs
Normal file
@@ -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<Day[]> GetDetailsFor(string date)
|
||||
{
|
||||
return (await databaseContext.ElectricityLogs
|
||||
.Where(l => l.Date == date)
|
||||
.OrderBy(l => l.TimeUtc)
|
||||
.ToArrayAsync())
|
||||
.Select(Day.FromEntity)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<Day[]> GetSummariesFor(string from, string to)
|
||||
{
|
||||
var list = new List<Day>();
|
||||
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<Day> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/Electricity.Api/appsettings.Development.json
Normal file
8
src/Electricity.Api/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Electricity.Api/appsettings.json
Normal file
9
src/Electricity.Api/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
3
src/Electricity.Api/publish.sh
Executable file
3
src/Electricity.Api/publish.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
dotnet publish -c Release -r linux-arm --no-self-contained
|
||||
@@ -1,51 +0,0 @@
|
||||
#include <electricity/server/api.hpp>
|
||||
#include <electricity/server/configuration.hpp>
|
||||
#include <pistache/endpoint.h>
|
||||
#include <tclap/CmdLine.h>
|
||||
|
||||
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<unsigned>
|
||||
listeningPortArg("p", "listening-port", "TCP listening port number", true, 0u, "TCP listening port number");
|
||||
cmd.add(listeningPortArg);
|
||||
|
||||
TCLAP::ValueArg<std::string> logDirectoryPath(
|
||||
"d",
|
||||
"data-directory",
|
||||
"Absolute path pointing to the logging directory",
|
||||
true,
|
||||
"",
|
||||
"Absolute path pointing to the logging directory");
|
||||
cmd.add(logDirectoryPath);
|
||||
|
||||
TCLAP::ValueArg<std::string> 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();
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
#include <electricity/server/api.hpp>
|
||||
#include <electricity/server/configuration.hpp>
|
||||
#include <electricity/server/database.hpp>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sstream>
|
||||
#include <util/date.hpp>
|
||||
#include <vector>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <electricity/server/configuration.hpp>
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
#include <regex>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
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<sockaddr_in *>(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<std::mutex> lock(externalIpRefreshMutex);
|
||||
if(ExternalIpRequiresRefresh())
|
||||
{
|
||||
RefreshExternalIp();
|
||||
}
|
||||
}
|
||||
|
||||
return lastExternalIp;
|
||||
}
|
||||
|
||||
Configuration & Configuration::Get()
|
||||
{
|
||||
static Configuration c;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
#include <electricity/server/configuration.hpp>
|
||||
#include <electricity/server/database.hpp>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <util/date.hpp>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> 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<Record> GetFileContents(Util::Date const & date)
|
||||
{
|
||||
std::ifstream logFile(GetFileName(date));
|
||||
if(!logFile.is_open())
|
||||
{
|
||||
return std::vector<Record>();
|
||||
}
|
||||
|
||||
std::vector<Record> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user