Replace pistache solar-server with .net solar api
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
2
src/Solar.Api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
6
src/Solar.Api/Constants/Format.cs
Normal file
6
src/Solar.Api/Constants/Format.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Solar.Api.Constants;
|
||||||
|
|
||||||
|
internal static class Format
|
||||||
|
{
|
||||||
|
public const string Date = "yyyy-MM-dd";
|
||||||
|
}
|
||||||
125
src/Solar.Api/Controllers/SolarLogController.cs
Normal file
125
src/Solar.Api/Controllers/SolarLogController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Solar.Api/Converters/DateOnlyConverter.cs
Normal file
24
src/Solar.Api/Converters/DateOnlyConverter.cs
Normal 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));
|
||||||
|
}
|
||||||
24
src/Solar.Api/Converters/TimeOnlyConverter.cs
Normal file
24
src/Solar.Api/Converters/TimeOnlyConverter.cs
Normal 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));
|
||||||
|
}
|
||||||
63
src/Solar.Api/DatabaseContext.cs
Normal file
63
src/Solar.Api/DatabaseContext.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Solar.Api/Entities/EnvoyLog.cs
Normal file
12
src/Solar.Api/Entities/EnvoyLog.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Solar.Api/Entities/ZeverLog.cs
Normal file
11
src/Solar.Api/Entities/ZeverLog.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Solar.Api/Entities/ZeverSummary.cs
Normal file
9
src/Solar.Api/Entities/ZeverSummary.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/Solar.Api/Models/DayResponse.cs
Normal file
4
src/Solar.Api/Models/DayResponse.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record DayResponse(ZeverDayLog[] ZeverLogs, EnvoyDayLog[] EnvoyLogs);
|
||||||
3
src/Solar.Api/Models/DaySummaryLog.cs
Normal file
3
src/Solar.Api/Models/DaySummaryLog.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record DaySummaryLog(DateOnly Date, int ZeverTotalWatts, int EnvoyTotalWatts);
|
||||||
3
src/Solar.Api/Models/DaysResponse.cs
Normal file
3
src/Solar.Api/Models/DaysResponse.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record DaysResponse(DaySummaryLog[] DayLogs);
|
||||||
3
src/Solar.Api/Models/EnvoyDayLog.cs
Normal file
3
src/Solar.Api/Models/EnvoyDayLog.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record EnvoyDayLog(TimeOnly TimeUtc, int CurrentWatts, int TotalWatts);
|
||||||
3
src/Solar.Api/Models/MonthLog.cs
Normal file
3
src/Solar.Api/Models/MonthLog.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record MonthLog(int Year, int Month, int ZeverTotalWatts, int EnvoyTotalWatts);
|
||||||
3
src/Solar.Api/Models/MonthSummariesResponse.cs
Normal file
3
src/Solar.Api/Models/MonthSummariesResponse.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Solar.Api.Models;
|
||||||
|
|
||||||
|
public record MonthSummariesResponse(MonthLog[] MonthLogs);
|
||||||
3
src/Solar.Api/Models/ZeverDayLog.cs
Normal file
3
src/Solar.Api/Models/ZeverDayLog.cs
Normal 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
82
src/Solar.Api/Program.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/Solar.Api/Properties/launchSettings.json
Normal file
31
src/Solar.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: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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
src/Solar.Api/Services/SolarService.cs
Normal file
151
src/Solar.Api/Services/SolarService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Solar.Api/Solar.Api.csproj
Normal file
19
src/Solar.Api/Solar.Api.csproj
Normal 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>
|
||||||
8
src/Solar.Api/appsettings.Development.json
Normal file
8
src/Solar.Api/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Solar.Api/appsettings.json
Normal file
9
src/Solar.Api/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user