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