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

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": "*"
}