Rewrite electricity server to .NET web api
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
build/
|
build/
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# ---> C++
|
# ---> C++
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <pistache/http.h>
|
|
||||||
#include <pistache/router.h>
|
|
||||||
|
|
||||||
namespace Server::Api
|
|
||||||
{
|
|
||||||
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite);
|
|
||||||
|
|
||||||
void SetupRouting(Pistache::Rest::Router & router);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <chrono>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace Server
|
|
||||||
{
|
|
||||||
class Configuration {
|
|
||||||
private:
|
|
||||||
std::string logDirectory;
|
|
||||||
std::string serverDomain;
|
|
||||||
std::string lastExternalIp;
|
|
||||||
std::chrono::time_point<std::chrono::steady_clock> 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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#include <string>
|
|
||||||
#include <util/date.hpp>
|
|
||||||
|
|
||||||
namespace Server::Database
|
|
||||||
{
|
|
||||||
std::string GetDetailedJsonOf(Util::Date const & date);
|
|
||||||
std::string GetSummaryJsonOf(Util::Date const & startDate, Util::Date const & stopDate);
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=A pistache based HTTP server serving the Electricity API
|
Description=A .NET Core application providing the Electricity REST API
|
||||||
Requires=network.target
|
Requires=network.target
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
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
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
|
User=pi
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user