Replace pistache solar-server with .net solar api

This commit is contained in:
2022-12-28 19:16:26 +01:00
parent cb2aef0674
commit 756435ae2a
28 changed files with 599 additions and 469 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
build/ build/
bin/ bin/
samples/
.vscode/ .vscode/
!.vscode/settings.json !.vscode/settings.json

2
src/Solar.Api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/
obj/

View File

@@ -0,0 +1,6 @@
namespace Solar.Api.Constants;
internal static class Format
{
public const string Date = "yyyy-MM-dd";
}

View File

@@ -0,0 +1,125 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using Solar.Api.Models;
using Solar.Api.Services;
using Swashbuckle.AspNetCore.Annotations;
namespace Solar.Api.Controllers;
[ApiController]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
public class SolarLogController : ControllerBase
{
private readonly SolarService solarService;
private readonly ILogger<SolarLogController> logger;
public SolarLogController(
SolarService solarService,
ILogger<SolarLogController> logger)
{
this.solarService = solarService;
this.logger = logger;
}
[HttpGet()]
[Route("/day")]
public async Task<ActionResult<DayResponse>> GetDayDetails(
[SwaggerParameter(Required = true)]
[SwaggerSchema(Format = "date")]
[FromQuery]
string date)
{
var parsedDate = TryParseDate(date);
if (!parsedDate.HasValue || parsedDate.Value.Year < 2000 || parsedDate.Value.Year > 3000)
{
logger.LogInformation("Invalid date {Date} requested", date);
return BadRequest();
}
return await solarService.GetDayDetails(parsedDate.Value);
}
/// Parameter <parameter>to</parameter> is inclusive
[HttpGet()]
[Route("/days")]
public async Task<ActionResult<DaysResponse>> GetDaySummaries(
[SwaggerParameter(Required = true)]
[SwaggerSchema(Format = "date")]
[FromQuery]
string start,
[SwaggerParameter(Required = true)]
[SwaggerSchema(Format = "date")]
[FromQuery]
string stop)
{
var parsedStartDate = TryParseDate(start);
if (!parsedStartDate.HasValue || parsedStartDate.Value.Year < 2000 || parsedStartDate.Value.Year > 3000)
{
logger.LogInformation("Invalid start date {Date} requested", start);
return BadRequest();
}
var parsedStopDate = TryParseDate(stop);
if (!parsedStopDate.HasValue || parsedStopDate.Value.Year < 2000 || parsedStopDate.Value.Year > 3000)
{
logger.LogInformation("Invalid stop date {Date} requested", stop);
return BadRequest();
}
else if (parsedStopDate < parsedStartDate)
{
logger.LogInformation("Stop date {StopDate} must come before start date {StartDate} requested", stop, start);
return BadRequest();
}
return await solarService.GetDaySummaries(parsedStartDate.Value, parsedStopDate.Value);
}
[HttpGet()]
[Route("/months")]
public async Task<ActionResult<MonthSummariesResponse>> GetMonthSummaries(
[SwaggerParameter(Required = true)]
[SwaggerSchema(Format = "yyyy-MM")]
[FromQuery]
string start,
[SwaggerParameter(Required = true)]
[SwaggerSchema(Format = "yyyy-MM")]
[FromQuery]
string stop)
{
var parsedStartDate = TryParseDate($"{start}-01");
if (!parsedStartDate.HasValue || parsedStartDate.Value.Year < 2000 || parsedStartDate.Value.Year > 3000)
{
logger.LogInformation("Invalid start year month {YearMonth} requested", start);
return BadRequest();
}
var parsedStopDate = TryParseDate($"{stop}-01");
if (!parsedStopDate.HasValue || parsedStopDate.Value.Year < 2000 || parsedStopDate.Value.Year > 3000)
{
logger.LogInformation("Invalid stop year month {YearMonth} requested", stop);
return BadRequest();
}
else if (parsedStopDate < parsedStartDate)
{
logger.LogInformation("Stop year month {StopYearMonth} must come before start year month {StartYearMonth} requested", stop, start);
return BadRequest();
}
return await solarService.GetMonthSummaries(
parsedStartDate.Value.Year,
parsedStartDate.Value.Month,
parsedStopDate.Value.Year,
parsedStopDate.Value.Month);
}
private static DateOnly? TryParseDate(string value)
{
if (DateOnly.TryParseExact(value, "yyyy-MM-dd", out var date))
{
return date;
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Solar.Api.Converters;
public class DateOnlyConverter : JsonConverter<DateOnly>
{
private const string serializationFormat = "yyyy-MM-dd";
public override DateOnly Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
var value = reader.GetString();
return DateOnly.ParseExact(value!, serializationFormat);
}
public override void Write(
Utf8JsonWriter writer,
DateOnly value,
JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(serializationFormat));
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Solar.Api.Converters;
public class TimeOnlyConverter : JsonConverter<TimeOnly>
{
private const string serializationFormat = "HH:mm";
public override TimeOnly Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
var value = reader.GetString();
return TimeOnly.ParseExact(value!, serializationFormat);
}
public override void Write(
Utf8JsonWriter writer,
TimeOnly value,
JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(serializationFormat));
}

View File

@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore;
using Solar.Api.Entities;
namespace Solar.Api
{
public partial class DatabaseContext : DbContext
{
public DatabaseContext()
{
}
public DatabaseContext(DbContextOptions<DatabaseContext> options)
: base(options)
{
}
public virtual DbSet<EnvoyLog> EnvoyLogs { get; set; } = null!;
public virtual DbSet<ZeverLog> ZeverLogs { get; set; } = null!;
public virtual DbSet<ZeverSummary> ZeverSummaries { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EnvoyLog>(entity =>
{
entity.HasIndex(e => e.Date, "idx_EnvoyLogs_Date");
entity.HasIndex(e => e.TimeUtc, "idx_EnvoyLogs_TimeUtc");
entity.HasIndex(e => e.TotalWatts, "idx_EnvoyLogs_TotalWatts");
entity.Property(e => e.TotalWatts).HasColumnType("BIGINT");
});
modelBuilder.Entity<ZeverLog>(entity =>
{
entity.HasIndex(e => e.Date, "idx_ZeverLogs_Date");
entity.HasIndex(e => e.TimeUtc, "idx_ZeverLogs_TimeUtc");
entity.HasIndex(e => e.TotalWatts, "idx_ZeverLogs_TotalWatts");
entity.Property(e => e.TotalWatts).HasColumnType("BIGINT");
});
modelBuilder.Entity<ZeverSummary>(entity =>
{
entity.ToTable("ZeverSummary");
entity.HasIndex(e => e.Date, "IX_ZeverSummary_Date")
.IsUnique();
entity.HasIndex(e => e.Date, "idx_ZeverSummary_Date")
.IsUnique();
entity.Property(e => e.TotalWatts).HasColumnType("BIGINT");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}

View File

@@ -0,0 +1,12 @@
namespace Solar.Api.Entities
{
public partial class EnvoyLog
{
public long Id { get; set; }
public DateOnly Date { get; set; }
public TimeOnly TimeUtc { get; set; }
public long CurrentWatts { get; set; }
public long TotalWatts { get; set; }
public long Inverters { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace Solar.Api.Entities
{
public partial class ZeverLog
{
public long Id { get; set; }
public DateOnly Date { get; set; }
public TimeOnly TimeUtc { get; set; }
public long CurrentWatts { get; set; }
public long TotalWatts { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace Solar.Api.Entities
{
public partial class ZeverSummary
{
public long Id { get; set; }
public DateOnly Date { get; set; }
public long TotalWatts { get; set; }
}
}

View File

@@ -0,0 +1,4 @@
namespace Solar.Api.Models;
public record DayResponse(ZeverDayLog[] ZeverLogs, EnvoyDayLog[] EnvoyLogs);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record DaySummaryLog(DateOnly Date, int ZeverTotalWatts, int EnvoyTotalWatts);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record DaysResponse(DaySummaryLog[] DayLogs);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record EnvoyDayLog(TimeOnly TimeUtc, int CurrentWatts, int TotalWatts);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record MonthLog(int Year, int Month, int ZeverTotalWatts, int EnvoyTotalWatts);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record MonthSummariesResponse(MonthLog[] MonthLogs);

View File

@@ -0,0 +1,3 @@
namespace Solar.Api.Models;
public record ZeverDayLog(TimeOnly TimeUtc, int CurrentWatts, int TotalWatts);

82
src/Solar.Api/Program.cs Normal file
View File

@@ -0,0 +1,82 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Solar.Api.Converters;
using Solar.Api.Services;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Solar.Api;
public partial class Program
{
// TODO add launch parameters
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Services
builder.Services.AddScoped<SolarService>();
// Database
builder.Services.AddDbContext<DatabaseContext>(options =>
{
// TODO replace with launch argument
options.UseSqlite("Data Source=/home/tijmen/project/home-data-collection-tools/samples/solarpaneloutput.db");
});
// REST infrastructure
builder.Services.AddCors(options =>
{
options.AddPolicy(
name: "default",
policy =>
{
policy.WithOrigins("http://localhost:8080");
});
});
builder.Services.AddControllers().AddJsonOptions(config =>
{
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
config.JsonSerializerOptions.Converters.Add(new DateOnlyConverter());
config.JsonSerializerOptions.Converters.Add(new TimeOnlyConverter());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(config =>
{
config.MapType<DateOnly>(() => new OpenApiSchema
{
Type = "string",
Format = "date",
Example = OpenApiAnyFactory.CreateFromJson("\"2022-12-31\"")
});
config.MapType<TimeOnly>(() => new OpenApiSchema
{
Type = "string",
Format = "time",
Example = OpenApiAnyFactory.CreateFromJson("\"13:45:42.0000000\"")
});
config.SupportNonNullableReferenceTypes();
config.EnableAnnotations();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("default");
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}

View File

@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:16253",
"sslPort": 44321
}
},
"profiles": {
"Solar.Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7012;http://localhost:5199",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,151 @@
using Microsoft.EntityFrameworkCore;
using Solar.Api.Entities;
using Solar.Api.Models;
namespace Solar.Api.Services;
public class SolarService
{
private readonly DatabaseContext databaseContext;
private readonly ILogger<SolarService> logger;
public SolarService(
DatabaseContext databaseContext,
ILogger<SolarService> logger)
{
this.databaseContext = databaseContext;
this.logger = logger;
}
public async Task<DayResponse> GetDayDetails(DateOnly date)
{
var zeverRecords = await databaseContext
.ZeverLogs
.Where(zl => zl.Date == date)
.OrderBy(zl => zl.TimeUtc)
.ToArrayAsync();
var envoyRecords = await databaseContext
.EnvoyLogs
.Where(er => er.Date == date)
.OrderBy(er => er.TimeUtc)
.ToArrayAsync();
NormalizeEnvoyLogs(envoyRecords);
return new DayResponse(
zeverRecords
.Select(zr => new ZeverDayLog(
zr.TimeUtc,
(int)zr.CurrentWatts,
(int)zr.TotalWatts))
.ToArray(),
envoyRecords
.Select(er => new EnvoyDayLog(
er.TimeUtc,
(int)er.CurrentWatts,
(int)er.TotalWatts))
.ToArray());
}
public async Task<DaysResponse> GetDaySummaries(DateOnly start, DateOnly stop)
{
var zeverRecords = await databaseContext
.ZeverSummaries
.Where(zl => zl.Date >= start && zl.Date <= stop)
.ToArrayAsync();
List<DaySummaryLog> logs = new();
var current = start;
do
{
var zeverResult = zeverRecords.FirstOrDefault(zr => zr.Date == current)?.TotalWatts ?? 0;
var envoyResult = await GetEnvoyDayTotalWatts(current);
logs.Add(new DaySummaryLog(
current,
(int)zeverResult,
envoyResult));
current = current.AddDays(1);
} while (current <= stop);
return new DaysResponse(logs.ToArray());
}
public async Task<MonthSummariesResponse> GetMonthSummaries(int fromYear, int fromMonth, int toYear, int toMonth)
{
var results = new List<MonthLog>();
var current = (Year: fromYear, Month: fromMonth);
do
{
var zeverResult = (int)await databaseContext.ZeverSummaries
.Where(zs => zs.Date.Year == current.Year && zs.Date.Month == current.Month)
.SumAsync(zs => zs.TotalWatts);
var envoyYear = await GetEnvoyMonthTotalWatts(current.Year, current.Month);
results.Add(new MonthLog(current.Year, current.Month, zeverResult, envoyYear));
if (current.Month == 12)
{
current = (current.Year + 1, 1);
}
else
{
current = (current.Year, current.Month + 1);
}
} while (current.Year < toYear || current.Month < toMonth);
return new MonthSummariesResponse(results.ToArray());
}
private async Task<int> GetEnvoyDayTotalWatts(DateOnly date)
{
var min = await databaseContext.EnvoyLogs
.Where(el => el.Date == date)
.OrderBy(el => el.TotalWatts)
.FirstOrDefaultAsync();
var max = await databaseContext.EnvoyLogs
.Where(el => el.Date == date)
.OrderByDescending(el => el.TotalWatts)
.FirstOrDefaultAsync();
if (min == null || max == null)
{
return 0;
}
return (int)(max.TotalWatts - min.TotalWatts);
}
private async Task<int> GetEnvoyMonthTotalWatts(int year, int month)
{
var min = await databaseContext.EnvoyLogs
.Where(el => el.Date.Year == year && el.Date.Month == month)
.OrderBy(el => el.TotalWatts)
.FirstOrDefaultAsync();
var max = await databaseContext.EnvoyLogs
.Where(el => el.Date.Year == year && el.Date.Month == month)
.OrderByDescending(el => el.TotalWatts)
.FirstOrDefaultAsync();
if (min == null || max == null)
{
return 0;
}
return (int)(max.TotalWatts - min.TotalWatts);
}
private static void NormalizeEnvoyLogs(EnvoyLog[] entities)
{
if (entities.Length < 1)
{
return;
}
var baseValue = entities[0].TotalWatts;
foreach (var entity in entities)
{
entity.TotalWatts -= baseValue;
}
}
}

View File

@@ -0,0 +1,19 @@
<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.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.4.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,93 +0,0 @@
#include <solar/server/api.hpp>
#include <solar/server/configuration.hpp>
#include <util/date.hpp>
namespace Api
{
using Pistache::Http::Mime::Subtype;
using Pistache::Http::Mime::Type;
Util::Date ParseUtcDate(std::string const & date, Util::Date const & fallbackValue)
{
Util::Date result;
if(!result.TryParse(date))
{
return fallbackValue;
}
return result;
}
Util::Date ParseUtcDate(Pistache::Optional<std::string> const & date, Util::Date const & fallbackValue)
{
if(date.isEmpty() || date.unsafeGet().size() != 10)
{
return fallbackValue;
}
return ParseUtcDate(date.unsafeGet(), fallbackValue);
}
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite)
{
Util::Date const start = ParseUtcDate(request.query().get("start"), Util::Date::UtcNow());
auto const stopQuery = request.query().get("stop");
if(stopQuery.isEmpty())
{
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetEntireDay(start),
MIME(Application, Json));
return;
}
Util::Date const stop = ParseUtcDate(request.query().get("stop"), start);
if(!start.IsBefore(stop))
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetSummarizedPerDayRecords(start, stop),
MIME(Application, Json));
}
void GetMonth(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite)
{
auto const startQuery = request.query().get("start");
auto const stopQuery = request.query().get("stop");
if(startQuery.isEmpty() || stopQuery.isEmpty())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
auto const start = Util::Date(startQuery.unsafeGet());
auto stop = Util::Date(stopQuery.unsafeGet());
if(!start.IsValid() || !stop.IsValid())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
stop.SetDayToEndOfMonth();
if(stop.IsBefore(start))
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetSummarizedPerMonthRecords(start, stop),
MIME(Application, Json));
}
void SetupRouting(Pistache::Rest::Router & router)
{
Pistache::Rest::Routes::Get(router, "/day", Pistache::Rest::Routes::bind(&Api::GetDay));
Pistache::Rest::Routes::Get(router, "/month", Pistache::Rest::Routes::bind(&Api::GetMonth));
}
}

View File

@@ -1,24 +0,0 @@
#include <filesystem>
#include <solar/server/configuration.hpp>
#include <stdexcept>
Configuration::Configuration() : database() { }
Configuration & Configuration::SetupDatabase(std::string const & filePath)
{
if(!database.Connect(filePath))
{
throw std::runtime_error("Cannot open SQLite database at " + filePath);
}
return *this;
}
Database::Connection Configuration::GetDatabaseConnection() const { return database.GetConnection(); }
Configuration & Configuration::Get()
{
static Configuration c;
return c;
}

View File

@@ -1,254 +0,0 @@
#include <array>
#include <cmath>
#include <iomanip>
#include <solar/server/database/connection.hpp>
#include <spdlog/spdlog.h>
#include <sstream>
#include <vector>
namespace Database
{
Connection::Connection(sqlite3 * const databaseConnectionPtr) : connectionPtr(databaseConnectionPtr) { }
class JsonResult {
private:
long dateEpoch;
std::stringstream jsonStream;
unsigned insertions;
void Reset()
{
insertions = 0u;
jsonStream.clear();
jsonStream.str(std::string());
jsonStream << std::fixed << std::setprecision(2);
jsonStream << '[';
}
public:
bool AddRecord(long const epoch, char const * currentWatts, double const kwh)
{
++insertions;
jsonStream << "{\"time\":" << epoch << ",\"watt\":" << currentWatts << ",\"kwh\":" << kwh << "},";
return true;
}
bool AddRecord(long const epoch, char const * currentWatts, char const * totalWatts)
{
auto const totalWattsInt = std::atoi(totalWatts);
auto const kwh = static_cast<double>(totalWattsInt) / 1000;
return AddRecord(epoch, currentWatts, kwh);
}
bool AddRecord(char const * timeUtc, char const * currentWatts, char const * totalWatts)
{
auto const timeInSeconds = Util::ToSecondsFromTimeString(timeUtc);
if(timeInSeconds < 0)
{
spdlog::error("AddRecord: cannot parse {0} to hours, minutes and seconds", timeUtc);
return false;
}
return AddRecord(timeInSeconds + dateEpoch, currentWatts, totalWatts);
}
bool AddRecord(
unsigned const year,
unsigned const month,
unsigned const day,
char const * currentWatts,
char const * totalWatts)
{
return AddRecord(Util::GetEpoch(year, month, day), currentWatts, totalWatts);
}
// Returns the records added thus far as JSON array and resets
// itself to an empty array afterwards.
std::string GetJsonArray()
{
if(insertions)
{
// Replace last inserted comma
jsonStream.seekp(-1, std::ios_base::end);
jsonStream << ']';
}
else
{
jsonStream << ']';
}
auto const result = jsonStream.str();
Reset();
return result;
}
JsonResult(long _dateEpoch) : dateEpoch(_dateEpoch), jsonStream(), insertions(0u) { Reset(); }
};
int DetailedCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// TimeUtc, Watt and KilowattHour
if(argc != 3)
{
spdlog::error("DetailedCallback: unexpected number of arguments {0}", argc);
return -1;
}
JsonResult * result = reinterpret_cast<JsonResult *>(data);
result->AddRecord(argv[0], argv[1], argv[2]);
return 0;
}
std::string Connection::GetEntireDay(Util::Date const & date)
{
std::stringstream queryStream;
queryStream << "SELECT TimeUtc, CurrentWatts, TotalWatts FROM ZeverLogs WHERE Date = " << '\''
<< date.ToISOString() << '\'' << " ORDER BY TimeUtc ASC;";
JsonResult result(date.ToEpoch());
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), DetailedCallback, &result, nullptr);
if(sqlResult)
{
spdlog::error("GetEntireDay: SQLite error code {0}, returning empty JSON array", sqlResult);
return "[]";
}
return result.GetJsonArray();
}
int SummaryCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// Date and KilowattHour
if(argc != 2)
{
spdlog::error("SummaryCallback: unexpected number of arguments {0}", argc);
return -1;
}
JsonResult * result = reinterpret_cast<JsonResult *>(data);
Util::Date recordDate(argv[0]);
result->AddRecord(recordDate.Year(), recordDate.Month(), recordDate.Day(), "0", argv[1]);
return 0;
}
std::string Connection::GetSummarizedPerDayRecords(Util::Date const & startDate, Util::Date const & endDate)
{
std::stringstream queryStream;
queryStream << "SELECT Date, TotalWatts FROM ZeverSummary"
<< " WHERE Date >= '" << startDate.ToISOString() << '\'' << " AND Date <= '"
<< endDate.ToISOString() << '\'' << " ORDER BY Date;";
JsonResult result(0);
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), SummaryCallback, &result, nullptr);
if(sqlResult)
{
spdlog::error("GetSummarizedPerDayRecords: SQLite error code {0}, returning empty JSON array", sqlResult);
return "[]";
}
return result.GetJsonArray();
}
struct YearResult
{
int year;
std::array<int, 12> monthValues;
// month is in range 1 to 12
void AddMonthValue(int month, int value) { monthValues[month - 1] += value; }
YearResult(int _year) : year(_year), monthValues() { }
};
int MonthSummaryCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// Date and KilowattHour
if(argc != 2)
{
spdlog::error("MonthSummaryCallback: unexpected number of arguments {0}", argc);
return -1;
}
Util::Date recordDate;
if(!recordDate.TryParse(argv[0]))
{
spdlog::error("MonthSummaryCallback: error parsing date {0}", argv[0]);
return -1;
}
std::vector<YearResult> & yearResults = *reinterpret_cast<std::vector<YearResult> *>(data);
auto const totalWatts = std::atoi(argv[1]);
if(std::isnan(totalWatts) || totalWatts < 0)
{
// This value makes no sense, ignore it
spdlog::warn("MonthSummaryCallback: ignoring bogus value for year month {0}", argv[1]);
return 0;
}
for(std::size_t i = 0; i < yearResults.size(); ++i)
{
if(yearResults[i].year == recordDate.Year())
{
yearResults[i].AddMonthValue(recordDate.Month(), totalWatts);
return 0;
}
if(yearResults[i].year > recordDate.Year())
{
yearResults.insert(yearResults.begin() + i, YearResult(recordDate.Year()));
yearResults[i].AddMonthValue(recordDate.Month(), totalWatts);
return 0;
}
}
yearResults.push_back(YearResult(recordDate.Year()));
yearResults[yearResults.size() - 1].AddMonthValue(recordDate.Month(), totalWatts);
return 0;
}
std::string Connection::GetSummarizedPerMonthRecords(Util::Date const & startDate, Util::Date const & endDate)
{
std::stringstream queryStream;
queryStream << "SELECT Date, TotalWatts FROM ZeverSummary"
<< " WHERE Date >= '" << startDate.ToISOString() << '\'' << " AND Date <= '"
<< endDate.ToISOString() << "';";
std::vector<YearResult> yearResults;
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), MonthSummaryCallback, &yearResults, nullptr);
if(sqlResult || yearResults.size() == 0)
{
spdlog::error(
"GetSummarizedPerMonthRecords: SQLite return code {0} and {0} years retrieved, returning empty JSON array",
sqlResult,
yearResults.size());
return "[]";
}
JsonResult result(0);
for(std::size_t i = 0; i < yearResults.size(); ++i)
{
auto const year = yearResults[i].year;
for(int month = 0; month < yearResults[i].monthValues.size(); ++month)
{
if(startDate.IsAfter(year, month + 1, 1) || endDate.IsBefore(year, month + 1, 1))
{
continue;
}
auto const epoch = Util::GetEpoch(year, month + 1, 1);
auto const totalWatts = yearResults[i].monthValues[month];
result.AddRecord(epoch, "0", totalWatts);
}
}
return result.GetJsonArray();
}
}

View File

@@ -1,33 +0,0 @@
#include <solar/server/database/database.hpp>
namespace Database
{
bool Database::Connect(std::string const & path)
{
if(connectionPtr)
{
// Already connected
return true;
}
if(sqlite3_open(path.c_str(), &connectionPtr))
{
return false;
}
sqlite3_extended_result_codes(connectionPtr, 1);
return true;
}
Connection Database::GetConnection() const { return Connection(connectionPtr); }
Database::Database() : connectionPtr(nullptr) { }
Database::~Database()
{
if(connectionPtr)
{
sqlite3_close(connectionPtr);
}
}
}

View File

@@ -1,65 +0,0 @@
#include <cxxopts.hpp>
#include <optional>
#include <pistache/endpoint.h>
#include <solar/server/api.hpp>
#include <solar/server/configuration.hpp>
#include <spdlog/spdlog.h>
std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
{
cxxopts::Options options(
"solar-server",
"solar-server is a small Pistache based HTTP content server with a REST API to access the solar log database");
options.add_options()(
"p,listening-port",
"TCP port to listen on for REST API requests.",
cxxopts::value<unsigned>())(
"connection-string",
"Path to the sqlite3 database file",
cxxopts::value<std::string>());
if(argc == 1)
{
std::cout << options.help() << std::endl;
return {};
}
try
{
auto const parsed = options.parse(argc, argv);
return parsed;
}
catch(cxxopts::OptionException const & e)
{
spdlog::error(e.what());
return {};
}
}
int main(int argc, char ** argv)
{
auto const maybeArgs = ExtractArgs(argc, argv);
if(!maybeArgs.has_value())
{
return 1;
}
auto const & args = maybeArgs.value();
Configuration & config = Configuration::Get();
config.SetupDatabase(args["connection-string"].as<std::string>());
Pistache::Address address(Pistache::Ipv4::any(), args["listening-port"].as<unsigned>());
Pistache::Http::Endpoint server(address);
auto options = Pistache::Http::Endpoint::options().threads(2);
server.init(options);
Pistache::Rest::Router router;
Api::SetupRouting(router);
server.setHandler(router.handler());
spdlog::info("solar-server listening on localhost:{0}", args["listening-port"].as<unsigned>());
server.serve();
}