Rewrite .NET electricity API in rust

This commit is contained in:
2025-09-01 20:54:18 +02:00
parent 89623f26db
commit 90544b5b07
27 changed files with 2847 additions and 583 deletions

5
.gitignore vendored
View File

@@ -1,6 +1,7 @@
build/
bin/
samples/
target/
.vscode/
!.vscode/settings.json
@@ -40,3 +41,7 @@ samples/
*.out
*.app
# Sqlite database files
*.db
*.db-shm
*.db-wal

1472
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

7
Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[workspace]
resolver = "3"
members = [
"src/electricity_api"
]
[workspace.dependencies]

View File

@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Electricity.Api", "src\Electricity.Api\Electricity.Api.csproj", "{8791CE17-DC8F-4630-9315-C3EDF639E8C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Solar.Api", "src\Solar.Api\Solar.Api.csproj", "{5EC49F22-9031-4124-88E6-C7C1F636695A}"
EndProject
Global
@@ -16,10 +14,6 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8791CE17-DC8F-4630-9315-C3EDF639E8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8791CE17-DC8F-4630-9315-C3EDF639E8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8791CE17-DC8F-4630-9315-C3EDF639E8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8791CE17-DC8F-4630-9315-C3EDF639E8C6}.Release|Any CPU.Build.0 = Release|Any CPU
{5EC49F22-9031-4124-88E6-C7C1F636695A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EC49F22-9031-4124-88E6-C7C1F636695A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EC49F22-9031-4124-88E6-C7C1F636695A}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

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

View File

@@ -1,72 +0,0 @@
using System.Net;
using Electricity.Api.Options;
namespace Electricity.Api;
internal class AllowedAccessMiddleware(
RequestDelegate next,
ILogger<AllowedAccessMiddleware> logger,
AllowedAccessOptions options
)
{
private readonly RequestDelegate next = next;
private readonly ILogger<AllowedAccessMiddleware> logger = logger;
private readonly AllowedAccessOptions options = options;
public async Task Invoke(HttpContext context)
{
if (
!context.Request.Headers.TryGetValue(
options.HttpIpAddressHeaderName,
out var headerValue
)
)
{
logger.LogDebug(
"Rejecting request missing the expected HTTP header {HeaderName}",
options.HttpIpAddressHeaderName
);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
if (headerValue.Count != 1)
{
logger.LogDebug(
"Rejecting request with malformed HTTP header {HeaderName}",
options.HttpIpAddressHeaderName
);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
if (string.IsNullOrWhiteSpace(headerValue[0]))
{
logger.LogDebug(
"Rejecting request with malformed value {HeaderValue} in HTTP header {HeaderName}",
headerValue[0],
options.HttpIpAddressHeaderName
);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
if (!options.HttpIpAddressHeaderValues.Contains(headerValue[0]))
{
logger.LogInformation(
"Rejecting request with disallowed header value {HeaderValue} in HTTP header {HeaderName}",
headerValue[0],
options.HttpIpAddressHeaderName
);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
logger.LogDebug(
"Accepting request with allowed value {HeaderValue} in HTTP header {HeaderName}",
headerValue[0],
options.HttpIpAddressHeaderName
);
await next.Invoke(context);
}
}

View File

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

View File

@@ -1,74 +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})."
);
}

View File

@@ -1,41 +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);
}
}

View File

@@ -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="9.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>

View File

@@ -1,25 +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();
}
}

View File

@@ -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);
}

View File

@@ -1,21 +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
);
}
};

View File

@@ -1,34 +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
);
}
};

View File

@@ -1,47 +0,0 @@
namespace Electricity.Api.Options;
internal class AllowedAccessOptions(IConfiguration configuration)
{
public string HttpIpAddressHeaderName { get; } = ResolveHttpIpAddressHeaderName(configuration);
public string[] HttpIpAddressHeaderValues { get; } =
ResolveHttpIpAddressHeaderValues(configuration);
private static string ResolveHttpIpAddressHeaderName(IConfiguration configuration)
{
const string configurationKey = "AllowedAccess:HttpIpAddressHeaderName";
var value = configuration["AllowedAccess:HttpIpAddressHeaderName"];
if (string.IsNullOrWhiteSpace(value))
{
throw new NotSupportedException($"The app setting {configurationKey} cannot be empty");
}
return value;
}
private static string[] ResolveHttpIpAddressHeaderValues(IConfiguration configuration)
{
const string configurationKey = "AllowedAccess:HttpIpAddressHeaderValues";
var values = configuration.GetSection(configurationKey).Get<List<string>>();
if (values == null || values.Count == 0)
{
throw new NotSupportedException(
$"The app setting {configurationKey} must be an string array with at least 1 value"
);
}
List<string> validatedValues = [];
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new NotSupportedException(
$"The value \"{value}\" from app setting {configurationKey} cannot be empty"
);
}
validatedValues.Add(value);
}
return validatedValues.ToArray();
}
}

View File

