Compare commits
23 Commits
developmen
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e756e6f972 | |||
| c35ec6e7d7 | |||
| 7c93c980bc | |||
| 2a725a1a90 | |||
| 84231b5ef7 | |||
| 6f3e0629c5 | |||
| 05a0974ce8 | |||
| e56a8b0ccf | |||
| 90544b5b07 | |||
| 89623f26db | |||
| 620bb57e6d | |||
| ce19d05260 | |||
| 5e2985212b | |||
| 363cefa00f | |||
| fffd611a52 | |||
| 4166a05a7f | |||
| 3192a5bc83 | |||
| eccbd6439b | |||
| af19ab7c71 | |||
| 42155ac748 | |||
| 9edb3d17de | |||
| b6bc2ae25c | |||
| eead83fdcd |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
build/
|
build/
|
||||||
bin/
|
bin/
|
||||||
samples/
|
samples/
|
||||||
|
target/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
@@ -40,3 +41,7 @@ samples/
|
|||||||
*.out
|
*.out
|
||||||
*.app
|
*.app
|
||||||
|
|
||||||
|
# Sqlite database files
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"llvm-vs-code-extensions.vscode-clangd",
|
"llvm-vs-code-extensions.vscode-clangd",
|
||||||
|
"ms-dotnettools.csharp",
|
||||||
|
"csharpier.csharpier-vscode",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
1497
Cargo.lock
generated
Normal file
1497
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = ["src/electricity_api", "src/shared_api_lib", "src/solar_api"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
chrono = "0.4.41"
|
||||||
|
clap = "4.5.46"
|
||||||
|
colog = "1.3.0"
|
||||||
|
log = "0.4.27"
|
||||||
|
rusqlite = "0.37.0"
|
||||||
|
serde = "1.0.219"
|
||||||
|
serde_json = "1.0.143"
|
||||||
|
shared_api_lib = { path = "../shared_api_lib" }
|
||||||
|
tokio = "1"
|
||||||
|
warp = "0.4"
|
||||||
@@ -14,10 +14,10 @@ A mono repository containing all the homebrew collectors and REST APIs running o
|
|||||||
|
|
||||||
### Runtime (server)
|
### Runtime (server)
|
||||||
|
|
||||||
- .NET 6, [website](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
|
|
||||||
- Curl: the multiprotocol file tranfser library, [website](https://curl.haxx.se/libcurl/)
|
- Curl: the multiprotocol file tranfser library, [website](https://curl.haxx.se/libcurl/)
|
||||||
- GNU Make, [website](https://www.gnu.org/software/make/)
|
- GNU Make, [website](https://www.gnu.org/software/make/)
|
||||||
- Libfmt, [website](https://github.com/fmtlib/fmt)
|
- Libfmt, [website](https://github.com/fmtlib/fmt)
|
||||||
|
- Rust, [website](https://www.rust-lang.org/learn/get-started)
|
||||||
- Spdlog, [website](https://github.com/gabime/spdlog)
|
- Spdlog, [website](https://github.com/gabime/spdlog)
|
||||||
- Sqlite3: a small SQL database engine, v3.39.2, [website](https://www.sqlite.org/index.html)
|
- Sqlite3: a small SQL database engine, v3.39.2, [website](https://www.sqlite.org/index.html)
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ In addition to all the runtime dependencies the following dependencies are requi
|
|||||||
|
|
||||||
- A C++20 compatible compiler
|
- A C++20 compatible compiler
|
||||||
- Nlohmann JSON, [website](https://github.com/nlohmann/json/tree/v3.11.2)
|
- Nlohmann JSON, [website](https://github.com/nlohmann/json/tree/v3.11.2)
|
||||||
- cxxopts, [website](https://github.com/jarro2783/cxxopts/tree/v3.0.0)
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
2
src/Electricity.Api/.gitignore
vendored
2
src/Electricity.Api/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
obj/
|
|
||||||
bin/
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Electricity.Api;
|
|
||||||
|
|
||||||
internal static class Constants
|
|
||||||
{
|
|
||||||
public const string isoDateFormat = "yyyy-MM-dd";
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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<Minute[]>> Get([FromQuery] string? date)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(date) || !DateOnly.TryParseExact(date, Constants.isoDateFormat, out _))
|
|
||||||
{
|
|
||||||
return BadDateParameter(nameof(date));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await electricityService.GetDetailsFor(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet()]
|
|
||||||
[Route("/days")]
|
|
||||||
public async Task<ActionResult<Day[]>> Get([FromQuery] string? start, [FromQuery] string? stop)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(start) || !DateOnly.TryParseExact(start, Constants.isoDateFormat, out _))
|
|
||||||
{
|
|
||||||
return BadDateParameter(nameof(start));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(stop) || !DateOnly.TryParseExact(stop, Constants.isoDateFormat, out _))
|
|
||||||
{
|
|
||||||
return BadDateParameter(nameof(stop));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Compare(start, stop) > 0)
|
|
||||||
{
|
|
||||||
return BadRequest($"The date argument of {nameof(stop)} must be greater or equal to the argument value of {nameof(start)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await electricityService.GetSummariesFor(start, stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BadRequestObjectResult BadDateParameter(string parameterName) =>
|
|
||||||
BadRequest($"The search parameter {parameterName} is missing or is not a valid ISO date ({Constants.isoDateFormat}).");
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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; }
|
|
||||||
|
|
||||||
public DateTime GetDateTime() => DateTime
|
|
||||||
.Parse(
|
|
||||||
$"{Date}T{TimeUtc}Z",
|
|
||||||
null,
|
|
||||||
System.Globalization.DateTimeStyles.AssumeUniversal)
|
|
||||||
.ToUniversalTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Electricity.Api.Extensions;
|
|
||||||
|
|
||||||
public static class DateTimeExtensions
|
|
||||||
{
|
|
||||||
public static uint ToEpoch(this DateTime dateTime) =>
|
|
||||||
(uint)Math.Round((dateTime - DateTime.UnixEpoch).TotalSeconds);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Electricity.Api.Extensions;
|
|
||||||
|
|
||||||
namespace Electricity.Api.Models;
|
|
||||||
|
|
||||||
public record Day(string Date, double TotalPowerUse, double TotalPowerReturn, double TotalGasUse)
|
|
||||||
{
|
|
||||||
public static Day Empty(DateOnly date)
|
|
||||||
{
|
|
||||||
return new Day(
|
|
||||||
date.ToString(Constants.isoDateFormat),
|
|
||||||
0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Day FromEntity(Entities.ElectricityLog entity)
|
|
||||||
{
|
|
||||||
return new Day(
|
|
||||||
Date: entity.Date,
|
|
||||||
TotalPowerUse: entity.TotalPowerConsumptionDay + entity.TotalPowerConsumptionNight,
|
|
||||||
TotalPowerReturn: entity.TotalPowerReturnDay + entity.TotalPowerReturnNight,
|
|
||||||
TotalGasUse: entity.GasConsumptionInCubicMeters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using Electricity.Api.Extensions;
|
|
||||||
|
|
||||||
namespace Electricity.Api.Models;
|
|
||||||
|
|
||||||
public record Minute(uint DateTime, double CurrentPowerUsage, double TotalPowerUse, double TotalPowerReturn, double TotalGasUse)
|
|
||||||
{
|
|
||||||
public static Minute Empty(DateOnly date)
|
|
||||||
{
|
|
||||||
return new Minute(
|
|
||||||
date.ToDateTime(new TimeOnly(0, 0), DateTimeKind.Utc).ToEpoch(),
|
|
||||||
0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Minute FromEntity(Entities.ElectricityLog entity)
|
|
||||||
{
|
|
||||||
return new Minute(
|
|
||||||
DateTime: entity.GetDateTime().ToEpoch(),
|
|
||||||
CurrentPowerUsage: entity.CurrentPowerUsage,
|
|
||||||
TotalPowerUse: entity.TotalPowerConsumptionDay + entity.TotalPowerConsumptionNight,
|
|
||||||
TotalPowerReturn: entity.TotalPowerReturnDay + entity.TotalPowerReturnNight,
|
|
||||||
TotalGasUse: entity.GasConsumptionInCubicMeters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy(
|
|
||||||
name: "default",
|
|
||||||
policy =>
|
|
||||||
{
|
|
||||||
policy.WithOrigins("http://localhost:8080");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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.UseHttpsRedirection();
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
app.UseCors("default");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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<Minute[]> GetDetailsFor(string date)
|
|
||||||
{
|
|
||||||
return (await databaseContext.ElectricityLogs
|
|
||||||
.Where(l => l.Date == date)
|
|
||||||
.OrderBy(l => l.TimeUtc)
|
|
||||||
.ToArrayAsync())
|
|
||||||
.Select(Minute.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.Date,
|
|
||||||
lastAsModel.TotalPowerUse - firstAsModel.TotalPowerUse,
|
|
||||||
lastAsModel.TotalPowerReturn - firstAsModel.TotalPowerReturn,
|
|
||||||
lastAsModel.TotalGasUse - firstAsModel.TotalGasUse
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
dotnet publish -c Release -r linux-arm --no-self-contained
|
|
||||||
2
src/Solar.Api/.gitignore
vendored
2
src/Solar.Api/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
bin/
|
|
||||||
obj/
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Solar.Api.Constants;
|
|
||||||
|
|
||||||
internal static class CacheKeys
|
|
||||||
{
|
|
||||||
public static string GetMonthSummaryCacheKey(string source, int year, int month)
|
|
||||||
{
|
|
||||||
return $"month-summary-{source}-{year}-{month}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Solar.Api.Constants;
|
|
||||||
|
|
||||||
internal static class Format
|
|
||||||
{
|
|
||||||
public const string Date = "yyyy-MM-dd";
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Solar.Api.Models;
|
|
||||||
using Solar.Api.Services;
|
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
|
||||||
using System.Net.Mime;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Solar.Api.Entities
|
|
||||||
{
|
|
||||||
public partial class ZeverSummary
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public DateOnly Date { get; set; }
|
|
||||||
public long TotalWatts { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record DayResponse(ZeverDayLog[] ZeverLogs, EnvoyDayLog[] EnvoyLogs);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record DaySummaryLog(DateOnly Date, int ZeverTotalWatts, int EnvoyTotalWatts);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record DaysResponse(DaySummaryLog[] DayLogs);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record EnvoyDayLog(TimeOnly TimeUtc, int CurrentWatts, int TotalWatts);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record MonthLog(int Year, int Month, int ZeverTotalWatts, int EnvoyTotalWatts);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record MonthSummariesResponse(MonthLog[] MonthLogs);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Solar.Api.Models;
|
|
||||||
|
|
||||||
public record ZeverDayLog(TimeOnly TimeUtc, int CurrentWatts, int TotalWatts);
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Solar.Api.Converters;
|
|
||||||
using Solar.Api.Services;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Solar.Api;
|
|
||||||
|
|
||||||
public partial class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
// Services
|
|
||||||
builder.Services.AddScoped<SolarService>();
|
|
||||||
builder.Services.AddMemoryCache();
|
|
||||||
|
|
||||||
// Database
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>(options =>
|
|
||||||
{
|
|
||||||
options.UseSqlite(builder.Configuration.GetConnectionString("Database"));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Solar.Api.Constants;
|
|
||||||
using Solar.Api.Entities;
|
|
||||||
using Solar.Api.Models;
|
|
||||||
|
|
||||||
namespace Solar.Api.Services;
|
|
||||||
|
|
||||||
public class SolarService
|
|
||||||
{
|
|
||||||
private readonly DatabaseContext databaseContext;
|
|
||||||
private readonly IMemoryCache memoryCache;
|
|
||||||
private readonly ILogger<SolarService> logger;
|
|
||||||
|
|
||||||
public SolarService(
|
|
||||||
DatabaseContext databaseContext,
|
|
||||||
IMemoryCache memoryCache,
|
|
||||||
ILogger<SolarService> logger)
|
|
||||||
{
|
|
||||||
this.databaseContext = databaseContext;
|
|
||||||
this.memoryCache = memoryCache;
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void NormalizeEnvoyLogs(EnvoyLog[] entities)
|
|
||||||
{
|
|
||||||
if (entities.Length < 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseValue = entities[0].TotalWatts;
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
entity.TotalWatts -= baseValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = await GetOrAddToCache("zever", current.Year, current.Month, GetZeverMonthTotalWatts);
|
|
||||||
var envoyYear = await GetOrAddToCache("envoy", current.Year, current.Month, GetEnvoyMonthTotalWatts);
|
|
||||||
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> GetZeverMonthTotalWatts(int year, int month)
|
|
||||||
{
|
|
||||||
return (int)await databaseContext.ZeverSummaries
|
|
||||||
.Where(zs => zs.Date.Year == year && zs.Date.Month == month)
|
|
||||||
.SumAsync(zs => zs.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 async Task<int> GetOrAddToCache(
|
|
||||||
string source,
|
|
||||||
int year,
|
|
||||||
int month,
|
|
||||||
Func<int, int, Task<int>> fetchMethod)
|
|
||||||
{
|
|
||||||
return await memoryCache.GetOrCreateAsync(
|
|
||||||
CacheKeys.GetMonthSummaryCacheKey(source, year, month),
|
|
||||||
async (cacheEntry) =>
|
|
||||||
{
|
|
||||||
if (DateTime.Today.Month == month && DateTime.Today.Year == year)
|
|
||||||
{
|
|
||||||
cacheEntry.SlidingExpiration = TimeSpan.FromHours(12);
|
|
||||||
}
|
|
||||||
return await fetchMethod(year, month);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Database": "Data Source=/home/tijmen/project/home-data-collection-tools/samples/solarpaneloutput.db"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Database": "Data Source=/home/tijmen/project/home-data-collection-tools/samples/solarpaneloutput.db"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#include <electricity/logger/database.hpp>
|
#include <electricity/logger/database.hpp>
|
||||||
#include <exception>
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|||||||
@@ -4,12 +4,32 @@
|
|||||||
#include <electricity/logger/serialport.hpp>
|
#include <electricity/logger/serialport.hpp>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
|
struct CommandlineParameters
|
||||||
|
{
|
||||||
|
CommandlineParameters(std::string const & serialDevicePath, std::string const & databaseConnectionString)
|
||||||
|
: DeviceType(serialDevicePath), DatabaseConnectionString(databaseConnectionString) { };
|
||||||
|
|
||||||
|
std::string DeviceType;
|
||||||
|
std::string DatabaseConnectionString;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<std::string> TryGetStringArgument(cxxopts::ParseResult const & parseResult, std::string const & name)
|
||||||
|
{
|
||||||
|
if(!parseResult.contains(name))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResult[name].as_optional<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<CommandlineParameters> TryGetCommandlineArguments(int argc, char ** argv)
|
||||||
{
|
{
|
||||||
cxxopts::Options options(
|
cxxopts::Options options(
|
||||||
"electricity-logger",
|
"electricity-logger",
|
||||||
@@ -23,22 +43,25 @@ std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
|
|||||||
"Path to the sqlite3 database file",
|
"Path to the sqlite3 database file",
|
||||||
cxxopts::value<std::string>());
|
cxxopts::value<std::string>());
|
||||||
|
|
||||||
if(argc == 1)
|
|
||||||
{
|
|
||||||
std::cout << options.help() << std::endl;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto const parsed = options.parse(argc, argv);
|
auto const parseResult = options.parse(argc, argv);
|
||||||
return parsed;
|
auto const serialDevicePath = TryGetStringArgument(parseResult, "serial-device");
|
||||||
|
auto const databaseConnectionString = TryGetStringArgument(parseResult, "connection-string");
|
||||||
|
|
||||||
|
if(serialDevicePath.has_value() && databaseConnectionString.has_value())
|
||||||
|
{
|
||||||
|
return CommandlineParameters(serialDevicePath.value(), databaseConnectionString.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(cxxopts::OptionException const & e)
|
catch(cxxopts::exceptions::exception const & e)
|
||||||
{
|
{
|
||||||
spdlog::error(e.what());
|
spdlog::error(e.what());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::cout << options.help() << std::endl;
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
DSMR::Data ReadData(std::string const & devicePath)
|
DSMR::Data ReadData(std::string const & devicePath)
|
||||||
@@ -99,18 +122,15 @@ DSMR::Data ReadData(std::string const & devicePath)
|
|||||||
|
|
||||||
int main(int argc, char ** argv)
|
int main(int argc, char ** argv)
|
||||||
{
|
{
|
||||||
auto const maybeArgs = ExtractArgs(argc, argv);
|
auto const parameters = TryGetCommandlineArguments(argc, argv);
|
||||||
if(!maybeArgs.has_value())
|
if(!parameters.has_value())
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const & args = maybeArgs.value();
|
auto const data = ReadData(parameters->DeviceType);
|
||||||
|
|
||||||
auto const data = ReadData(args["serial-device"].as<std::string>());
|
Database db(parameters->DatabaseConnectionString);
|
||||||
|
|
||||||
auto const connectionStringValue = args["connection-string"].as<std::string>();
|
|
||||||
Database db(connectionStringValue);
|
|
||||||
db.Insert(data, std::time(nullptr));
|
db.Insert(data, std::time(nullptr));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
842
src/electricity_api/Cargo.lock
generated
Normal file
842
src/electricity_api/Cargo.lock
generated
Normal file
@@ -0,0 +1,842 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.24.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.75"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "electricity_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
"warp",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.31.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-uring"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.175"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.36.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.143"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.47.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"bytes",
|
||||||
|
"io-uring",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "warp"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"headers",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"scoped-tls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
16
src/electricity_api/Cargo.toml
Normal file
16
src/electricity_api/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "electricity_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
colog = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
rusqlite = { workspace = true, features = ["bundled"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
shared_api_lib = { path = "../shared_api_lib" }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
warp = { workspace = true, features = ["server"] }
|
||||||
331
src/electricity_api/src/database.rs
Normal file
331
src/electricity_api/src/database.rs
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
use shared_api_lib::year_month::YearMonth;
|
||||||
|
|
||||||
|
pub fn get_day_power_entities(date: &chrono::NaiveDate, database_path: &str) -> Vec<LogEntity> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT
|
||||||
|
\"Date\",
|
||||||
|
\"TimeUtc\",
|
||||||
|
\"CurrentPowerUsage\",
|
||||||
|
\"TotalPowerConsumptionDay\",
|
||||||
|
\"TotalPowerConsumptionNight\",
|
||||||
|
\"CurrentPowerReturn\",
|
||||||
|
\"TotalPowerReturnDay\",
|
||||||
|
\"TotalPowerReturnNight\",
|
||||||
|
\"DayTarifEnabled\",
|
||||||
|
\"GasConsumptionInCubicMeters\"
|
||||||
|
FROM \"ElectricityLog\"
|
||||||
|
WHERE \"Date\" = :date
|
||||||
|
ORDER BY \"Date\", \"TimeUtc\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepate database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_date = format!("{}", date);
|
||||||
|
log::info!("Fetching day power data for date {}", formatted_date);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(&[(":date", &formatted_date)], |row| {
|
||||||
|
Ok(LogEntity {
|
||||||
|
// date: row.get(0)?,
|
||||||
|
time_utc: row.get(1)?,
|
||||||
|
current_power_usage: row.get(2)?,
|
||||||
|
total_power_consumption_day: row.get(3)?,
|
||||||
|
total_power_consumption_night: row.get(4)?,
|
||||||
|
// current_power_return: row.get(5)?,
|
||||||
|
total_power_return_day: row.get(6)?,
|
||||||
|
total_power_return_night: row.get(7)?,
|
||||||
|
// day_tarif_enabled: row.get(8)?,
|
||||||
|
gas_consumption_in_cubic_meters: row.get(9)?,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => {
|
||||||
|
items.push(entity);
|
||||||
|
}
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute day power data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogEntity {
|
||||||
|
// pub date: String,
|
||||||
|
pub time_utc: String,
|
||||||
|
pub current_power_usage: f64,
|
||||||
|
pub total_power_consumption_day: f64,
|
||||||
|
pub total_power_consumption_night: f64,
|
||||||
|
// pub current_power_return: f64,
|
||||||
|
pub total_power_return_day: f64,
|
||||||
|
pub total_power_return_night: f64,
|
||||||
|
// pub day_tarif_enabled: u8,
|
||||||
|
pub gas_consumption_in_cubic_meters: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_day_power_summaries(
|
||||||
|
start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<LogDateSummary> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"WITH MaximumValuesPerDate AS (
|
||||||
|
SELECT \"Date\", MAX(\"TimeUtc\") AS \"TimeUtc\"
|
||||||
|
FROM \"ElectricityLog\"
|
||||||
|
WHERE
|
||||||
|
\"Date\" >= :start AND
|
||||||
|
\"Date\" <= :stop
|
||||||
|
GROUP BY \"Date\"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
l.\"Date\",
|
||||||
|
\"TotalPowerConsumptionDay\",
|
||||||
|
\"TotalPowerConsumptionNight\",
|
||||||
|
\"TotalPowerReturnDay\",
|
||||||
|
\"TotalPowerReturnNight\",
|
||||||
|
\"GasConsumptionInCubicMeters\"
|
||||||
|
FROM \"ElectricityLog\" l
|
||||||
|
JOIN MaximumValuesPerDate m ON
|
||||||
|
m.\"Date\" = l.\"Date\" AND
|
||||||
|
m.\"TimeUtc\" = l.\"TimeUtc\"
|
||||||
|
ORDER BY l.\"Date\", l.\"TimeUtc\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepate database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_start = format!("{}", start);
|
||||||
|
let formatted_stop = format!("{}", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching day power data for date range {} to {}",
|
||||||
|
formatted_start,
|
||||||
|
formatted_stop
|
||||||
|
);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(
|
||||||
|
&[(":start", &formatted_start), (":stop", &formatted_stop)],
|
||||||
|
|row| {
|
||||||
|
Ok(LogDateSummary {
|
||||||
|
date: row.get(0)?,
|
||||||
|
total_power_consumption_day: row.get(1)?,
|
||||||
|
total_power_consumption_night: row.get(2)?,
|
||||||
|
total_power_return_day: row.get(3)?,
|
||||||
|
total_power_return_night: row.get(4)?,
|
||||||
|
gas_consumption_in_cubic_meters: row.get(5)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => {
|
||||||
|
items.push(entity);
|
||||||
|
}
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute day power data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogDateSummary {
|
||||||
|
pub date: String,
|
||||||
|
pub total_power_consumption_day: f64,
|
||||||
|
pub total_power_consumption_night: f64,
|
||||||
|
pub total_power_return_day: f64,
|
||||||
|
pub total_power_return_night: f64,
|
||||||
|
pub gas_consumption_in_cubic_meters: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_month_power_summaries(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<LogMonthSummary> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT
|
||||||
|
STRFTIME('%Y-%m', \"Date\") AS YearMonth,
|
||||||
|
MAX(TotalPowerConsumptionDay),
|
||||||
|
MIN(TotalPowerConsumptionDay),
|
||||||
|
MAX(TotalPowerConsumptionNight),
|
||||||
|
MIN(TotalPowerConsumptionNight),
|
||||||
|
MAX(TotalPowerReturnDay),
|
||||||
|
MIN(TotalPowerReturnDay),
|
||||||
|
MAX(TotalPowerReturnNight),
|
||||||
|
MIN(TotalPowerReturnNight),
|
||||||
|
MAX(GasConsumptionInCubicMeters),
|
||||||
|
MIN(GasConsumptionInCubicMeters)
|
||||||
|
FROM \"ElectricityLog\"
|
||||||
|
WHERE \"Date\" >= :start_date AND \"Date\" <= :stop_date
|
||||||
|
GROUP BY YearMonth
|
||||||
|
ORDER BY YearMonth;",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepate database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_start = format!("{}-01", start);
|
||||||
|
let formatted_stop = format!("{}-31", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching month power data for date range {} to {}",
|
||||||
|
formatted_start,
|
||||||
|
formatted_stop
|
||||||
|
);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(
|
||||||
|
&[
|
||||||
|
(":start_date", &formatted_start),
|
||||||
|
(":stop_date", &formatted_stop),
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(LogMonthSummaryEntity {
|
||||||
|
year_month: row.get(0)?,
|
||||||
|
total_power_consumption_day: row.get::<usize, f64>(1)?
|
||||||
|
- row.get::<usize, f64>(2)?,
|
||||||
|
total_power_consumption_night: row.get::<usize, f64>(3)?
|
||||||
|
- row.get::<usize, f64>(4)?,
|
||||||
|
total_power_return_day: row.get::<usize, f64>(5)? - row.get::<usize, f64>(6)?,
|
||||||
|
total_power_return_night: row.get::<usize, f64>(7)? - row.get::<usize, f64>(8)?,
|
||||||
|
gas_consumption_in_cubic_meters: row.get::<usize, f64>(9)?
|
||||||
|
- row.get::<usize, f64>(10)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => {
|
||||||
|
if let Some(year_month) = YearMonth::try_parse(&entity.year_month) {
|
||||||
|
items.push(LogMonthSummary {
|
||||||
|
year: year_month.year,
|
||||||
|
month: year_month.month,
|
||||||
|
total_power_consumption_day: entity.total_power_consumption_day,
|
||||||
|
total_power_consumption_night: entity.total_power_consumption_night,
|
||||||
|
total_power_return_day: entity.total_power_return_day,
|
||||||
|
total_power_return_night: entity.total_power_return_night,
|
||||||
|
gas_consumption_in_cubic_meters: entity
|
||||||
|
.gas_consumption_in_cubic_meters,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"Failed to parse year month {} from SQL row",
|
||||||
|
entity.year_month
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute month power data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogMonthSummaryEntity {
|
||||||
|
pub year_month: String,
|
||||||
|
pub total_power_consumption_day: f64,
|
||||||
|
pub total_power_consumption_night: f64,
|
||||||
|
pub total_power_return_day: f64,
|
||||||
|
pub total_power_return_night: f64,
|
||||||
|
pub gas_consumption_in_cubic_meters: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogMonthSummary {
|
||||||
|
pub year: i32,
|
||||||
|
pub month: u8,
|
||||||
|
pub total_power_consumption_day: f64,
|
||||||
|
pub total_power_consumption_night: f64,
|
||||||
|
pub total_power_return_day: f64,
|
||||||
|
pub total_power_return_night: f64,
|
||||||
|
pub gas_consumption_in_cubic_meters: f64,
|
||||||
|
}
|
||||||
408
src/electricity_api/src/main.rs
Normal file
408
src/electricity_api/src/main.rs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use warp::Filter;
|
||||||
|
use warp::http::Response;
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = std::sync::Arc::new(CommandLineArgs::parse());
|
||||||
|
|
||||||
|
colog::init();
|
||||||
|
log::info!("Electricity API is starting up");
|
||||||
|
|
||||||
|
serve(&args).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
struct CommandLineArgs {
|
||||||
|
#[arg(long = "database-path")]
|
||||||
|
database_path: String,
|
||||||
|
|
||||||
|
#[arg(long = "listening-port")]
|
||||||
|
listening_port: u16,
|
||||||
|
|
||||||
|
#[arg(long = "http-header-name-to-validate")]
|
||||||
|
http_header_name_to_validate: String,
|
||||||
|
|
||||||
|
#[arg(long = "http-header-value-to-validate")]
|
||||||
|
http_header_value_to_validate: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(configuration: &std::sync::Arc<CommandLineArgs>) {
|
||||||
|
let day = warp::get()
|
||||||
|
.and(warp::path("day"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::header::headers_cloned())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>,
|
||||||
|
headers,
|
||||||
|
configuration: std::sync::Arc<CommandLineArgs>| {
|
||||||
|
if !has_required_header(
|
||||||
|
&headers,
|
||||||
|
&configuration.http_header_name_to_validate,
|
||||||
|
&configuration.http_header_value_to_validate,
|
||||||
|
) {
|
||||||
|
log::info!("Access requested to /day with invalid header value");
|
||||||
|
return Response::builder()
|
||||||
|
.status(403)
|
||||||
|
.body(String::from("Forbidden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match shared_api_lib::query_helpers::try_parse_query_date(query.get("date")) {
|
||||||
|
Some(date) => {
|
||||||
|
let json = get_day_power_json(&date, &configuration.database_path);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
}
|
||||||
|
_ => Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"date\" param in query.")),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let days = warp::get()
|
||||||
|
.and(warp::path("days"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::header::headers_cloned())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>,
|
||||||
|
headers,
|
||||||
|
configuration: std::sync::Arc<CommandLineArgs>| {
|
||||||
|
if !has_required_header(
|
||||||
|
&headers,
|
||||||
|
&configuration.http_header_name_to_validate,
|
||||||
|
&configuration.http_header_value_to_validate,
|
||||||
|
) {
|
||||||
|
log::info!("Access requested to /days with invalid header value");
|
||||||
|
return Response::builder()
|
||||||
|
.status(403)
|
||||||
|
.body(String::from("Forbidden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_start =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_date(query.get("start"));
|
||||||
|
if maybe_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"start\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_stop =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_date(query.get("stop"));
|
||||||
|
if maybe_stop.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"stop\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = maybe_start.unwrap();
|
||||||
|
let stop = maybe_stop.unwrap();
|
||||||
|
if start > stop {
|
||||||
|
return Response::builder().status(400).body(String::from(
|
||||||
|
"Param \"start\" must be smaller than or equal to param \"stop\" in query.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_day_before_start = start.checked_sub_days(chrono::Days::new(1));
|
||||||
|
if maybe_day_before_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Param \"start\" in query is too early."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let day_before_start = maybe_day_before_start.unwrap();
|
||||||
|
let json =
|
||||||
|
get_days_power_json(&day_before_start, &stop, &configuration.database_path);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let months = warp::get()
|
||||||
|
.and(warp::path("months"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::header::headers_cloned())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>,
|
||||||
|
headers,
|
||||||
|
configuration: std::sync::Arc<CommandLineArgs>| {
|
||||||
|
if !has_required_header(
|
||||||
|
&headers,
|
||||||
|
&configuration.http_header_name_to_validate,
|
||||||
|
&configuration.http_header_value_to_validate,
|
||||||
|
) {
|
||||||
|
log::info!("Access requested to /months with invalid header value");
|
||||||
|
return Response::builder()
|
||||||
|
.status(403)
|
||||||
|
.body(String::from("Forbidden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_start =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start"));
|
||||||
|
if maybe_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"start\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_stop =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("stop"));
|
||||||
|
if maybe_stop.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"stop\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = maybe_start.unwrap();
|
||||||
|
let stop = maybe_stop.unwrap();
|
||||||
|
if start > stop {
|
||||||
|
return Response::builder().status(400).body(String::from(
|
||||||
|
"Param \"start\" must be smaller than or equal to param \"stop\" in query.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = get_months_power_json(&start, &stop, &configuration.database_path);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
warp::serve(day.or(days).or(months))
|
||||||
|
.run(([127, 0, 0, 1], configuration.listening_port))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_required_header(
|
||||||
|
headers: &warp::http::HeaderMap,
|
||||||
|
expected_header_name: &str,
|
||||||
|
allowed_header_values: &str,
|
||||||
|
) -> bool {
|
||||||
|
match headers.iter().find(|h| *h.0 == *expected_header_name) {
|
||||||
|
Some((_, header_value)) => {
|
||||||
|
for value in allowed_header_values.split(',') {
|
||||||
|
if *header_value == *value {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_power_json(date: &chrono::NaiveDate, database_path: &str) -> String {
|
||||||
|
let entities = database::get_day_power_entities(date, database_path);
|
||||||
|
let mut models = Vec::new();
|
||||||
|
|
||||||
|
for entity in entities {
|
||||||
|
let formatted_date_time = format!("{}T{}Z", date, entity.time_utc);
|
||||||
|
match chrono::DateTime::parse_from_rfc3339(&formatted_date_time) {
|
||||||
|
Ok(date_time) => {
|
||||||
|
models.push(DayResponseItem {
|
||||||
|
date_time: date_time.timestamp(),
|
||||||
|
current_power_usage: entity.current_power_usage,
|
||||||
|
total_power_use: entity.total_power_consumption_day
|
||||||
|
+ entity.total_power_consumption_night,
|
||||||
|
total_power_return: entity.total_power_return_day
|
||||||
|
+ entity.total_power_return_night,
|
||||||
|
total_gas_use: entity.gas_consumption_in_cubic_meters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to construct date time from {} with error {}",
|
||||||
|
formatted_date_time,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&models) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date {} with error {}",
|
||||||
|
date,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DayResponseItem {
|
||||||
|
date_time: i64,
|
||||||
|
current_power_usage: f64,
|
||||||
|
total_power_use: f64,
|
||||||
|
total_power_return: f64,
|
||||||
|
total_gas_use: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_days_power_json(
|
||||||
|
day_before_start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
database_path: &str,
|
||||||
|
) -> String {
|
||||||
|
let summaries = database::get_day_power_summaries(&day_before_start, &stop, database_path);
|
||||||
|
let mut models = Vec::new();
|
||||||
|
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
models.push(DaysResponseItem {
|
||||||
|
date: summary.date.clone(),
|
||||||
|
total_power_use: summary.total_power_consumption_day
|
||||||
|
+ summary.total_power_consumption_night,
|
||||||
|
total_power_return: summary.total_power_return_day + summary.total_power_return_night,
|
||||||
|
total_gas_use: summary.gas_consumption_in_cubic_meters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is there a more elegant way to do this? The problem is that the values
|
||||||
|
// are cumulative and the frontend wants actual delta's for each day instead
|
||||||
|
let mut total_power_use_to_subtract: f64 = 0.0;
|
||||||
|
let mut total_power_return_to_subtract: f64 = 0.0;
|
||||||
|
let mut total_gas_use_to_subtract: f64 = 0.0;
|
||||||
|
for model in models.iter_mut() {
|
||||||
|
let next_total_power_use_to_subtract = model.total_power_use;
|
||||||
|
let next_total_power_return_to_subtract = model.total_power_return;
|
||||||
|
let next_total_gas_use_to_subtract = model.total_gas_use;
|
||||||
|
|
||||||
|
model.total_power_use -= total_power_use_to_subtract;
|
||||||
|
model.total_power_return -= total_power_return_to_subtract;
|
||||||
|
model.total_gas_use -= total_gas_use_to_subtract;
|
||||||
|
|
||||||
|
total_power_use_to_subtract = next_total_power_use_to_subtract;
|
||||||
|
total_power_return_to_subtract = next_total_power_return_to_subtract;
|
||||||
|
total_gas_use_to_subtract = next_total_gas_use_to_subtract;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now fill any gaps for which the database had no results. This usually
|
||||||
|
// happens if future dates are requested.
|
||||||
|
let mut complete_models = Vec::new();
|
||||||
|
let mut current_date = day_before_start.clone();
|
||||||
|
loop {
|
||||||
|
let current_date_formatted = current_date.to_string();
|
||||||
|
match models.iter().find(|i| i.date == current_date_formatted) {
|
||||||
|
Some(existing_model) => complete_models.push(DaysResponseItem {
|
||||||
|
date: current_date_formatted,
|
||||||
|
total_power_use: existing_model.total_power_use,
|
||||||
|
total_power_return: existing_model.total_power_return,
|
||||||
|
total_gas_use: existing_model.total_gas_use,
|
||||||
|
}),
|
||||||
|
None => complete_models.push(DaysResponseItem {
|
||||||
|
date: current_date_formatted,
|
||||||
|
total_power_use: 0.0,
|
||||||
|
total_power_return: 0.0,
|
||||||
|
total_gas_use: 0.0,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
match current_date.checked_add_days(chrono::Days::new(1)) {
|
||||||
|
Some(next_date) => {
|
||||||
|
if next_date > *stop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_date = next_date
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_day_before_start = format!("{}", day_before_start);
|
||||||
|
if complete_models.len() > 0 && complete_models[0].date == formatted_day_before_start {
|
||||||
|
complete_models.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&complete_models) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date range {} to {} with error {}",
|
||||||
|
day_before_start,
|
||||||
|
stop,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DaysResponseItem {
|
||||||
|
date: String, // yyyy-MM-dd
|
||||||
|
total_power_use: f64,
|
||||||
|
total_power_return: f64,
|
||||||
|
total_gas_use: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_months_power_json(
|
||||||
|
start: &shared_api_lib::year_month::YearMonth,
|
||||||
|
stop: &shared_api_lib::year_month::YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
) -> String {
|
||||||
|
let summaries = database::get_month_power_summaries(&start, &stop, &database_path);
|
||||||
|
let mut response_model = MonthsResponse {
|
||||||
|
month_logs: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for summary in summaries {
|
||||||
|
response_model.month_logs.push(MonthsResponseItem {
|
||||||
|
year: summary.year,
|
||||||
|
month: summary.month,
|
||||||
|
total_power_use: summary.total_power_consumption_day
|
||||||
|
+ summary.total_power_consumption_night,
|
||||||
|
total_power_return: summary.total_power_return_day + summary.total_power_return_night,
|
||||||
|
total_gas_use: summary.gas_consumption_in_cubic_meters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&response_model) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date range {} to {} with error {}",
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MonthsResponse {
|
||||||
|
month_logs: Vec<MonthsResponseItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MonthsResponseItem {
|
||||||
|
year: i32,
|
||||||
|
month: u8,
|
||||||
|
total_power_use: f64,
|
||||||
|
total_power_return: f64,
|
||||||
|
total_gas_use: f64,
|
||||||
|
}
|
||||||
8
src/shared_api_lib/Cargo.toml
Normal file
8
src/shared_api_lib/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "shared_api_lib"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
2
src/shared_api_lib/src/lib.rs
Normal file
2
src/shared_api_lib/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod query_helpers;
|
||||||
|
pub mod year_month;
|
||||||
23
src/shared_api_lib/src/query_helpers.rs
Normal file
23
src/shared_api_lib/src/query_helpers.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use crate::year_month::YearMonth;
|
||||||
|
|
||||||
|
pub fn try_parse_query_date(value: Option<&String>) -> Option<NaiveDate> {
|
||||||
|
match value {
|
||||||
|
Some(string_value) => match chrono::NaiveDate::parse_from_str(string_value, "%Y-%m-%d") {
|
||||||
|
Ok(date) => Some(date),
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Failed to parse date {} with error {}", string_value, error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_parse_query_month_year(value: Option<&String>) -> Option<YearMonth> {
|
||||||
|
match value {
|
||||||
|
Some(string_value) => YearMonth::try_parse(string_value),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/shared_api_lib/src/year_month.rs
Normal file
150
src/shared_api_lib/src/year_month.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use chrono::Datelike;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct YearMonth {
|
||||||
|
pub year: i32,
|
||||||
|
pub month: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YearMonth {
|
||||||
|
pub fn get_next_month(&self) -> YearMonth {
|
||||||
|
if self.month >= 12 {
|
||||||
|
return YearMonth {
|
||||||
|
year: self.year + 1,
|
||||||
|
month: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return YearMonth {
|
||||||
|
year: self.year,
|
||||||
|
month: self.month + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_parse(string_value: &str) -> Option<YearMonth> {
|
||||||
|
let parts: std::vec::Vec<&str> = string_value.split('-').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (parts[0].parse::<i32>(), parts[1].parse::<u8>()) {
|
||||||
|
(Ok(year), Ok(month)) => {
|
||||||
|
if month < 1 || month > 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(YearMonth {
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn today() -> YearMonth {
|
||||||
|
let now = chrono::prelude::Local::now();
|
||||||
|
return YearMonth {
|
||||||
|
year: now.year(),
|
||||||
|
month: u8::try_from(now.month()).unwrap(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for YearMonth {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}-{:02}", self.year, self.month)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for YearMonth {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.year == other.year && self.month == other.month
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for YearMonth {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
if self.year > other.year {
|
||||||
|
return Some(std::cmp::Ordering::Greater);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.year < other.year {
|
||||||
|
return Some(std::cmp::Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.month > other.month {
|
||||||
|
return Some(std::cmp::Ordering::Greater);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.month < other.month {
|
||||||
|
return Some(std::cmp::Ordering::Less);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(std::cmp::Ordering::Equal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lt(&self, other: &Self) -> bool {
|
||||||
|
self.partial_cmp(other)
|
||||||
|
.is_some_and(std::cmp::Ordering::is_lt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le(&self, other: &Self) -> bool {
|
||||||
|
self.partial_cmp(other)
|
||||||
|
.is_some_and(std::cmp::Ordering::is_le)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gt(&self, other: &Self) -> bool {
|
||||||
|
self.partial_cmp(other)
|
||||||
|
.is_some_and(std::cmp::Ordering::is_gt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ge(&self, other: &Self) -> bool {
|
||||||
|
self.partial_cmp(other)
|
||||||
|
.is_some_and(std::cmp::Ordering::is_ge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds are inclusive
|
||||||
|
pub struct YearMonthRange {
|
||||||
|
start: YearMonth,
|
||||||
|
current: YearMonth,
|
||||||
|
stop: YearMonth,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YearMonthRange {
|
||||||
|
// Bounds are inclusive
|
||||||
|
pub fn new(start: &YearMonth, stop: &YearMonth) -> Result<YearMonthRange, &'static str> {
|
||||||
|
if *start > *stop {
|
||||||
|
return Err("Start cannot be greater than stop");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(YearMonthRange {
|
||||||
|
start: start.clone(),
|
||||||
|
current: start.clone(),
|
||||||
|
stop: stop.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> YearMonth {
|
||||||
|
self.start.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) -> YearMonth {
|
||||||
|
self.stop.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for YearMonthRange {
|
||||||
|
type Item = YearMonth;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.current > self.stop {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let result = self.current.clone();
|
||||||
|
self.current = result.get_next_month();
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
|
||||||
#include <ctime>
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <solar/logger/database.hpp>
|
#include <solar/logger/database.hpp>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cxxopts.hpp>
|
#include <cxxopts.hpp>
|
||||||
#include <iomanip>
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <solar/logger/database.hpp>
|
#include <solar/logger/database.hpp>
|
||||||
#include <solar/logger/envoydata.hpp>
|
#include <solar/logger/envoydata.hpp>
|
||||||
#include <solar/logger/zeverdata.hpp>
|
#include <solar/logger/zeverdata.hpp>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace detail
|
namespace detail
|
||||||
@@ -22,7 +21,43 @@ namespace detail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
|
struct CommandlineParameters
|
||||||
|
{
|
||||||
|
CommandlineParameters(
|
||||||
|
std::string const & deviceType,
|
||||||
|
std::string const & deviceUrl,
|
||||||
|
unsigned const deviceFetchTimeOut,
|
||||||
|
std::string const & databaseConnectionString)
|
||||||
|
: DeviceType(deviceType), DeviceUrl(deviceUrl), DeviceFetchTimeOut(deviceFetchTimeOut),
|
||||||
|
DatabaseConnectionString(databaseConnectionString) { };
|
||||||
|
|
||||||
|
std::string DeviceType;
|
||||||
|
std::string DeviceUrl;
|
||||||
|
unsigned DeviceFetchTimeOut;
|
||||||
|
std::string DatabaseConnectionString;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<std::string> TryGetStringArgument(cxxopts::ParseResult const & parseResult, std::string const & name)
|
||||||
|
{
|
||||||
|
if(!parseResult.contains(name))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResult[name].as_optional<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<unsigned> TryGetUnsignedArgument(cxxopts::ParseResult const & parseResult, std::string const & name)
|
||||||
|
{
|
||||||
|
if(!parseResult.contains(name))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResult[name].as_optional<unsigned>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<CommandlineParameters> TryGetCommandlineArguments(int argc, char ** argv)
|
||||||
{
|
{
|
||||||
cxxopts::Options options(
|
cxxopts::Options options(
|
||||||
"solar-logger",
|
"solar-logger",
|
||||||
@@ -50,14 +85,30 @@ std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto const parsed = options.parse(argc, argv);
|
auto const parseResult = options.parse(argc, argv);
|
||||||
return parsed;
|
auto const deviceType = TryGetStringArgument(parseResult, "type");
|
||||||
|
auto const deviceUrl = TryGetStringArgument(parseResult, "url");
|
||||||
|
auto const deviceFetchTimeOut = TryGetUnsignedArgument(parseResult, "timeout");
|
||||||
|
auto const databaseConnectionString = TryGetStringArgument(parseResult, "connection-string");
|
||||||
|
|
||||||
|
if(deviceType.has_value() && deviceUrl.has_value() && deviceFetchTimeOut.has_value()
|
||||||
|
&& databaseConnectionString.has_value())
|
||||||
|
{
|
||||||
|
return CommandlineParameters(
|
||||||
|
deviceType.value(),
|
||||||
|
deviceUrl.value(),
|
||||||
|
deviceFetchTimeOut.value(),
|
||||||
|
databaseConnectionString.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(cxxopts::OptionException const & e)
|
catch(cxxopts::exceptions::exception const & e)
|
||||||
{
|
{
|
||||||
spdlog::error(e.what());
|
spdlog::error(e.what());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::cout << options.help() << std::endl;
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> FetchDataFromURL(const std::string & url, const unsigned int timeout)
|
std::optional<std::string> FetchDataFromURL(const std::string & url, const unsigned int timeout)
|
||||||
@@ -81,7 +132,10 @@ std::optional<std::string> FetchDataFromURL(const std::string & url, const unsig
|
|||||||
const auto result = curl_easy_perform(curl);
|
const auto result = curl_easy_perform(curl);
|
||||||
if(result)
|
if(result)
|
||||||
{
|
{
|
||||||
spdlog::error("Failed to fetch data from URL {} with cURL error code {}", url.c_str(), result);
|
spdlog::error(
|
||||||
|
"Failed to fetch data from URL {} with cURL error code {}",
|
||||||
|
url.c_str(),
|
||||||
|
static_cast<int>(result));
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -95,21 +149,19 @@ std::optional<std::string> FetchDataFromURL(const std::string & url, const unsig
|
|||||||
|
|
||||||
int main(int argc, char ** argv)
|
int main(int argc, char ** argv)
|
||||||
{
|
{
|
||||||
auto const maybeArgs = ExtractArgs(argc, argv);
|
auto const parameters = TryGetCommandlineArguments(argc, argv);
|
||||||
if(!maybeArgs.has_value())
|
if(!parameters.has_value())
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const & args = maybeArgs.value();
|
Database db(parameters->DatabaseConnectionString);
|
||||||
|
|
||||||
Database db(args["connection-string"].as<std::string>());
|
|
||||||
auto const now = std::time(nullptr);
|
auto const now = std::time(nullptr);
|
||||||
auto const resourceType = args["type"].as<std::string>();
|
auto const resourceType = parameters->DeviceType;
|
||||||
|
|
||||||
if(resourceType == "Zeverlution")
|
if(resourceType == "Zeverlution")
|
||||||
{
|
{
|
||||||
auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
|
auto const webResource = FetchDataFromURL(parameters->DeviceUrl, parameters->DeviceFetchTimeOut);
|
||||||
if(!webResource.has_value())
|
if(!webResource.has_value())
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
@@ -134,7 +186,7 @@ int main(int argc, char ** argv)
|
|||||||
}
|
}
|
||||||
else if(resourceType == "Envoy")
|
else if(resourceType == "Envoy")
|
||||||
{
|
{
|
||||||
auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
|
auto const webResource = FetchDataFromURL(parameters->DeviceUrl, parameters->DeviceFetchTimeOut);
|
||||||
if(!webResource.has_value())
|
if(!webResource.has_value())
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
16
src/solar_api/Cargo.toml
Normal file
16
src/solar_api/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "solar_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
colog = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
rusqlite = { workspace = true, features = ["bundled"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
shared_api_lib = { path = "../shared_api_lib" }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
warp = { workspace = true, features = ["server"] }
|
||||||
596
src/solar_api/src/database.rs
Normal file
596
src/solar_api/src/database.rs
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
use shared_api_lib::year_month::YearMonth;
|
||||||
|
|
||||||
|
pub fn get_day_solar_entities(date: &chrono::NaiveDate, database_path: &str) -> DayEntities {
|
||||||
|
let mut result = DayEntities {
|
||||||
|
envoy_logs: Vec::new(),
|
||||||
|
zever_logs: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
get_day_solar_entities_envoy(&date, &connection, &mut result.envoy_logs);
|
||||||
|
get_day_solar_entities_zever(&date, &connection, &mut result.zever_logs);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_solar_entities_envoy(
|
||||||
|
date: &chrono::NaiveDate,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<EnvoyLogEntity>,
|
||||||
|
) {
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT
|
||||||
|
\"TimeUtc\",
|
||||||
|
\"CurrentWatts\",
|
||||||
|
\"TotalWatts\"
|
||||||
|
FROM \"EnvoyLogs\"
|
||||||
|
WHERE \"Date\" = :date
|
||||||
|
ORDER BY \"TimeUtc\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare envoy solar database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_date = format!("{}", date);
|
||||||
|
log::info!("Fetching envoy solar day data for date {}", formatted_date);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(&[(":date", &formatted_date)], |row| {
|
||||||
|
Ok(EnvoyLogEntity {
|
||||||
|
time_utc: row.get(0)?,
|
||||||
|
current_watts: row.get(1)?,
|
||||||
|
total_watts: row.get(2)?,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => {
|
||||||
|
output.push(entity);
|
||||||
|
}
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret envoy solar row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute envoy solar data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoy logs have a total_watts column that is cumulative instead of
|
||||||
|
// resetting every day like Zever logs are
|
||||||
|
if output.len() > 0 {
|
||||||
|
let offset = output[0].total_watts;
|
||||||
|
output[0].total_watts = 0;
|
||||||
|
for entity in output.iter_mut().skip(1) {
|
||||||
|
entity.total_watts -= offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_solar_entities_zever(
|
||||||
|
date: &chrono::NaiveDate,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<ZeverLogEntity>,
|
||||||
|
) {
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT
|
||||||
|
\"TimeUtc\",
|
||||||
|
\"CurrentWatts\",
|
||||||
|
\"TotalWatts\"
|
||||||
|
FROM \"ZeverLogs\"
|
||||||
|
WHERE \"Date\" = :date
|
||||||
|
ORDER BY \"TimeUtc\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_date = format!("{}", date);
|
||||||
|
log::info!("Fetching zever solar day data for date {}", formatted_date);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(&[(":date", &formatted_date)], |row| {
|
||||||
|
Ok(ZeverLogEntity {
|
||||||
|
time_utc: row.get(0)?,
|
||||||
|
current_watts: row.get(1)?,
|
||||||
|
total_watts: row.get(2)?,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => {
|
||||||
|
output.push(entity);
|
||||||
|
}
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret zever solar row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute zever solar data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EnvoyLogEntity {
|
||||||
|
pub time_utc: String,
|
||||||
|
pub current_watts: i32,
|
||||||
|
pub total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ZeverLogEntity {
|
||||||
|
pub time_utc: String,
|
||||||
|
pub current_watts: i32,
|
||||||
|
pub total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DayEntities {
|
||||||
|
pub envoy_logs: Vec<EnvoyLogEntity>,
|
||||||
|
pub zever_logs: Vec<ZeverLogEntity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_day_solar_summaries(
|
||||||
|
start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<LogDateSummary> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
let mut current_date = *start;
|
||||||
|
loop {
|
||||||
|
result.push(LogDateSummary {
|
||||||
|
date: current_date.to_string(),
|
||||||
|
envoy_total_watts: 0,
|
||||||
|
zever_total_watts: 0,
|
||||||
|
});
|
||||||
|
match current_date.checked_add_days(chrono::Days::new(1)) {
|
||||||
|
Some(next_date) => {
|
||||||
|
if next_date > *stop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_date = next_date
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
get_day_solar_summaries_envoy(&start, &stop, &connection, &mut result);
|
||||||
|
get_day_solar_summaries_zever(&start, &stop, &connection, &mut result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_solar_summaries_envoy(
|
||||||
|
start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<LogDateSummary>,
|
||||||
|
) {
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT \"Date\", MAX(\"TotalWatts\"), MIN(\"TotalWatts\")
|
||||||
|
FROM \"EnvoyLogs\"
|
||||||
|
WHERE \"Date\" >= :start_date AND \"Date\" <= :stop_date
|
||||||
|
GROUP BY \"Date\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare envoy day summary database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_start_date = format!("{}", start);
|
||||||
|
let formatted_stop_date = format!("{}", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching envoy solar day summary data for date range {} to {}",
|
||||||
|
formatted_start_date,
|
||||||
|
formatted_stop_date
|
||||||
|
);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(
|
||||||
|
&[
|
||||||
|
(":start_date", &formatted_start_date),
|
||||||
|
(":stop_date", &formatted_stop_date),
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(LogDateSummary {
|
||||||
|
date: row.get(0)?,
|
||||||
|
envoy_total_watts: row.get::<usize, i32>(1)? - row.get::<usize, i32>(2)?,
|
||||||
|
zever_total_watts: 0,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => match output.iter_mut().find(|e| e.date == entity.date) {
|
||||||
|
Some(existing_output_element) => {
|
||||||
|
existing_output_element.envoy_total_watts = entity.envoy_total_watts
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with non existing date {} in output of envoy solar summary data",
|
||||||
|
entity.date
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret envoy solar summary row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute envoy solar summary data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_solar_summaries_zever(
|
||||||
|
start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<LogDateSummary>,
|
||||||
|
) {
|
||||||
|
let maybe_statement = connection.prepare(
|
||||||
|
"SELECT \"Date\", MAX(\"TotalWatts\")
|
||||||
|
FROM \"ZeverLogs\"
|
||||||
|
WHERE \"Date\" >= :start_date AND \"Date\" <= :stop_date
|
||||||
|
GROUP BY \"Date\";",
|
||||||
|
);
|
||||||
|
if maybe_statement.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare zever day summary database statement with error {}",
|
||||||
|
maybe_statement.unwrap_err()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_start_date = format!("{}", start);
|
||||||
|
let formatted_stop_date = format!("{}", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching zever solar day summary data for date range {} to {}",
|
||||||
|
formatted_start_date,
|
||||||
|
formatted_stop_date
|
||||||
|
);
|
||||||
|
let mut statement = maybe_statement.unwrap();
|
||||||
|
let maybe_row_iterator = statement.query_map(
|
||||||
|
&[
|
||||||
|
(":start_date", &formatted_start_date),
|
||||||
|
(":stop_date", &formatted_stop_date),
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(LogDateSummary {
|
||||||
|
date: row.get(0)?,
|
||||||
|
envoy_total_watts: 0,
|
||||||
|
zever_total_watts: row.get(1)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match maybe_row_iterator {
|
||||||
|
Ok(iterator) => {
|
||||||
|
for row in iterator {
|
||||||
|
match row {
|
||||||
|
Ok(entity) => match output.iter_mut().find(|e| e.date == entity.date) {
|
||||||
|
Some(existing_output_element) => {
|
||||||
|
existing_output_element.zever_total_watts = entity.zever_total_watts
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with non existing date {} in output of zever solar summary data",
|
||||||
|
entity.date
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => log::error!(
|
||||||
|
"Failed to interpret zever solar summary row from SQL query with error {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute zever solar summary data SQL query with error {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogDateSummary {
|
||||||
|
pub date: String,
|
||||||
|
pub envoy_total_watts: i32,
|
||||||
|
pub zever_total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_month_solar_summaries(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<LogYearMonthSummary> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut current_year_month = *start;
|
||||||
|
loop {
|
||||||
|
result.push(LogYearMonthSummary {
|
||||||
|
year: current_year_month.year,
|
||||||
|
month: current_year_month.month,
|
||||||
|
envoy_total_watts: 0,
|
||||||
|
zever_total_watts: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
current_year_month = current_year_month.get_next_month();
|
||||||
|
if current_year_month > *stop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_connection = rusqlite::Connection::open(database_path);
|
||||||
|
if maybe_connection.is_err() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to open database connection to {} with error {}",
|
||||||
|
database_path,
|
||||||
|
maybe_connection.unwrap_err()
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = maybe_connection.unwrap();
|
||||||
|
get_month_solar_summaries_envoy(&start, &stop, &connection, &mut result);
|
||||||
|
get_month_solar_summaries_zever(&start, &stop, &connection, &mut result);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_month_solar_summaries_envoy(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<LogYearMonthSummary>,
|
||||||
|
) {
|
||||||
|
let formatted_start_date = format!("{}-01", start);
|
||||||
|
let formatted_stop_date = format!("{}-31", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching envoy solar month summary data for date range {} to {}",
|
||||||
|
formatted_start_date,
|
||||||
|
formatted_stop_date
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut statement = match connection.prepare(
|
||||||
|
"SELECT
|
||||||
|
STRFTIME('%Y-%m', \"Date\") AS YearMonth,
|
||||||
|
MAX(TotalWatts),
|
||||||
|
MIN(TotalWatts)
|
||||||
|
FROM EnvoyLogs
|
||||||
|
WHERE \"Date\" >= :start_date AND \"Date\" <= :stop_date
|
||||||
|
GROUP BY YearMonth
|
||||||
|
ORDER BY YearMonth;",
|
||||||
|
) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare envoy month summary database statement with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = match statement.query_map(
|
||||||
|
&[
|
||||||
|
(":start_date", &formatted_start_date),
|
||||||
|
(":stop_date", &formatted_stop_date),
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(LogYearMonthSummaryEntity {
|
||||||
|
year_month: row.get(0)?,
|
||||||
|
total_watts: row.get::<usize, i32>(1)? - row.get::<usize, i32>(2)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(it) => it,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute envoy solar month summary data SQL query with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let entity = match row {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to interpret envoy month solar summary row from SQL query with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(entity_year_month) = YearMonth::try_parse(&entity.year_month) else {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with invalid year month {} in output of envoy solar month summary data",
|
||||||
|
entity.year_month
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(existing_output_element) = output
|
||||||
|
.iter_mut()
|
||||||
|
.find(|e| e.year == entity_year_month.year && e.month == entity_year_month.month)
|
||||||
|
else {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with non existing year month {} in output of envoy solar month summary data",
|
||||||
|
entity.year_month
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
existing_output_element.envoy_total_watts = entity.total_watts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_month_solar_summaries_zever(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
connection: &rusqlite::Connection,
|
||||||
|
output: &mut Vec<LogYearMonthSummary>,
|
||||||
|
) {
|
||||||
|
let formatted_start_date = format!("{}-01", start);
|
||||||
|
let formatted_stop_date = format!("{}-31", stop);
|
||||||
|
log::info!(
|
||||||
|
"Fetching zever solar month summary data for date range {} to {}",
|
||||||
|
formatted_start_date,
|
||||||
|
formatted_stop_date
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut statement = match connection.prepare(
|
||||||
|
"WITH MaximumValuesPerDate AS (
|
||||||
|
SELECT \"Date\", MAX(\"TotalWatts\") AS \"TotalWatts\"
|
||||||
|
FROM \"ZeverLogs\"
|
||||||
|
WHERE
|
||||||
|
\"Date\" >= :start_date AND
|
||||||
|
\"Date\" <= :stop_date
|
||||||
|
GROUP BY \"Date\"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
STRFTIME('%Y-%m', \"Date\") AS YearMonth,
|
||||||
|
SUM(TotalWatts)
|
||||||
|
FROM MaximumValuesPerDate
|
||||||
|
GROUP BY YearMonth
|
||||||
|
ORDER BY YearMonth;",
|
||||||
|
) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to prepare zever month summary database statement with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = match statement.query_map(
|
||||||
|
&[
|
||||||
|
(":start_date", &formatted_start_date),
|
||||||
|
(":stop_date", &formatted_stop_date),
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(LogYearMonthSummaryEntity {
|
||||||
|
year_month: row.get(0)?,
|
||||||
|
total_watts: row.get(1)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(it) => it,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to execute zever solar month summary data SQL query with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let entity = match row {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to interpret zever month solar summary row from SQL query with error {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(entity_year_month) = YearMonth::try_parse(&entity.year_month) else {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with invalid year month {} in output of zever solar month summary data",
|
||||||
|
entity.year_month
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(existing_output_element) = output
|
||||||
|
.iter_mut()
|
||||||
|
.find(|e| e.year == entity_year_month.year && e.month == entity_year_month.month)
|
||||||
|
else {
|
||||||
|
log::error!(
|
||||||
|
"Processed entity with non existing year month {} in output of zever solar month summary data",
|
||||||
|
entity.year_month
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
existing_output_element.zever_total_watts = entity.total_watts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogYearMonthSummaryEntity {
|
||||||
|
pub year_month: String,
|
||||||
|
pub total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogYearMonthSummary {
|
||||||
|
pub year: i32,
|
||||||
|
pub month: u8,
|
||||||
|
pub envoy_total_watts: i32,
|
||||||
|
pub zever_total_watts: i32,
|
||||||
|
}
|
||||||
528
src/solar_api/src/main.rs
Normal file
528
src/solar_api/src/main.rs
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
use chrono;
|
||||||
|
use clap::Parser;
|
||||||
|
use shared_api_lib::year_month::{YearMonth, YearMonthRange};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use warp::Filter;
|
||||||
|
use warp::http::Response;
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = std::sync::Arc::new(CommandLineArgs::parse());
|
||||||
|
|
||||||
|
colog::init();
|
||||||
|
log::info!("Solar API is starting up");
|
||||||
|
|
||||||
|
serve(&args).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
struct CommandLineArgs {
|
||||||
|
#[arg(long = "database-path")]
|
||||||
|
database_path: String,
|
||||||
|
|
||||||
|
#[arg(long = "listening-port")]
|
||||||
|
listening_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(configuration: &std::sync::Arc<CommandLineArgs>) {
|
||||||
|
let day = warp::get()
|
||||||
|
.and(warp::path("day"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>, configuration: std::sync::Arc<CommandLineArgs>| {
|
||||||
|
match shared_api_lib::query_helpers::try_parse_query_date(query.get("date")) {
|
||||||
|
Some(date) => {
|
||||||
|
let json = get_day_solar_json(&date, &configuration.database_path);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
}
|
||||||
|
_ => Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"date\" param in query.")),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let days = warp::get()
|
||||||
|
.and(warp::path("days"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>, configuration: std::sync::Arc<CommandLineArgs>| {
|
||||||
|
let maybe_start =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_date(query.get("start"));
|
||||||
|
if maybe_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"start\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_stop =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_date(query.get("stop"));
|
||||||
|
if maybe_stop.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"stop\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = maybe_start.unwrap();
|
||||||
|
let stop = maybe_stop.unwrap();
|
||||||
|
if start > stop {
|
||||||
|
return Response::builder().status(400).body(String::from(
|
||||||
|
"Param \"start\" must be smaller than or equal to param \"stop\" in query.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_day_before_start = start.checked_sub_days(chrono::Days::new(1));
|
||||||
|
if maybe_day_before_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Param \"start\" in query is too early."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let day_before_start = maybe_day_before_start.unwrap();
|
||||||
|
let json =
|
||||||
|
get_days_solar_json(&day_before_start, &stop, &configuration.database_path);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let month_cache: std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>> =
|
||||||
|
std::sync::Arc::new(RwLock::new(Vec::new()));
|
||||||
|
let months = warp::get()
|
||||||
|
.and(warp::path("months"))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and(warp::any().map({
|
||||||
|
let configuration = configuration.clone();
|
||||||
|
move || configuration.clone()
|
||||||
|
}))
|
||||||
|
.and(warp::any().map({
|
||||||
|
let month_cache = month_cache.clone();
|
||||||
|
move || month_cache.clone()
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
|query: HashMap<String, String>,
|
||||||
|
configuration: std::sync::Arc<CommandLineArgs>,
|
||||||
|
month_cache| {
|
||||||
|
let maybe_start =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start"));
|
||||||
|
if maybe_start.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"start\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_stop =
|
||||||
|
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("stop"));
|
||||||
|
if maybe_stop.is_none() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(String::from("Unsupported \"stop\" param in query."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = maybe_start.unwrap();
|
||||||
|
let stop = maybe_stop.unwrap();
|
||||||
|
if start > stop {
|
||||||
|
return Response::builder().status(400).body(String::from(
|
||||||
|
"Param \"start\" must be smaller than or equal to param \"stop\" in query.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json =
|
||||||
|
get_months_solar_json(&start, &stop, &configuration.database_path, month_cache);
|
||||||
|
return Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(json);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
warp::serve(day.or(days).or(months))
|
||||||
|
.run(([127, 0, 0, 1], configuration.listening_port))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_day_solar_json(date: &chrono::NaiveDate, database_path: &str) -> String {
|
||||||
|
let entities = database::get_day_solar_entities(date, database_path);
|
||||||
|
let mut response_model = DayResponse {
|
||||||
|
envoy_logs: Vec::new(),
|
||||||
|
zever_logs: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for entity in entities.envoy_logs {
|
||||||
|
response_model.envoy_logs.push(DayResponseLogItem {
|
||||||
|
time_utc: entity.time_utc,
|
||||||
|
current_watts: entity.current_watts,
|
||||||
|
total_watts: entity.total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for entity in entities.zever_logs {
|
||||||
|
response_model.zever_logs.push(DayResponseLogItem {
|
||||||
|
time_utc: entity.time_utc,
|
||||||
|
current_watts: entity.current_watts,
|
||||||
|
total_watts: entity.total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&response_model) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date {} with error {}",
|
||||||
|
date,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DayResponse {
|
||||||
|
envoy_logs: Vec<DayResponseLogItem>,
|
||||||
|
zever_logs: Vec<DayResponseLogItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DayResponseLogItem {
|
||||||
|
time_utc: String, // HH:mm:ss
|
||||||
|
current_watts: i32,
|
||||||
|
total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_days_solar_json(
|
||||||
|
day_before_start: &chrono::NaiveDate,
|
||||||
|
stop: &chrono::NaiveDate,
|
||||||
|
database_path: &str,
|
||||||
|
) -> String {
|
||||||
|
let summaries = database::get_day_solar_summaries(&day_before_start, &stop, database_path);
|
||||||
|
let mut response_model = DaysResponse {
|
||||||
|
day_logs: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for summary in summaries {
|
||||||
|
response_model.day_logs.push(DaysResponseItem {
|
||||||
|
date: summary.date.clone(),
|
||||||
|
envoy_total_watts: summary.envoy_total_watts,
|
||||||
|
zever_total_watts: summary.zever_total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&response_model) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date range {} to {} with error {}",
|
||||||
|
day_before_start,
|
||||||
|
stop,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DaysResponse {
|
||||||
|
day_logs: Vec<DaysResponseItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DaysResponseItem {
|
||||||
|
date: String, // yyyy-MM-dd
|
||||||
|
envoy_total_watts: i32,
|
||||||
|
zever_total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_months_solar_json(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
month_cache: std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>>,
|
||||||
|
) -> String {
|
||||||
|
let mut response_model = MonthsResponse {
|
||||||
|
month_logs: YearMonthRange::new(start, stop)
|
||||||
|
.unwrap()
|
||||||
|
.map(|ym| MonthsResponseItem {
|
||||||
|
year: ym.year,
|
||||||
|
month: ym.month,
|
||||||
|
envoy_total_watts: 0,
|
||||||
|
zever_total_watts: 0,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cached_months = get_cached_months(&start, &stop, &month_cache);
|
||||||
|
let mut missing_months_from_cache;
|
||||||
|
if cached_months.len() > 0 {
|
||||||
|
missing_months_from_cache = Vec::new();
|
||||||
|
let mut matching_cached_month_index = 0;
|
||||||
|
for response_month in response_model.month_logs.iter_mut() {
|
||||||
|
if matching_cached_month_index < cached_months.len() {
|
||||||
|
let cached_month = &cached_months[matching_cached_month_index];
|
||||||
|
if response_month.year == cached_month.year
|
||||||
|
&& response_month.month == cached_month.month
|
||||||
|
{
|
||||||
|
response_month.envoy_total_watts = cached_month.envoy_total_watts;
|
||||||
|
response_month.zever_total_watts = cached_month.zever_total_watts;
|
||||||
|
|
||||||
|
matching_cached_month_index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_months_from_cache.push(YearMonth {
|
||||||
|
year: response_month.year,
|
||||||
|
month: response_month.month,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing_months_from_cache = response_model
|
||||||
|
.month_logs
|
||||||
|
.iter()
|
||||||
|
.map(|i| YearMonth {
|
||||||
|
year: i.year,
|
||||||
|
month: i.month,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if missing_months_from_cache.len() > 0 {
|
||||||
|
let missing_ranges = compact_missing_months_to_ranges(&missing_months_from_cache);
|
||||||
|
for range in missing_ranges {
|
||||||
|
let range_start = range.start();
|
||||||
|
let database_months =
|
||||||
|
get_month_summary_range_from_database(&range_start, &range.stop(), &database_path);
|
||||||
|
|
||||||
|
let mut response_item_index = response_model
|
||||||
|
.month_logs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, item)| {
|
||||||
|
if item.year == range_start.year && item.month == range_start.month {
|
||||||
|
return Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
for database_month in database_months.iter() {
|
||||||
|
let mut response_month = &mut response_model.month_logs[response_item_index];
|
||||||
|
// Database may return only partial results, i.e. not all dates
|
||||||
|
// for the range may be returned. Hence we need to check if the
|
||||||
|
// database result is for the same year month.
|
||||||
|
while database_month.year != response_month.year
|
||||||
|
|| database_month.month != response_month.month
|
||||||
|
{
|
||||||
|
response_item_index += 1;
|
||||||
|
response_month = &mut response_model.month_logs[response_item_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
response_month.envoy_total_watts = database_month.envoy_total_watts;
|
||||||
|
response_month.zever_total_watts = database_month.zever_total_watts;
|
||||||
|
response_item_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
insert_missing_months_in_month_cache(&database_months, &month_cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string(&response_model) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to format JSON data for date range {} to {} with error {}",
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return String::from("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_month_summary_range_from_database(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<MonthsResponseItem> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let summaries = database::get_month_solar_summaries(&start, &stop, &database_path);
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
results.push(MonthsResponseItem {
|
||||||
|
year: summary.year,
|
||||||
|
month: summary.month,
|
||||||
|
envoy_total_watts: summary.envoy_total_watts,
|
||||||
|
zever_total_watts: summary.zever_total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached_months(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
month_cache: &std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>>,
|
||||||
|
) -> Vec<MonthsResponseItem> {
|
||||||
|
if let Ok(cache) = month_cache.read() {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for item in cache
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.year_month >= *start && i.year_month <= *stop)
|
||||||
|
{
|
||||||
|
results.push(MonthsResponseItem {
|
||||||
|
year: item.year_month.year,
|
||||||
|
month: item.year_month.month,
|
||||||
|
envoy_total_watts: item.envoy_total_watts,
|
||||||
|
zever_total_watts: item.zever_total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Retrieved {} entries by month summary cache for {} to {} range",
|
||||||
|
results.len(),
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to acquire read lock for month cache");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compact_missing_months_to_ranges(missing_year_months: &[YearMonth]) -> Vec<YearMonthRange> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
if missing_year_months.len() < 1 {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start_index = 0;
|
||||||
|
let mut current_year_month = missing_year_months[start_index].clone();
|
||||||
|
for (index, next_year_month) in missing_year_months.iter().skip(1).enumerate() {
|
||||||
|
if current_year_month.get_next_month() != *next_year_month {
|
||||||
|
results.push(
|
||||||
|
YearMonthRange::new(&missing_year_months[start_index], ¤t_year_month)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
start_index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_year_month = next_year_month.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_last_result_entry = results.last();
|
||||||
|
if maybe_last_result_entry.is_none()
|
||||||
|
|| maybe_last_result_entry.unwrap().stop() < *missing_year_months.last().unwrap()
|
||||||
|
{
|
||||||
|
results.push(
|
||||||
|
YearMonthRange::new(&missing_year_months[start_index], ¤t_year_month).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_missing_months_in_month_cache(
|
||||||
|
summaries: &[MonthsResponseItem],
|
||||||
|
month_cache: &RwLock<Vec<CachedMonthsResponseItem>>,
|
||||||
|
) {
|
||||||
|
if summaries.len() < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut cache) = month_cache.write() {
|
||||||
|
let first_summary_year_month = YearMonth {
|
||||||
|
year: summaries[0].year,
|
||||||
|
month: summaries[0].month,
|
||||||
|
};
|
||||||
|
let mut summary_insertion_index = cache
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, item)| {
|
||||||
|
if item.year_month > first_summary_year_month {
|
||||||
|
return Some(index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let current_year_month = YearMonth::today();
|
||||||
|
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
let summary_year_month = YearMonth {
|
||||||
|
year: summary.year,
|
||||||
|
month: summary.month,
|
||||||
|
};
|
||||||
|
// Do not cache results that are subject to change, such as the
|
||||||
|
// current month or any future month
|
||||||
|
if summary_year_month >= current_year_month {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_insert = CachedMonthsResponseItem {
|
||||||
|
year_month: summary_year_month,
|
||||||
|
envoy_total_watts: summary.envoy_total_watts,
|
||||||
|
zever_total_watts: summary.zever_total_watts,
|
||||||
|
};
|
||||||
|
if summary_insertion_index < cache.len() {
|
||||||
|
while cache[summary_insertion_index].year_month < summary_year_month {
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary_insertion_index < cache.len() {
|
||||||
|
cache.insert(summary_insertion_index, to_insert);
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
} else {
|
||||||
|
cache.push(to_insert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache.push(to_insert);
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to acquire write lock for month cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MonthsResponse {
|
||||||
|
month_logs: Vec<MonthsResponseItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MonthsResponseItem {
|
||||||
|
year: i32,
|
||||||
|
month: u8,
|
||||||
|
envoy_total_watts: i32,
|
||||||
|
zever_total_watts: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CachedMonthsResponseItem {
|
||||||
|
year_month: YearMonth,
|
||||||
|
envoy_total_watts: i32,
|
||||||
|
zever_total_watts: i32,
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=A .NET Core application providing the Electricity REST API
|
Description=The electricity JSON API written in rust
|
||||||
Requires=network.target
|
Requires=network.target
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
Environment=ASPNETCORE_URLS=http://*:3002
|
WorkingDirectory=/home/pi/project/home-data-collection-tools/target/release
|
||||||
WorkingDirectory=/home/pi
|
ExecStart=/home/pi/project/home-data-collection-tools/target/release/electricity_api --database-path /home/pi/logs/electricity.db --listening-port 3002 --http-header-name-to-validate 'X-Real-IP' --http-header-value-to-validate 'IP_ADDR_V4_1,IP_ADDR_V4_2'
|
||||||
ExecStart=/home/pi/.dotnet/dotnet /home/pi/bin/Electricity.Api/Electricity.Api.dll --connection-string /home/pi/logs/electricity.logs
|
|
||||||
Restart=on-abnormal
|
Restart=on-abnormal
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
User=pi
|
User=pi
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=A .NET Core application providing the Solar REST API
|
Description=The solar JSON API written in rust
|
||||||
Requires=network.target
|
Requires=network.target
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
Environment=ASPNETCORE_URLS=http://*:3001
|
WorkingDirectory=/home/pi/project/home-data-collection-tools/target/release
|
||||||
WorkingDirectory=/home/pi
|
ExecStart=/home/pi/project/home-data-collection-tools/target/release/solar_api --database-path /home/pi/logs/solarpaneloutput.db --listening-port 3001
|
||||||
ExecStart=/home/pi/.dotnet/dotnet /home/pi/bin/Solar.Api/Solar.Api.dll
|
|
||||||
Restart=on-abnormal
|
Restart=on-abnormal
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
User=pi
|
User=pi
|
||||||
|
|||||||
Reference in New Issue
Block a user