@@ -1,97 +0,0 @@
using System.CommandLine;
using Electricity.Api;
using Electricity.Api.Options;
using Electricity.Api.Services;
using Microsoft.EntityFrameworkCore;
internal class Program
{
private static int Main(string[] args)
{
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>(
(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.AddSingleton(sp => new AllowedAccessOptions(
sp.GetRequiredService<IConfiguration>()
));
builder.Services.AddScoped<ElectricityService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("default");
}
app.UseMiddleware<AllowedAccessMiddleware>(
app.Services.GetRequiredService<AllowedAccessOptions>()
);
app.MapControllers();
app.Run();
return 0;
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -1,68 +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
);
}
}

View File

@@ -1,12 +0,0 @@
{
"AllowedAccess": {
"HttpIpAddressHeaderName": "Host",
"HttpIpAddressHeaderValues": ["localhost:7290"]
},
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,13 +0,0 @@
{
"AllowedHosts": "*",
"AllowedAccess": {
"HttpIpAddressHeaderName": "X-Real-IP",
"HttpIpAddressHeaderValues": ["213.233.220.64", "86.83.136.215"]
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,3 +0,0 @@
#!/bin/bash
dotnet publish -c Release -r linux-arm --no-self-contained

842
src/electricity_api/Cargo.lock generated Normal file
View 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"

View File

@@ -0,0 +1,15 @@
[package]
name = "electricity_api"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.41"
clap = { version = "4.5.46", features = ["derive"] }
colog = "1.3.0"
log = "0.4.27"
rusqlite = { version = "0.37.0", features = ["bundled"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
tokio = { version = "1", features = ["full"] }
warp = { version = "0.4", features = ["server"] }

View File

@@ -0,0 +1,197 @@
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,
}

View File

@@ -0,0 +1,14 @@
use chrono::NaiveDate;
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,
}
}

View File

@@ -0,0 +1,292 @@
use clap::Parser;
use std::collections::HashMap;
use warp::Filter;
use warp::http::Response;
mod database;
mod helpers;
#[tokio::main]
async fn main() {
let args = CommandLineArgs::parse();
colog::init();
log::info!("Electricity API is starting up");
serve(
leak_string_to_static(&args.database_path),
args.listening_port,
leak_string_to_static(&args.http_header_name_to_validate),
leak_string_to_static(&args.http_header_value_to_validate),
)
.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,
}
// This is really stupid to do but the alternative seems to be that every
// endpoint needs its own complete copy of all the configuration values? Weird.
fn leak_string_to_static(value: &String) -> &'static str {
Box::leak(value.clone().into_boxed_str())
}
async fn serve(
database_path: &'static str,
listening_port: u16,
http_header_name_to_validate: &'static str,
http_header_value_to_validate: &'static str,
) {
let day = warp::get()
.and(warp::path("day"))
.and(warp::query::<HashMap<String, String>>())
.and(warp::header::<String>(http_header_name_to_validate))
.map(|query: HashMap<String, String>, header_value: String| {
if !is_valid_header(&header_value, http_header_value_to_validate) {
log::info!(
"Access requested to /day with invalid header value {}",
header_value
);
return Response::builder()
.status(403)
.body(String::from("Forbidden"));
}
match helpers::try_parse_query_date(query.get("date")) {
Some(date) => {
let json = get_day_power_json(&date, 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::<String>(http_header_name_to_validate))
.map(|query: HashMap<String, String>, header_value: String| {
if !is_valid_header(&header_value, http_header_value_to_validate) {
log::info!(
"Access requested to /days with invalid header value {}",
header_value
);
return Response::builder()
.status(403)
.body(String::from("Forbidden"));
}
let maybe_start = 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 = 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, database_path);
return Response::builder()
.header("Content-Type", "application/json")
.body(json);
});
warp::serve(day.or(days))
.run(([127, 0, 0, 1], listening_port))
.await;
}
fn is_valid_header(header_value: &str, allowed_values: &str) -> bool {
for value in allowed_values.split(',') {
if *header_value == *value {
return true;
}
}
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,
}

View File

@@ -1,13 +1,12 @@
[Unit]
Description=A .NET Core application providing the Electricity REST API
Description=The Electricity JSON API written in rust
Requires=network.target
After=network.target
[Service]
Type=simple
Environment=ASPNETCORE_URLS=http://*:3002
WorkingDirectory=/home/pi
ExecStart=/home/pi/.dotnet/dotnet /home/pi/bin/Electricity.Api/Electricity.Api.dll --connection-string /home/pi/logs/electricity.logs
WorkingDirectory=/home/pi/project/home-data-collection-tools/target/release
ExecStart=/home/pi/project/home-data-collection-tools/target/release/electricity_api --database-path /home/pi/logs/electricity.logs --listening-port 3002 --http-header-name-to-validate 'X-Real-IP' --http-header-value-to-validate 'IP_ADDR_V4_1,IP_ADDR_V4_2'
Restart=on-abnormal
RestartSec=30
User=pi