From ef616d621612af4ea7e6835eac2f593793dda839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 27 Nov 2023 19:00:48 +0100 Subject: [PATCH 01/27] refactor(BaseConfigService): remove unused usings --- .../Server/Services/BaseConfigurationService.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs index 27178b6a9..45b8a184a 100644 --- a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs @@ -1,8 +1,5 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Data.Sqlite; -using Microsoft.Net.Http.Headers; +using Microsoft.Data.Sqlite; using System.IO.Compression; -using System.Net; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Scheduling; @@ -10,7 +7,6 @@ using TeslaSolarCharger.Shared.Dtos.BaseConfiguration; using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Enums; -using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; namespace TeslaSolarCharger.Server.Services; From b5b756006e76e71d789a1c179a189b7f24e40915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 27 Nov 2023 19:59:04 +0100 Subject: [PATCH 02/27] feat(EF): Add TeslaToken to database --- .../Contracts/ITeslaSolarChargerContext.cs | 1 + .../Entities/TeslaSolarCharger/TeslaToken.cs | 14 ++ .../TeslaSolarChargerContext.cs | 1 + .../20231127185708_AddTeslaToken.Designer.cs | 213 ++++++++++++++++++ .../20231127185708_AddTeslaToken.cs | 40 ++++ .../TeslaSolarChargerContextModelSnapshot.cs | 34 +++ 6 files changed, 303 insertions(+) create mode 100644 TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs diff --git a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs index 09fdea53a..0b446c78c 100644 --- a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs @@ -15,5 +15,6 @@ public interface ITeslaSolarChargerContext Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()); DatabaseFacade Database { get; } DbSet SpotPrices { get; set; } + DbSet TeslaTokens { get; set; } void RejectChanges(); } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs new file mode 100644 index 000000000..98cdceeaf --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Primitives; + +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class TeslaToken +{ + public int Id { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public string IdToken { get; set; } + public DateTime ExpiresAtUtc { get; set; } + public string State { get; set; } + public string TokenType { get; set; } +} diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index 4546c6b70..8a66e9d1f 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -12,6 +12,7 @@ public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext public DbSet HandledCharges { get; set; } = null!; public DbSet PowerDistributions { get; set; } = null!; public DbSet SpotPrices { get; set; } = null!; + public DbSet TeslaTokens { get; set; } = null!; // ReSharper disable once UnassignedGetOnlyAutoProperty public string DbPath { get; } diff --git a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs b/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs new file mode 100644 index 000000000..94fc33701 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs @@ -0,0 +1,213 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20231127185708_AddTeslaToken")] + partial class AddTeslaToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("IdToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TokenType") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TeslaTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs b/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs new file mode 100644 index 000000000..f21425f07 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddTeslaToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TeslaTokens", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccessToken = table.Column(type: "TEXT", nullable: false), + RefreshToken = table.Column(type: "TEXT", nullable: false), + IdToken = table.Column(type: "TEXT", nullable: false), + ExpiresAtUtc = table.Column(type: "TEXT", nullable: false), + State = table.Column(type: "TEXT", nullable: false), + TokenType = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeslaTokens", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TeslaTokens"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index 167cc6a6d..f3da55f4c 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -155,6 +155,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SpotPrices"); }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("IdToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TokenType") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TeslaTokens"); + }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => { b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") From 47a8e777ace135fc3404063d0c993ac30a7da800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 27 Nov 2023 19:59:34 +0100 Subject: [PATCH 03/27] feat(TeslaTokenService): add TeslaTokenService --- .../Server/Dtos/DtoTeslaToken.cs | 10 +++ TeslaSolarCharger/Server/Program.cs | 3 +- .../Server/ServiceCollectionExtensions.cs | 20 ++++-- .../Server/Services/TeslaFleetApiService.cs | 71 +++++++++++++++++++ .../Server/appsettings.Development.json | 1 + TeslaSolarCharger/Server/appsettings.json | 1 + 6 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs create mode 100644 TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs diff --git a/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs b/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs new file mode 100644 index 000000000..dc521d7f1 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs @@ -0,0 +1,10 @@ +namespace TeslaSolarCharger.Server.Dtos; + +public class DtoTeslaToken +{ + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public string IdToken { get; set; } + public DateTime ExpiresAtUtc { get; set; } + public string TokenType { get; set; } +} diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index e70e02fc1..9447161ab 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -21,7 +21,8 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddMyDependencies(); +var useFleetApi = configurationManager.GetValue("UseFleetApi"); +builder.Services.AddMyDependencies(useFleetApi); builder.Services.AddGridPriceProvider(); builder.Host.UseSerilog((context, configuration) => configuration diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 8c1da5182..d50eaea63 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -33,8 +33,9 @@ namespace TeslaSolarCharger.Server; public static class ServiceCollectionExtensions { - public static IServiceCollection AddMyDependencies(this IServiceCollection services) - => services + public static IServiceCollection AddMyDependencies(this IServiceCollection services, bool useFleetApi) + { + services .AddSingleton() .AddTransient() .AddTransient() @@ -52,7 +53,6 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddTransient() .AddSingleton() .AddSingleton() .AddSingleton() @@ -93,6 +93,16 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddSharedBackendDependencies() - ; + .AddSharedBackendDependencies(); + if (useFleetApi) + { + services.AddTransient(); + } + else + { + services.AddTransient(); + } + + return services; + } } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs new file mode 100644 index 000000000..e7486fe8f --- /dev/null +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -0,0 +1,71 @@ +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Dtos; +using TeslaSolarCharger.Server.MappingExtensions; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Server.Services; + +public class TeslaFleetApiService : ITeslaService +{ + private readonly ILogger _logger; + private readonly ITeslaSolarChargerContext _context; + private readonly IMapperConfigurationFactory _mapperConfigurationFactory; + private readonly IDateTimeProvider _dateTimeProvider; + + public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext context, + IMapperConfigurationFactory mapperConfigurationFactory, IDateTimeProvider dateTimeProvider) + { + _logger = logger; + _context = context; + _mapperConfigurationFactory = mapperConfigurationFactory; + _dateTimeProvider = dateTimeProvider; + } + + public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) + { + _logger.LogTrace("{method}({carId}, {startAmp}, {carState})", nameof(StartCharging), carId, startAmp, carState); + var token = await GetTeslaTokenAsync().ConfigureAwait(false); + throw new NotImplementedException(); + } + + public Task WakeUpCar(int carId) + { + throw new NotImplementedException(); + } + + public Task StopCharging(int carId) + { + throw new NotImplementedException(); + } + + public Task SetAmp(int carId, int amps) + { + throw new NotImplementedException(); + } + + public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) + { + throw new NotImplementedException(); + } + + private async Task GetTeslaTokenAsync() + { + var mapper = _mapperConfigurationFactory.Create(cfg => + { + cfg.CreateMap() + ; + }); + var currentDateTime = _dateTimeProvider.UtcNow(); + var token = await _context.TeslaTokens + .Where(t => t.ExpiresAtUtc > currentDateTime) + .OrderByDescending(t => t.ExpiresAtUtc) + .ProjectTo(mapper) + .FirstAsync().ConfigureAwait(false); + return token; + } +} diff --git a/TeslaSolarCharger/Server/appsettings.Development.json b/TeslaSolarCharger/Server/appsettings.Development.json index f208db1bb..9eb11a115 100644 --- a/TeslaSolarCharger/Server/appsettings.Development.json +++ b/TeslaSolarCharger/Server/appsettings.Development.json @@ -56,6 +56,7 @@ "TeslaMateDbPassword": "secret", "AllowCORS": true, "DisplayApiRequestCounter": true, + "UseFleetApi": true, "GridPriceProvider": { "EnergyProvider": "Tibber", "Octopus": { diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index e94baeeba..89e8845b3 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -59,6 +59,7 @@ "GeoFence": "Home", "DisplayApiRequestCounter": false, "IgnoreSslErrors": false, + "UseFleetApi": false, "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": { From 5f7d3d39fce16926c121faf63a8e5729e870219a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 27 Nov 2023 21:10:24 +0100 Subject: [PATCH 04/27] feat(TeslaFleetApi): Implement charge start, stop and amp set --- .../Server/Dtos/DtoVehicleCommandResult.cs | 7 ++ .../Server/Services/TeslaFleetApiService.cs | 68 +++++++++++++++---- 2 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs diff --git a/TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs b/TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs new file mode 100644 index 000000000..ef0386a1d --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs @@ -0,0 +1,7 @@ +namespace TeslaSolarCharger.Server.Dtos; + +public class DtoVehicleCommandResult +{ + bool Result { get; set; } + string Reason { get; set; } +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index e7486fe8f..124c6e512 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,11 +1,13 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; +using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.MappingExtensions; using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; namespace TeslaSolarCharger.Server.Services; @@ -13,44 +15,74 @@ namespace TeslaSolarCharger.Server.Services; public class TeslaFleetApiService : ITeslaService { private readonly ILogger _logger; - private readonly ITeslaSolarChargerContext _context; + private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; private readonly IMapperConfigurationFactory _mapperConfigurationFactory; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ITeslamateContext _teslamateContext; - public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext context, - IMapperConfigurationFactory mapperConfigurationFactory, IDateTimeProvider dateTimeProvider) + private readonly string _chargeStartComand = "command/charge_start"; + private readonly string _chargeStopComand = "command/charge_stop"; + private readonly string _setChargingAmps = "command/set_charging_amps"; + private readonly string _wakeUpComand = "wake_up"; + + public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, + IMapperConfigurationFactory mapperConfigurationFactory, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext) { _logger = logger; - _context = context; + _teslaSolarChargerContext = teslaSolarChargerContext; _mapperConfigurationFactory = mapperConfigurationFactory; _dateTimeProvider = dateTimeProvider; + _teslamateContext = teslamateContext; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) { _logger.LogTrace("{method}({carId}, {startAmp}, {carState})", nameof(StartCharging), carId, startAmp, carState); var token = await GetTeslaTokenAsync().ConfigureAwait(false); - throw new NotImplementedException(); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var result = await SendCommandToTeslaApi(token, id, _chargeStartComand).ConfigureAwait(false); } - public Task WakeUpCar(int carId) + + public async Task WakeUpCar(int carId) { - throw new NotImplementedException(); + _logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); + var token = await GetTeslaTokenAsync().ConfigureAwait(false); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var result = await SendCommandToTeslaApi(token, id, _wakeUpComand).ConfigureAwait(false); } - public Task StopCharging(int carId) + public async Task StopCharging(int carId) { - throw new NotImplementedException(); + _logger.LogTrace("{method}({carId})", nameof(StopCharging), carId); + var token = await GetTeslaTokenAsync().ConfigureAwait(false); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var result = await SendCommandToTeslaApi(token, id, _chargeStopComand).ConfigureAwait(false); } - public Task SetAmp(int carId, int amps) + public async Task SetAmp(int carId, int amps) { - throw new NotImplementedException(); + _logger.LogTrace("{method}({carId}, {amps})", nameof(SetAmp), carId, amps); + var token = await GetTeslaTokenAsync().ConfigureAwait(false); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var commandData = $"{{\"charging_amps\":{amps}}}"; + var result = await SendCommandToTeslaApi(token, id, _setChargingAmps, commandData).ConfigureAwait(false); } public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) { - throw new NotImplementedException(); + _logger.LogError("This is currently not supported with Fleet API"); + return Task.CompletedTask; + } + + private static async Task SendCommandToTeslaApi(DtoTeslaToken token, long id, string commandName, string contentData = "{}") + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); + var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json"); + var requestUri = $"https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; + var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } private async Task GetTeslaTokenAsync() @@ -61,11 +93,21 @@ private async Task GetTeslaTokenAsync() ; }); var currentDateTime = _dateTimeProvider.UtcNow(); - var token = await _context.TeslaTokens + var token = await _teslaSolarChargerContext.TeslaTokens .Where(t => t.ExpiresAtUtc > currentDateTime) .OrderByDescending(t => t.ExpiresAtUtc) .ProjectTo(mapper) .FirstAsync().ConfigureAwait(false); return token; } + + //Just to look up code, not used. + //private async Task GetAllAccountVehicles(DtoTeslaToken token) + //{ + // using var httpClient = new HttpClient(); + // var request = new HttpRequestMessage(HttpMethod.Get, $"https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles"); + // request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); + // var vehicleResponse = await httpClient.SendAsync(request).ConfigureAwait(false); + // var responseString = await vehicleResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + //} } From 8390a1f8d4256d52f11e3000e965ebe474bccfad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 28 Nov 2023 00:03:56 +0100 Subject: [PATCH 05/27] feat(TeslaFleetApiService): can refresh token --- .../Server/Dtos/DtoTeslaFleetApiToken.cs | 21 ++++++ .../Server/Services/TeslaFleetApiService.cs | 65 +++++++++++-------- TeslaSolarCharger/Server/appsettings.json | 1 + .../Shared/Contracts/IConfigurationWrapper.cs | 1 + .../Shared/Wrappers/ConfigurationWrapper.cs | 7 ++ 5 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs diff --git a/TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs b/TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs new file mode 100644 index 000000000..c73018945 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace TeslaSolarCharger.Server.Dtos; + +public class DtoTeslaFleetApiToken +{ + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + + [JsonProperty("id_token")] + public string IdToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 124c6e512..de683e44d 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,13 +1,10 @@ -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; -using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; -using TeslaSolarCharger.Server.MappingExtensions; using TeslaSolarCharger.Shared.Contracts; -using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; namespace TeslaSolarCharger.Server.Services; @@ -16,9 +13,9 @@ public class TeslaFleetApiService : ITeslaService { private readonly ILogger _logger; private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; - private readonly IMapperConfigurationFactory _mapperConfigurationFactory; private readonly IDateTimeProvider _dateTimeProvider; private readonly ITeslamateContext _teslamateContext; + private readonly IConfigurationWrapper _configurationWrapper; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; @@ -26,47 +23,43 @@ public class TeslaFleetApiService : ITeslaService private readonly string _wakeUpComand = "wake_up"; public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, - IMapperConfigurationFactory mapperConfigurationFactory, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext) + IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; - _mapperConfigurationFactory = mapperConfigurationFactory; _dateTimeProvider = dateTimeProvider; _teslamateContext = teslamateContext; + _configurationWrapper = configurationWrapper; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) { _logger.LogTrace("{method}({carId}, {startAmp}, {carState})", nameof(StartCharging), carId, startAmp, carState); - var token = await GetTeslaTokenAsync().ConfigureAwait(false); var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); - var result = await SendCommandToTeslaApi(token, id, _chargeStartComand).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(id, _chargeStartComand).ConfigureAwait(false); } public async Task WakeUpCar(int carId) { _logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); - var token = await GetTeslaTokenAsync().ConfigureAwait(false); var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); - var result = await SendCommandToTeslaApi(token, id, _wakeUpComand).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(id, _wakeUpComand).ConfigureAwait(false); } public async Task StopCharging(int carId) { _logger.LogTrace("{method}({carId})", nameof(StopCharging), carId); - var token = await GetTeslaTokenAsync().ConfigureAwait(false); var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); - var result = await SendCommandToTeslaApi(token, id, _chargeStopComand).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(id, _chargeStopComand).ConfigureAwait(false); } public async Task SetAmp(int carId, int amps) { _logger.LogTrace("{method}({carId}, {amps})", nameof(SetAmp), carId, amps); - var token = await GetTeslaTokenAsync().ConfigureAwait(false); var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); var commandData = $"{{\"charging_amps\":{amps}}}"; - var result = await SendCommandToTeslaApi(token, id, _setChargingAmps, commandData).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(id, _setChargingAmps, commandData).ConfigureAwait(false); } public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) @@ -75,30 +68,46 @@ public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) return Task.CompletedTask; } - private static async Task SendCommandToTeslaApi(DtoTeslaToken token, long id, string commandName, string contentData = "{}") + private async Task SendCommandToTeslaApi(long id, string commandName, string contentData = "{}") { + var accessToken = await GetAccessTokenAsync().ConfigureAwait(false); using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json"); var requestUri = $"https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } - private async Task GetTeslaTokenAsync() + private async Task GetAccessTokenAsync() { - var mapper = _mapperConfigurationFactory.Create(cfg => - { - cfg.CreateMap() - ; - }); - var currentDateTime = _dateTimeProvider.UtcNow(); + _logger.LogTrace("{method}()", nameof(GetAccessTokenAsync)); var token = await _teslaSolarChargerContext.TeslaTokens - .Where(t => t.ExpiresAtUtc > currentDateTime) .OrderByDescending(t => t.ExpiresAtUtc) - .ProjectTo(mapper) .FirstAsync().ConfigureAwait(false); - return token; + var minimumTokenLifeTime = TimeSpan.FromMinutes(5); + if (token.ExpiresAtUtc < (_dateTimeProvider.UtcNow() + minimumTokenLifeTime)) + { + using var httpClient = new HttpClient(); + var tokenUrl = "https://auth.tesla.com/oauth2/v3/token"; + var requestData = new Dictionary + { + { "grant_type", "refresh_token" }, + { "client_id", _configurationWrapper.FleetApiClientId() }, + { "refresh_token", token.RefreshToken }, + }; + var encodedContent = new FormUrlEncodedContent(requestData); + encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); + token.AccessToken = newToken.AccessToken; + token.RefreshToken = newToken.RefreshToken; + token.IdToken = newToken.IdToken; + token.ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(newToken.ExpiresIn); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } + return token.AccessToken; } //Just to look up code, not used. diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 89e8845b3..82d7d9b31 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -60,6 +60,7 @@ "DisplayApiRequestCounter": false, "IgnoreSslErrors": false, "UseFleetApi": false, + "FleetApiClientId": "f29f71d6285a-4873-8b6b-80f15854892e", "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": { diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 67e3897d5..713b26a80 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -86,4 +86,5 @@ public interface IConfigurationWrapper string BackupCopyDestinationDirectory(); string GetSqliteFileNameWithoutPath(); string BackupZipDirectory(); + string FleetApiClientId(); } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index 491e5bd77..3820c9367 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -99,6 +99,13 @@ public bool AllowCors() return value; } + public string FleetApiClientId() + { + var environmentVariableName = "FleetApiClientId"; + var value = GetNotNullableConfigurationValue(environmentVariableName); + return value; + } + public TimeSpan ChargingValueJobUpdateIntervall() { var minimum = TimeSpan.FromSeconds(20); From a0ce117884e944e02dbd56883324ef6ac63df4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 28 Nov 2023 01:02:22 +0100 Subject: [PATCH 06/27] feat(TeslaFleetApiService): allow adding new Tokens --- .../Entities/TeslaSolarCharger/TeslaToken.cs | 4 +- .../Enums/TeslaFleetApiRegion.cs | 7 +++ ... 20231127233402_AddTeslaToken.Designer.cs} | 11 ++--- ...ken.cs => 20231127233402_AddTeslaToken.cs} | 3 +- .../TeslaSolarChargerContextModelSnapshot.cs | 9 +--- .../TeslaSolarCharger.Model.csproj | 4 ++ .../Server/Controllers/ConfigController.cs | 16 +++++-- .../Server/Dtos/DtoTeslaToken.cs | 10 ---- .../DtoTeslaFleetApiRefreshToken.cs} | 7 +-- .../DtoVehicleCommandResult.cs | 2 +- TeslaSolarCharger/Server/Program.cs | 2 +- .../Server/ServiceCollectionExtensions.cs | 1 + .../Contracts/ITeslaFleetApiService.cs | 10 ++++ .../Server/Services/TeslaFleetApiService.cs | 48 +++++++++++++------ 14 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 TeslaSolarCharger.Model/Enums/TeslaFleetApiRegion.cs rename TeslaSolarCharger.Model/Migrations/{20231127185708_AddTeslaToken.Designer.cs => 20231127233402_AddTeslaToken.Designer.cs} (95%) rename TeslaSolarCharger.Model/Migrations/{20231127185708_AddTeslaToken.cs => 20231127233402_AddTeslaToken.cs} (88%) delete mode 100644 TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs rename TeslaSolarCharger/Server/Dtos/{DtoTeslaFleetApiToken.cs => TeslaFleetApi/DtoTeslaFleetApiRefreshToken.cs} (68%) rename TeslaSolarCharger/Server/Dtos/{ => TeslaFleetApi}/DtoVehicleCommandResult.cs (64%) create mode 100644 TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs index 98cdceeaf..23356bf02 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Primitives; +using TeslaSolarCharger.Model.Enums; namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; @@ -9,6 +10,5 @@ public class TeslaToken public string RefreshToken { get; set; } public string IdToken { get; set; } public DateTime ExpiresAtUtc { get; set; } - public string State { get; set; } - public string TokenType { get; set; } + public TeslaFleetApiRegion Region { get; set; } } diff --git a/TeslaSolarCharger.Model/Enums/TeslaFleetApiRegion.cs b/TeslaSolarCharger.Model/Enums/TeslaFleetApiRegion.cs new file mode 100644 index 000000000..f58546ba3 --- /dev/null +++ b/TeslaSolarCharger.Model/Enums/TeslaFleetApiRegion.cs @@ -0,0 +1,7 @@ +namespace TeslaSolarCharger.Model.Enums; + +public enum TeslaFleetApiRegion +{ + Emea, + NorthAmerica, +} diff --git a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs b/TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.Designer.cs similarity index 95% rename from TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs rename to TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.Designer.cs index 94fc33701..e008509cb 100644 --- a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.Designer.cs +++ b/TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.Designer.cs @@ -11,7 +11,7 @@ namespace TeslaSolarCharger.Model.Migrations { [DbContext(typeof(TeslaSolarChargerContext))] - [Migration("20231127185708_AddTeslaToken")] + [Migration("20231127233402_AddTeslaToken")] partial class AddTeslaToken { /// @@ -179,13 +179,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("State") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TokenType") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Region") + .HasColumnType("INTEGER"); b.HasKey("Id"); diff --git a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs b/TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.cs similarity index 88% rename from TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs rename to TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.cs index f21425f07..a4010fb25 100644 --- a/TeslaSolarCharger.Model/Migrations/20231127185708_AddTeslaToken.cs +++ b/TeslaSolarCharger.Model/Migrations/20231127233402_AddTeslaToken.cs @@ -21,8 +21,7 @@ protected override void Up(MigrationBuilder migrationBuilder) RefreshToken = table.Column(type: "TEXT", nullable: false), IdToken = table.Column(type: "TEXT", nullable: false), ExpiresAtUtc = table.Column(type: "TEXT", nullable: false), - State = table.Column(type: "TEXT", nullable: false), - TokenType = table.Column(type: "TEXT", nullable: false) + Region = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index f3da55f4c..1018d57e2 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -176,13 +176,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("State") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TokenType") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Region") + .HasColumnType("INTEGER"); b.HasKey("Id"); diff --git a/TeslaSolarCharger.Model/TeslaSolarCharger.Model.csproj b/TeslaSolarCharger.Model/TeslaSolarCharger.Model.csproj index 9752af60e..59adf1234 100644 --- a/TeslaSolarCharger.Model/TeslaSolarCharger.Model.csproj +++ b/TeslaSolarCharger.Model/TeslaSolarCharger.Model.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/TeslaSolarCharger/Server/Controllers/ConfigController.cs b/TeslaSolarCharger/Server/Controllers/ConfigController.cs index 16e66961d..793daadf3 100644 --- a/TeslaSolarCharger/Server/Controllers/ConfigController.cs +++ b/TeslaSolarCharger/Server/Controllers/ConfigController.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Dtos; +using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Dtos.Settings; @@ -10,10 +14,12 @@ namespace TeslaSolarCharger.Server.Controllers public class ConfigController : ApiBaseController { private readonly IConfigService _service; + private readonly ITeslaFleetApiService _teslaFleetApiService; - public ConfigController(IConfigService service) + public ConfigController(IConfigService service, ITeslaFleetApiService teslaFleetApiService) { _service = service; + _teslaFleetApiService = teslaFleetApiService; } /// @@ -28,7 +34,7 @@ public ConfigController(IConfigService service) /// Car Id of car to update /// Car Configuration which should be set to car [HttpPut] - public void UpdateCarConfiguration(int carId, [FromBody] CarConfiguration carConfiguration) => + public Task UpdateCarConfiguration(int carId, [FromBody] CarConfiguration carConfiguration) => _service.UpdateCarConfiguration(carId, carConfiguration); /// @@ -43,7 +49,11 @@ public void UpdateCarConfiguration(int carId, [FromBody] CarConfiguration carCon /// Car Id of car to update /// Car Configuration which should be set to car [HttpPut] - public void UpdateCarBasicConfiguration(int carId, [FromBody] CarBasicConfiguration carBasicConfiguration) => + public Task UpdateCarBasicConfiguration(int carId, [FromBody] CarBasicConfiguration carBasicConfiguration) => _service.UpdateCarBasicConfiguration(carId, carBasicConfiguration); + + [HttpPost] + public Task AddTeslaFleetApiToken([FromBody]DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) => + _teslaFleetApiService.AddNewTokenAsync(token, region); } } diff --git a/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs b/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs deleted file mode 100644 index dc521d7f1..000000000 --- a/TeslaSolarCharger/Server/Dtos/DtoTeslaToken.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace TeslaSolarCharger.Server.Dtos; - -public class DtoTeslaToken -{ - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string IdToken { get; set; } - public DateTime ExpiresAtUtc { get; set; } - public string TokenType { get; set; } -} diff --git a/TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoTeslaFleetApiRefreshToken.cs similarity index 68% rename from TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs rename to TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoTeslaFleetApiRefreshToken.cs index c73018945..afd86d893 100644 --- a/TeslaSolarCharger/Server/Dtos/DtoTeslaFleetApiToken.cs +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoTeslaFleetApiRefreshToken.cs @@ -1,8 +1,8 @@ using Newtonsoft.Json; -namespace TeslaSolarCharger.Server.Dtos; +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; -public class DtoTeslaFleetApiToken +public class DtoTeslaFleetApiRefreshToken { [JsonProperty("access_token")] public string AccessToken { get; set; } @@ -15,7 +15,4 @@ public class DtoTeslaFleetApiToken [JsonProperty("expires_in")] public int ExpiresIn { get; set; } - - [JsonProperty("token_type")] - public string TokenType { get; set; } } diff --git a/TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs similarity index 64% rename from TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs rename to TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs index ef0386a1d..2ed828c5c 100644 --- a/TeslaSolarCharger/Server/Dtos/DtoVehicleCommandResult.cs +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs @@ -1,4 +1,4 @@ -namespace TeslaSolarCharger.Server.Dtos; +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; public class DtoVehicleCommandResult { diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 9447161ab..54ab176f2 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -14,7 +14,7 @@ // Add services to the container. builder.Services.AddControllersWithViews(); -builder.Services.AddControllers(); +builder.Services.AddControllers().AddNewtonsoftJson(); builder.Services.AddRazorPages(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index d50eaea63..82d7fe05c 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -93,6 +93,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs new file mode 100644 index 000000000..025b1ce78 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -0,0 +1,10 @@ +using TeslaSolarCharger.Model.Enums; +using TeslaSolarCharger.Server.Dtos; +using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; + +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface ITeslaFleetApiService +{ + public Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region); +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index de683e44d..573bcd630 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -2,14 +2,18 @@ using Newtonsoft.Json; using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; +using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Enums; namespace TeslaSolarCharger.Server.Services; -public class TeslaFleetApiService : ITeslaService +public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService { private readonly ILogger _logger; private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; @@ -72,14 +76,20 @@ public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) { var accessToken = await GetAccessTokenAsync().ConfigureAwait(false); using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json"); - var requestUri = $"https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; + var regionCode = accessToken.Region switch + { + TeslaFleetApiRegion.Emea => "eu", + TeslaFleetApiRegion.NorthAmerica => "na", + _ => throw new NotImplementedException($"Region {accessToken.Region} is not implemented."), + }; + var requestUri = $"https://fleet-api.prd.{regionCode}.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } - private async Task GetAccessTokenAsync() + private async Task GetAccessTokenAsync() { _logger.LogTrace("{method}()", nameof(GetAccessTokenAsync)); var token = await _teslaSolarChargerContext.TeslaTokens @@ -88,6 +98,7 @@ private async Task GetAccessTokenAsync() var minimumTokenLifeTime = TimeSpan.FromMinutes(5); if (token.ExpiresAtUtc < (_dateTimeProvider.UtcNow() + minimumTokenLifeTime)) { + _logger.LogInformation("Token is expired. Getting new token."); using var httpClient = new HttpClient(); var tokenUrl = "https://auth.tesla.com/oauth2/v3/token"; var requestData = new Dictionary @@ -100,23 +111,30 @@ private async Task GetAccessTokenAsync() encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); + var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); token.AccessToken = newToken.AccessToken; token.RefreshToken = newToken.RefreshToken; token.IdToken = newToken.IdToken; token.ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(newToken.ExpiresIn); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + _logger.LogInformation("New Token saved to database."); } - return token.AccessToken; + return token; } - //Just to look up code, not used. - //private async Task GetAllAccountVehicles(DtoTeslaToken token) - //{ - // using var httpClient = new HttpClient(); - // var request = new HttpRequestMessage(HttpMethod.Get, $"https://fleet-api.prd.eu.vn.cloud.tesla.com/api/1/vehicles"); - // request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - // var vehicleResponse = await httpClient.SendAsync(request).ConfigureAwait(false); - // var responseString = await vehicleResponse.Content.ReadAsStringAsync().ConfigureAwait(false); - //} + public async Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) + { + var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TeslaTokens.Add(new TeslaToken + { + AccessToken = token.AccessToken, + RefreshToken = token.RefreshToken, + IdToken = token.IdToken, + ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), + Region = region, + }); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } } From 87974947121918b368f61c22808ae9257eb3aab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 28 Nov 2023 17:11:55 +0100 Subject: [PATCH 07/27] fix(BaseConfigurationService): Do not backup carConfig.json as is not used --- TeslaSolarCharger/Server/Services/BaseConfigurationService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs index 45b8a184a..3fe2ec9d4 100644 --- a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs @@ -99,9 +99,7 @@ public async Task DownloadBackup() //Backup config files var baseConfigFileFullName = _configurationWrapper.BaseConfigFileFullName(); - var carConfigFileFullName = _configurationWrapper.CarConfigFileFullName(); File.Copy(baseConfigFileFullName, Path.Combine(backupCopyDestinationDirectory, Path.GetFileName(baseConfigFileFullName)), true); - File.Copy(carConfigFileFullName, Path.Combine(backupCopyDestinationDirectory, Path.GetFileName(carConfigFileFullName)), true); var backupFileName = "TSC-Backup.zip"; From 7e46ab7672acecd412500d68132ed543eefac5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 29 Nov 2023 09:22:55 +0100 Subject: [PATCH 08/27] feat(TeslaFleetApiService): log responses --- TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 573bcd630..8300e00d3 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -74,6 +74,7 @@ public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) private async Task SendCommandToTeslaApi(long id, string commandName, string contentData = "{}") { + _logger.LogTrace("{method}({id}, {commandName}, {contentData})", nameof(SendCommandToTeslaApi), id, commandName, contentData); var accessToken = await GetAccessTokenAsync().ConfigureAwait(false); using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); @@ -86,6 +87,8 @@ public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) }; var requestUri = $"https://fleet-api.prd.{regionCode}.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogDebug("Response: {responseString}", responseString); return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } From 5b7154b39d3b19c8cd0038ca64a08444feb7f6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 29 Nov 2023 09:37:33 +0100 Subject: [PATCH 09/27] feat(TeslaFleetApiService): wake up car if needed and resume teslamate logging --- .../Server/ServiceCollectionExtensions.cs | 1 + .../Contracts/ITeslamateApiService.cs | 6 ++++ .../Server/Services/TeslaFleetApiService.cs | 31 ++++++++++++++++++- .../Server/Services/TeslamateApiService.cs | 5 +-- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITeslamateApiService.cs diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 82d7fe05c..3686c3589 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -94,6 +94,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITeslamateApiService.cs b/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITeslamateApiService.cs new file mode 100644 index 000000000..e40103129 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITeslamateApiService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Server.Services.ApiServices.Contracts; + +public interface ITeslamateApiService +{ + Task ResumeLogging(int carId); +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 8300e00d3..79d8d47fd 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -7,6 +7,7 @@ using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Services.ApiServices.Contracts; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Enums; @@ -20,6 +21,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly IDateTimeProvider _dateTimeProvider; private readonly ITeslamateContext _teslamateContext; private readonly IConfigurationWrapper _configurationWrapper; + private readonly ITeslamateApiService _teslamateApiService; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; @@ -27,18 +29,27 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly string _wakeUpComand = "wake_up"; public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, - IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper) + IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper, + ITeslamateApiService teslamateApiService) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; _dateTimeProvider = dateTimeProvider; _teslamateContext = teslamateContext; _configurationWrapper = configurationWrapper; + _teslamateApiService = teslamateApiService; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) { _logger.LogTrace("{method}({carId}, {startAmp}, {carState})", nameof(StartCharging), carId, startAmp, carState); + if (startAmp == 0) + { + _logger.LogDebug("Should start charging with 0 amp. Skipping charge start."); + return; + } + await WakeUpCarIfNeeded(carId, carState).ConfigureAwait(false); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); var result = await SendCommandToTeslaApi(id, _chargeStartComand).ConfigureAwait(false); } @@ -49,6 +60,9 @@ public async Task WakeUpCar(int carId) _logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); var result = await SendCommandToTeslaApi(id, _wakeUpComand).ConfigureAwait(false); + await _teslamateApiService.ResumeLogging(carId).ConfigureAwait(false); + + await Task.Delay(TimeSpan.FromSeconds(20)).ConfigureAwait(false); } public async Task StopCharging(int carId) @@ -72,6 +86,21 @@ public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) return Task.CompletedTask; } + private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) + { + switch (carState) + { + case CarStateEnum.Offline or CarStateEnum.Asleep: + _logger.LogInformation("Wakeup car."); + await WakeUpCar(carId).ConfigureAwait(false); + break; + case CarStateEnum.Suspended: + _logger.LogInformation("Resume logging as is suspended"); + await _teslamateApiService.ResumeLogging(carId).ConfigureAwait(false); + break; + } + } + private async Task SendCommandToTeslaApi(long id, string commandName, string contentData = "{}") { _logger.LogTrace("{method}({id}, {commandName}, {contentData})", nameof(SendCommandToTeslaApi), id, commandName, contentData); diff --git a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs index 9dca541b3..db196438b 100644 --- a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs @@ -1,6 +1,7 @@ using System.Text; using Newtonsoft.Json; using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Services.ApiServices.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Dtos.Settings; @@ -8,7 +9,7 @@ namespace TeslaSolarCharger.Server.Services; -public class TeslamateApiService : ITeslaService +public class TeslamateApiService : ITeslaService, ITeslamateApiService { private readonly ILogger _logger; private readonly ITelegramService _telegramService; @@ -230,7 +231,7 @@ internal DateTimeOffset RoundToNextQuarterHour(DateTimeOffset chargingStartTime) return chargingStartTime; } - private async Task ResumeLogging(int carId) + public async Task ResumeLogging(int carId) { _logger.LogTrace("{method}({param1})", nameof(ResumeLogging), carId); var url = $"{_teslaMateBaseUrl}/api/v1/cars/{carId}/logging/resume"; From 75e223ad15115a7f9f846957e5a4f0b9834e30cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Fri, 1 Dec 2023 12:02:09 +0100 Subject: [PATCH 10/27] feat(Index.razor): Display installation Id --- .../Contracts/ITeslaSolarChargerContext.cs | 1 + .../TeslaSolarCharger/TscConfiguration.cs | 8 + .../TeslaSolarChargerContext.cs | 1 + ...201103452_AddTscConfigurations.Designer.cs | 226 ++++++++++++++++++ .../20231201103452_AddTscConfigurations.cs | 35 +++ .../TeslaSolarChargerContextModelSnapshot.cs | 18 ++ .../Contracts/IConstants.cs | 2 + .../Values/Constants.cs | 2 + TeslaSolarCharger/Client/Pages/Index.razor | 3 + .../Server/Contracts/ICoreService.cs | 1 + .../Server/Controllers/HelloController.cs | 3 + TeslaSolarCharger/Server/Program.cs | 5 + .../Server/ServiceCollectionExtensions.cs | 1 + .../Contracts/ITscConfigurationService.cs | 6 + .../Server/Services/CoreService.cs | 13 +- .../Services/TscConfigurationService.cs | 43 ++++ 16 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TscConfiguration.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.cs create mode 100644 TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs create mode 100644 TeslaSolarCharger/Server/Services/TscConfigurationService.cs diff --git a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs index 0b446c78c..67cff6814 100644 --- a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs @@ -16,5 +16,6 @@ public interface ITeslaSolarChargerContext DatabaseFacade Database { get; } DbSet SpotPrices { get; set; } DbSet TeslaTokens { get; set; } + DbSet TscConfigurations { get; set; } void RejectChanges(); } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TscConfiguration.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TscConfiguration.cs new file mode 100644 index 000000000..7d413c7d1 --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TscConfiguration.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class TscConfiguration +{ + public int Id { get; set; } + public string Key { get; set; } + public string? Value { get; set; } +} diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index 8a66e9d1f..c4f9c6e2b 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -13,6 +13,7 @@ public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext public DbSet PowerDistributions { get; set; } = null!; public DbSet SpotPrices { get; set; } = null!; public DbSet TeslaTokens { get; set; } = null!; + public DbSet TscConfigurations { get; set; } = null!; // ReSharper disable once UnassignedGetOnlyAutoProperty public string DbPath { get; } diff --git a/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.Designer.cs b/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.Designer.cs new file mode 100644 index 000000000..0548a9c5e --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.Designer.cs @@ -0,0 +1,226 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20231201103452_AddTscConfigurations")] + partial class AddTscConfigurations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("IdToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Region") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TeslaTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TscConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.cs b/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.cs new file mode 100644 index 000000000..8842a2963 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231201103452_AddTscConfigurations.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddTscConfigurations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TscConfigurations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Key = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TscConfigurations", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TscConfigurations"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index 1018d57e2..d11b2cbb7 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -184,6 +184,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("TeslaTokens"); }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TscConfigurations"); + }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => { b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") diff --git a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs index 38d113c4f..6d664966b 100644 --- a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs +++ b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs @@ -10,4 +10,6 @@ public interface IConstants /// Soc Difference needs to be higher than this value /// int MinimumSocDifference { get; } + + string InstallationIdKey { get; } } diff --git a/TeslaSolarCharger.SharedBackend/Values/Constants.cs b/TeslaSolarCharger.SharedBackend/Values/Constants.cs index f92c3260f..205f1ebb8 100644 --- a/TeslaSolarCharger.SharedBackend/Values/Constants.cs +++ b/TeslaSolarCharger.SharedBackend/Values/Constants.cs @@ -9,4 +9,6 @@ public class Constants : IConstants public int MinSocLimit => 50; public int DefaultOverage => -1000000; public int MinimumSocDifference => 2; + + public string InstallationIdKey => "InstallationId"; } diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index 0578955f7..ef1562149 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -358,6 +358,7 @@ else {

Version: @_version

} +
@_installationId
@@ -394,6 +395,7 @@ else private string? _serverTimeZoneDisplayName; private bool? _shouldDisplayApiRequestCounter; private int? _apiRequestCount; + private string _installationId = ""; private Timer? _timer; @@ -410,6 +412,7 @@ else var dtoSolarChargerInstallation = await HttpClient.GetFromJsonAsync>("api/Hello/IsSolarEdgeInstallation").ConfigureAwait(false); _isSolarEdgeInstallation = dtoSolarChargerInstallation?.Value; _version = await HttpClient.GetStringAsync("api/Hello/ProductVersion").ConfigureAwait(false); + _installationId = await HttpClient.GetStringAsync("api/Hello/GetInstallationId").ConfigureAwait(false); foreach (var carBaseState in _carBaseStates!) { _collapsedCarDetails.Add(carBaseState.CarId); diff --git a/TeslaSolarCharger/Server/Contracts/ICoreService.cs b/TeslaSolarCharger/Server/Contracts/ICoreService.cs index 706836fb9..8b5572642 100644 --- a/TeslaSolarCharger/Server/Contracts/ICoreService.cs +++ b/TeslaSolarCharger/Server/Contracts/ICoreService.cs @@ -19,4 +19,5 @@ public interface ICoreService DtoValue TeslaApiRequestsSinceStartup(); DtoValue ShouldDisplayApiRequestCounter(); Task> GetPriceData(DateTimeOffset from, DateTimeOffset to); + Task GetInstallationId(); } diff --git a/TeslaSolarCharger/Server/Controllers/HelloController.cs b/TeslaSolarCharger/Server/Controllers/HelloController.cs index f1fe03f4c..2e9a6a23d 100644 --- a/TeslaSolarCharger/Server/Controllers/HelloController.cs +++ b/TeslaSolarCharger/Server/Controllers/HelloController.cs @@ -53,5 +53,8 @@ public HelloController(ICoreService coreService) [HttpGet] public Task> GetPriceData(DateTimeOffset from, DateTimeOffset to) => _coreService.GetPriceData(from, to); + + [HttpGet] + public Task GetInstallationId() => _coreService.GetInstallationId(); } } diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 54ab176f2..118c2f2e7 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -5,6 +5,7 @@ using TeslaSolarCharger.Server; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Scheduling; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; var builder = WebApplication.CreateBuilder(args); @@ -70,6 +71,10 @@ var teslaSolarChargerContext = app.Services.GetRequiredService(); await teslaSolarChargerContext.Database.MigrateAsync().ConfigureAwait(false); + var tscConfigurationService = app.Services.GetRequiredService(); + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + logger.LogTrace("Installation Id: {installationId}", installationId); + var chargingCostService = app.Services.GetRequiredService(); await chargingCostService.DeleteDuplicatedHandleCharges().ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 3686c3589..0676bcbc5 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -95,6 +95,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs new file mode 100644 index 000000000..55e694179 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface ITscConfigurationService +{ + Task GetInstallationId(); +} diff --git a/TeslaSolarCharger/Server/Services/CoreService.cs b/TeslaSolarCharger/Server/Services/CoreService.cs index d43c9f8f8..5851910c1 100644 --- a/TeslaSolarCharger/Server/Services/CoreService.cs +++ b/TeslaSolarCharger/Server/Services/CoreService.cs @@ -2,8 +2,10 @@ using System.Reflection; using TeslaSolarCharger.GridPriceProvider.Data; using TeslaSolarCharger.GridPriceProvider.Services.Interfaces; +using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Scheduling; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Dtos.Contracts; @@ -22,11 +24,12 @@ public class CoreService : ICoreService private readonly ISolarMqttService _solarMqttService; private readonly ISettings _settings; private readonly IFixedPriceService _fixedPriceService; + private readonly ITscConfigurationService _tscConfigurationService; public CoreService(ILogger logger, IChargingService chargingService, IConfigurationWrapper configurationWrapper, IDateTimeProvider dateTimeProvider, IConfigJsonService configJsonService, JobManager jobManager, ITeslaMateMqttService teslaMateMqttService, ISolarMqttService solarMqttService, ISettings settings, - IFixedPriceService fixedPriceService) + IFixedPriceService fixedPriceService, ITscConfigurationService tscConfigurationService) { _logger = logger; _chargingService = chargingService; @@ -38,6 +41,7 @@ public CoreService(ILogger logger, IChargingService chargingService _solarMqttService = solarMqttService; _settings = settings; _fixedPriceService = fixedPriceService; + _tscConfigurationService = tscConfigurationService; } public Task GetCurrentVersion() @@ -169,4 +173,11 @@ public Task> GetPriceData(DateTimeOffset from, DateTimeOffset _logger.LogTrace("{method}({from}, {to})", nameof(GetPriceData), from, to); return _fixedPriceService.GetPriceData(from, to, null); } + + public async Task GetInstallationId() + { + _logger.LogTrace("{method}()", nameof(GetInstallationId)); + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + return installationId.ToString(); + } } diff --git a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs new file mode 100644 index 000000000..d8ec2627e --- /dev/null +++ b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs @@ -0,0 +1,43 @@ +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.SharedBackend.Contracts; + +namespace TeslaSolarCharger.Server.Services; + +public class TscConfigurationService : ITscConfigurationService +{ + private readonly ILogger _logger; + private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; + private readonly IConstants _constants; + + public TscConfigurationService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants) + { + _logger = logger; + _teslaSolarChargerContext = teslaSolarChargerContext; + _constants = constants; + } + + public async Task GetInstallationId() + { + _logger.LogTrace("{method}()", nameof(GetInstallationId)); + var configurationIdString = _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.InstallationIdKey) + .Select(c => c.Value) + .FirstOrDefault(); + + if (configurationIdString != default) + { + return Guid.Parse(configurationIdString); + } + + var installationIdConfiguration = new TscConfiguration() + { + Key = _constants.InstallationIdKey, + Value = Guid.NewGuid().ToString(), + }; + _teslaSolarChargerContext.TscConfigurations.Add(installationIdConfiguration); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + return Guid.Parse(installationIdConfiguration.Value); + } +} From 50694a0e369fed844041c2f2f836b4eef11481b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 3 Dec 2023 16:27:14 +0100 Subject: [PATCH 11/27] feat(TeslaFleetApiService): add UI for fleet api --- .../Contracts/IConstants.cs | 1 + .../Values/Constants.cs | 1 + .../Services/Server/BackendApiService.cs | 31 ++++++++ .../Client/Pages/BaseConfiguration.razor | 75 ++++++++++++++++--- TeslaSolarCharger/Client/Pages/Index.razor | 3 + .../Server/Controllers/FleetApiController.cs | 25 +++++++ .../DtoTeslaOAuthRequestInformation.cs | 11 +++ .../Dtos/TscBackend/TeslaTscDeliveryToken.cs | 11 +++ .../Server/ServiceCollectionExtensions.cs | 1 + .../Server/Services/BackendApiService.cs | 42 +++++++++++ .../Services/Contracts/IBackendApiService.cs | 8 ++ .../Contracts/ITeslaFleetApiService.cs | 3 + .../Server/Services/TeslaFleetApiService.cs | 62 +++++++++++---- TeslaSolarCharger/Server/appsettings.json | 1 + .../Shared/Contracts/IConfigurationWrapper.cs | 2 + .../Shared/Enums/FleetApiTokenState.cs | 10 +++ .../Shared/Wrappers/ConfigurationWrapper.cs | 14 ++++ 17 files changed, 274 insertions(+), 27 deletions(-) create mode 100644 TeslaSolarCharger.Tests/Services/Server/BackendApiService.cs create mode 100644 TeslaSolarCharger/Server/Controllers/FleetApiController.cs create mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaOAuthRequestInformation.cs create mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs create mode 100644 TeslaSolarCharger/Server/Services/BackendApiService.cs create mode 100644 TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs create mode 100644 TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs diff --git a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs index 6d664966b..c3db4ee27 100644 --- a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs +++ b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs @@ -12,4 +12,5 @@ public interface IConstants int MinimumSocDifference { get; } string InstallationIdKey { get; } + string FleetApiTokenRequested { get; } } diff --git a/TeslaSolarCharger.SharedBackend/Values/Constants.cs b/TeslaSolarCharger.SharedBackend/Values/Constants.cs index 205f1ebb8..806806b0d 100644 --- a/TeslaSolarCharger.SharedBackend/Values/Constants.cs +++ b/TeslaSolarCharger.SharedBackend/Values/Constants.cs @@ -11,4 +11,5 @@ public class Constants : IConstants public int MinimumSocDifference => 2; public string InstallationIdKey => "InstallationId"; + public string FleetApiTokenRequested => "FleetApiTokenRequested"; } diff --git a/TeslaSolarCharger.Tests/Services/Server/BackendApiService.cs b/TeslaSolarCharger.Tests/Services/Server/BackendApiService.cs new file mode 100644 index 000000000..32c12ea0f --- /dev/null +++ b/TeslaSolarCharger.Tests/Services/Server/BackendApiService.cs @@ -0,0 +1,31 @@ +using TeslaSolarCharger.Server.Dtos.TscBackend; +using Xunit; +using Xunit.Abstractions; + +namespace TeslaSolarCharger.Tests.Services.Server; + +public class BackendApiService : TestBase +{ + public BackendApiService(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + [Fact] + public void CanEnCodeCorrectUrl() + { + var backendApiService = Mock.Create(); + var requestInformation = new DtoTeslaOAuthRequestInformation() + { + ClientId = "f29f71d6285a-4873-8b6b-80f15854892e", + Prompt = "login", + RedirectUri = "https://www.teslasolarcharger.de/", + ResponseType = "code", + Scope = "offline_access vehicle_device_data vehicle_cmds vehicle_charging_cmds", + State = "8774fbe7-f9aa-4e36-8e88-5c8b27137f20", + }; + var url = backendApiService.GenerateAuthUrl(requestInformation, "en-US"); + var expectedUrl = "https://auth.tesla.com/oauth2/v3/authorize?&client_id=f29f71d6285a-4873-8b6b-80f15854892e&locale=en-US&prompt=login&redirect_uri=https%3A%2F%2Fwww.teslasolarcharger.de%2F&response_type=code&scope=offline_access%20vehicle_device_data%20vehicle_cmds%20vehicle_charging_cmds&state=8774fbe7-f9aa-4e36-8e88-5c8b27137f20"; + Assert.Equal(expectedUrl, url); + } +} diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 6f9327704..465e49b1f 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -1,9 +1,12 @@ @page "/BaseConfiguration" +@using System.Globalization @using TeslaSolarCharger.Shared.Dtos.BaseConfiguration @using TeslaSolarCharger.Shared @using TeslaSolarCharger.Shared.Enums @using Majorsoft.Blazor.Components.Collapse +@using TeslaSolarCharger.Shared.Dtos @inject HttpClient HttpClient +@inject NavigationManager NavigationManager @inject IToastService ToastService @@ -30,6 +33,37 @@ else
+ @if (_fleetApiTokenState != FleetApiTokenState.NotNeeded) + { +

Tesla Fleet API:

+ @switch (_fleetApiTokenState) + { + case FleetApiTokenState.NotRequested: +
+ You have to generate a token in order to use the Tesla Fleet API. +
+ break; + case FleetApiTokenState.NotReceived: +
+ You already have requested a token but did not receive it yet. It can take up to five minutes to receive the token. If the token did not arrive within five minutes please try again: +
+ break; + case FleetApiTokenState.Expired: +
+ Your token has expired. This could happen if you changed your Tesla password or did not use TeslaSolarCharger for too long. Please generate a new token: +
+ break; + case FleetApiTokenState.UpToDate: +
+ Everything is fine! If you want to generate a new token e.g. to switch to another Tesla Account please click the button below: +
+ break; + } +
+ +
+
+ }

TeslaMate:


- - - - - -
- - + @if (_fleetApiTokenState == FleetApiTokenState.NotNeeded) + { + + + + + +
+ } + ("/api/BaseConfiguration/GetBaseConfiguration").ConfigureAwait(false); + var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); + if (value != null) + { + _fleetApiTokenState = value.Value; + } } private async Task HandleValidSubmit() @@ -546,6 +589,16 @@ else } } + private async Task GenerateFleetApiToken() + { + var locale = CultureInfo.CurrentCulture.ToString(); + var url = await HttpClient.GetFromJsonAsync>($"api/FleetApi/GetOauthUrl?locale={locale}").ConfigureAwait(false); + if (url?.Value != null) + { + NavigationManager.NavigateTo(url.Value); + } + } + private string _collapsedColor = "LightGray"; private string _expandedColor = "LightGray"; private string _hoverColor = "LightGray"; diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index ef1562149..ce7d9e843 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -359,6 +359,9 @@ else

Version: @_version

}
@_installationId
+
+ CurrentCulture: @CultureInfo.CurrentCulture +
diff --git a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs new file mode 100644 index 000000000..650fd7175 --- /dev/null +++ b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Enums; +using TeslaSolarCharger.SharedBackend.Abstracts; + +namespace TeslaSolarCharger.Server.Controllers; + +public class FleetApiController : ApiBaseController +{ + private readonly ITeslaFleetApiService _fleetApiService; + private readonly IBackendApiService _backendApiService; + + public FleetApiController(ITeslaFleetApiService fleetApiService, IBackendApiService backendApiService) + { + _fleetApiService = fleetApiService; + _backendApiService = backendApiService; + } + + [HttpGet] + public Task> FleetApiTokenState() => _fleetApiService.GetFleetApiTokenState(); + + [HttpGet] + public Task> GetOauthUrl(string locale) => _backendApiService.StartTeslaOAuth(locale); +} diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaOAuthRequestInformation.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaOAuthRequestInformation.cs new file mode 100644 index 000000000..99ee3eeac --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaOAuthRequestInformation.cs @@ -0,0 +1,11 @@ +namespace TeslaSolarCharger.Server.Dtos.TscBackend; + +public class DtoTeslaOAuthRequestInformation +{ + public string ClientId { get; set; } + public string Prompt { get; set; } + public string RedirectUri { get; set; } + public string ResponseType { get; set; } + public string Scope { get; set; } + public string State { get; set; } +} diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs new file mode 100644 index 000000000..5adca059f --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs @@ -0,0 +1,11 @@ +using TeslaSolarCharger.Model.Enums; + +namespace TeslaSolarCharger.Server.Dtos.TscBackend; + +public class TeslaTscDeliveryToken +{ + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public string IdToken { get; set; } + public TeslaFleetApiRegion Region { get; set; } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 0676bcbc5..39cbc360d 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -96,6 +96,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs new file mode 100644 index 000000000..7fc45562e --- /dev/null +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using TeslaSolarCharger.Server.Dtos.TscBackend; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Server.Services; + +public class BackendApiService : IBackendApiService +{ + private readonly ILogger _logger; + private readonly ITscConfigurationService _tscConfigurationService; + private readonly IConfigurationWrapper _configurationWrapper; + + public BackendApiService(ILogger logger, ITscConfigurationService tscConfigurationService, IConfigurationWrapper configurationWrapper) + { + _logger = logger; + _tscConfigurationService = tscConfigurationService; + _configurationWrapper = configurationWrapper; + } + + public async Task> StartTeslaOAuth(string locale) + { + _logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var backendApiBaseUrl = _configurationWrapper.BackendApiBaseUrl(); + using var httpClient = new HttpClient(); + var requestUri = $"{backendApiBaseUrl}Tsc/StartTeslaOAuth?installationId={installationId}"; + var responseString = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false); + var oAuthRequestInformation = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get oAuth data"); + var requestUrl = GenerateAuthUrl(oAuthRequestInformation, locale); + return new DtoValue(requestUrl); + } + + internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation, string locale) + { + _logger.LogTrace("{method}({@oAuthInformation})", nameof(GenerateAuthUrl), oAuthInformation); + var url = + $"https://auth.tesla.com/oauth2/v3/authorize?&client_id={Uri.EscapeDataString(oAuthInformation.ClientId)}&locale={Uri.EscapeDataString(locale)}&prompt={Uri.EscapeDataString(oAuthInformation.Prompt)}&redirect_uri={Uri.EscapeDataString(oAuthInformation.RedirectUri)}&response_type={Uri.EscapeDataString(oAuthInformation.ResponseType)}&scope={Uri.EscapeDataString(oAuthInformation.Scope)}&state={Uri.EscapeDataString(oAuthInformation.State)}"; + return url; + } +} diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs new file mode 100644 index 000000000..55901b449 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -0,0 +1,8 @@ +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface IBackendApiService +{ + Task> StartTeslaOAuth(string locale); +} diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index 025b1ce78..7c9d1c094 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -1,10 +1,13 @@ using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Enums; namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiService { public Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region); + Task> GetFleetApiTokenState(); } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 79d8d47fd..cdd8e4514 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using System.Globalization; using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; @@ -10,7 +11,9 @@ using TeslaSolarCharger.Server.Services.ApiServices.Contracts; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Enums; +using TeslaSolarCharger.SharedBackend.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -22,6 +25,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly ITeslamateContext _teslamateContext; private readonly IConfigurationWrapper _configurationWrapper; private readonly ITeslamateApiService _teslamateApiService; + private readonly IConstants _constants; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; @@ -30,7 +34,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper, - ITeslamateApiService teslamateApiService) + ITeslamateApiService teslamateApiService, IConstants constants) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; @@ -38,6 +42,7 @@ public TeslaFleetApiService(ILogger logger, ITeslaSolarCha _teslamateContext = teslamateContext; _configurationWrapper = configurationWrapper; _teslamateApiService = teslamateApiService; + _constants = constants; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) @@ -121,6 +126,46 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } + public async Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) + { + var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TeslaTokens.Add(new TeslaToken + { + AccessToken = token.AccessToken, + RefreshToken = token.RefreshToken, + IdToken = token.IdToken, + ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), + Region = region, + }); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task> GetFleetApiTokenState() + { + if (!_configurationWrapper.UseFleetApi()) + { + return new DtoValue(FleetApiTokenState.NotNeeded); + } + var token = await _teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); + if (token != null) + { + return new DtoValue(token.ExpiresAtUtc < _dateTimeProvider.UtcNow() ? FleetApiTokenState.Expired : FleetApiTokenState.UpToDate); + } + var tokenRequestedDateString = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.FleetApiTokenRequested) + .Select(c => c.Value) + .FirstOrDefaultAsync().ConfigureAwait(false); + if (tokenRequestedDateString == null) + { + return new DtoValue(FleetApiTokenState.NotRequested); + } + var tokenRequestedDate = DateTime.Parse(tokenRequestedDateString, null, DateTimeStyles.RoundtripKind); + //ToDo: return not requested if request is older than x -> Currently not nown as not known how old a code can be to create a token out of it. + return new DtoValue(FleetApiTokenState.NotReceived); + } + private async Task GetAccessTokenAsync() { _logger.LogTrace("{method}()", nameof(GetAccessTokenAsync)); @@ -154,19 +199,4 @@ private async Task GetAccessTokenAsync() return token; } - public async Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) - { - var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); - _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - _teslaSolarChargerContext.TeslaTokens.Add(new TeslaToken - { - AccessToken = token.AccessToken, - RefreshToken = token.RefreshToken, - IdToken = token.IdToken, - ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), - Region = region, - }); - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - } } diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 82d7d9b31..386519662 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -61,6 +61,7 @@ "IgnoreSslErrors": false, "UseFleetApi": false, "FleetApiClientId": "f29f71d6285a-4873-8b6b-80f15854892e", + "BackendApiBaseUrl": "https://www.teslasolarcharger.de/api/", "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": { diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 713b26a80..297a353ad 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -87,4 +87,6 @@ public interface IConfigurationWrapper string GetSqliteFileNameWithoutPath(); string BackupZipDirectory(); string FleetApiClientId(); + bool UseFleetApi(); + string BackendApiBaseUrl(); } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs new file mode 100644 index 000000000..e8e1e9773 --- /dev/null +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -0,0 +1,10 @@ +namespace TeslaSolarCharger.Shared.Enums; + +public enum FleetApiTokenState +{ + NotNeeded, + NotRequested, + NotReceived, + Expired, + UpToDate, +} diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index 3820c9367..783411645 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -99,6 +99,20 @@ public bool AllowCors() return value; } + public bool UseFleetApi() + { + var environmentVariableName = "UseFleetApi"; + var value = _configuration.GetValue(environmentVariableName); + return value; + } + + public string BackendApiBaseUrl() + { + var environmentVariableName = "BackendApiBaseUrl"; + var value = _configuration.GetValue(environmentVariableName); + return value; + } + public string FleetApiClientId() { var environmentVariableName = "FleetApiClientId"; From afd2d0c833a5176fdb7a0a030570b4ea91aa1021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 3 Dec 2023 17:32:29 +0100 Subject: [PATCH 12/27] feat(TeslaFleetApiService): autorefresh token --- .../Client/Pages/BaseConfiguration.razor | 5 +- .../Server/Controllers/ConfigController.cs | 8 ++- ...ryToken.cs => DtoTeslaTscDeliveryToken.cs} | 2 +- .../Server/Scheduling/JobManager.cs | 7 ++- .../Jobs/FleetApiTokenRefreshJob.cs | 23 ++++++++ .../Server/ServiceCollectionExtensions.cs | 1 + .../Server/Services/BackendApiService.cs | 33 +++++++++++- .../Contracts/ITeslaFleetApiService.cs | 4 +- .../Server/Services/TeslaFleetApiService.cs | 52 ++++++++++++++++--- 9 files changed, 116 insertions(+), 19 deletions(-) rename TeslaSolarCharger/Server/Dtos/TscBackend/{TeslaTscDeliveryToken.cs => DtoTeslaTscDeliveryToken.cs} (88%) create mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 465e49b1f..d12583f0e 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -60,7 +60,7 @@ else break; }
- +

} @@ -566,6 +566,8 @@ else private FleetApiTokenState? _fleetApiTokenState; + private bool _tokenGenerationButtonDisabled; + protected override async Task OnInitializedAsync() { _dtoBaseConfiguration = await HttpClient.GetFromJsonAsync("/api/BaseConfiguration/GetBaseConfiguration").ConfigureAwait(false); @@ -591,6 +593,7 @@ else private async Task GenerateFleetApiToken() { + _tokenGenerationButtonDisabled = true; var locale = CultureInfo.CurrentCulture.ToString(); var url = await HttpClient.GetFromJsonAsync>($"api/FleetApi/GetOauthUrl?locale={locale}").ConfigureAwait(false); if (url?.Value != null) diff --git a/TeslaSolarCharger/Server/Controllers/ConfigController.cs b/TeslaSolarCharger/Server/Controllers/ConfigController.cs index 793daadf3..f2a015ff6 100644 --- a/TeslaSolarCharger/Server/Controllers/ConfigController.cs +++ b/TeslaSolarCharger/Server/Controllers/ConfigController.cs @@ -1,8 +1,6 @@ using Microsoft.AspNetCore.Mvc; -using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Contracts; -using TeslaSolarCharger.Server.Dtos; -using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Dtos.Contracts; @@ -53,7 +51,7 @@ public Task UpdateCarBasicConfiguration(int carId, [FromBody] CarBasicConfigurat _service.UpdateCarBasicConfiguration(carId, carBasicConfiguration); [HttpPost] - public Task AddTeslaFleetApiToken([FromBody]DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) => - _teslaFleetApiService.AddNewTokenAsync(token, region); + public Task AddTeslaFleetApiToken([FromBody] DtoTeslaTscDeliveryToken token) => + _teslaFleetApiService.AddNewTokenAsync(token); } } diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs similarity index 88% rename from TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs rename to TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs index 5adca059f..89034191d 100644 --- a/TeslaSolarCharger/Server/Dtos/TscBackend/TeslaTscDeliveryToken.cs +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs @@ -2,7 +2,7 @@ namespace TeslaSolarCharger.Server.Dtos.TscBackend; -public class TeslaTscDeliveryToken +public class DtoTeslaTscDeliveryToken { public string AccessToken { get; set; } public string RefreshToken { get; set; } diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index 960cc803b..f37f27e41 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -42,6 +42,7 @@ public async Task StartJobs() var mqttReconnectionJob = JobBuilder.Create().Build(); var newVersionCheckJob = JobBuilder.Create().Build(); var spotPriceJob = JobBuilder.Create().Build(); + var fleetApiTokenRefreshJob = JobBuilder.Create().Build(); var currentDate = _dateTimeProvider.DateTimeOffSetNow(); var chargingTriggerStartTime = currentDate.AddSeconds(5); @@ -63,8 +64,6 @@ public async Task StartJobs() .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)pvValueJobIntervall.TotalSeconds)) .Build(); - - var carStateCachingTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(3)).Build(); @@ -83,6 +82,9 @@ public async Task StartJobs() var spotPricePlanningTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatHourlyForever(1)).Build(); + var fleetApiTokenRefreshTrigger = TriggerBuilder.Create() + .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(59)).Build(); + var triggersAndJobs = new Dictionary> { {chargingValueJob, new HashSet { chargingValueTrigger }}, @@ -93,6 +95,7 @@ public async Task StartJobs() {mqttReconnectionJob, new HashSet {mqttReconnectionTrigger}}, {newVersionCheckJob, new HashSet {newVersionCheckTrigger}}, {spotPriceJob, new HashSet {spotPricePlanningTrigger}}, + {fleetApiTokenRefreshJob, new HashSet {fleetApiTokenRefreshTrigger}}, }; await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs new file mode 100644 index 000000000..cf5a21377 --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs @@ -0,0 +1,23 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +[DisallowConcurrentExecution] +public class FleetApiTokenRefreshJob : IJob +{ + private readonly ILogger _logger; + private readonly ITeslaFleetApiService _service; + + public FleetApiTokenRefreshJob(ILogger logger, ITeslaFleetApiService service) + { + _logger = logger; + _service = service; + } + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogTrace("{method}({context})", nameof(Execute), context); + await _service.RefreshTokenAsync().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 39cbc360d..50fee7932 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 7fc45562e..a19f34dd7 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,8 +1,12 @@ -using Newtonsoft.Json; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.SharedBackend.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -11,12 +15,20 @@ public class BackendApiService : IBackendApiService private readonly ILogger _logger; private readonly ITscConfigurationService _tscConfigurationService; private readonly IConfigurationWrapper _configurationWrapper; + private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; + private readonly IConstants _constants; + private readonly IDateTimeProvider _dateTimeProvider; - public BackendApiService(ILogger logger, ITscConfigurationService tscConfigurationService, IConfigurationWrapper configurationWrapper) + public BackendApiService(ILogger logger, ITscConfigurationService tscConfigurationService, + IConfigurationWrapper configurationWrapper, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, + IDateTimeProvider dateTimeProvider) { _logger = logger; _tscConfigurationService = tscConfigurationService; _configurationWrapper = configurationWrapper; + _teslaSolarChargerContext = teslaSolarChargerContext; + _constants = constants; + _dateTimeProvider = dateTimeProvider; } public async Task> StartTeslaOAuth(string locale) @@ -29,6 +41,23 @@ public async Task> StartTeslaOAuth(string locale) var responseString = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false); var oAuthRequestInformation = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get oAuth data"); var requestUrl = GenerateAuthUrl(oAuthRequestInformation, locale); + var tokenRequested = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.FleetApiTokenRequested) + .FirstOrDefaultAsync().ConfigureAwait(false); + if (tokenRequested == null) + { + var config = new TscConfiguration + { + Key = _constants.FleetApiTokenRequested, + Value = _dateTimeProvider.UtcNow().ToString("O"), + }; + _teslaSolarChargerContext.TscConfigurations.Add(config); + } + else + { + tokenRequested.Value = _dateTimeProvider.UtcNow().ToString("O"); + } + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); return new DtoValue(requestUrl); } diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index 7c9d1c094..1d02b4204 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -1,6 +1,7 @@ using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Enums; @@ -8,6 +9,7 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiService { - public Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region); + Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token); Task> GetFleetApiTokenState(); + Task RefreshTokenAsync(); } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index cdd8e4514..64c3142ec 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -8,6 +8,7 @@ using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Services.ApiServices.Contracts; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; @@ -26,6 +27,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly IConfigurationWrapper _configurationWrapper; private readonly ITeslamateApiService _teslamateApiService; private readonly IConstants _constants; + private readonly ITscConfigurationService _tscConfigurationService; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; @@ -34,7 +36,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper, - ITeslamateApiService teslamateApiService, IConstants constants) + ITeslamateApiService teslamateApiService, IConstants constants, ITscConfigurationService tscConfigurationService) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; @@ -43,6 +45,7 @@ public TeslaFleetApiService(ILogger logger, ITeslaSolarCha _configurationWrapper = configurationWrapper; _teslamateApiService = teslamateApiService; _constants = constants; + _tscConfigurationService = tscConfigurationService; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) @@ -109,7 +112,7 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) private async Task SendCommandToTeslaApi(long id, string commandName, string contentData = "{}") { _logger.LogTrace("{method}({id}, {commandName}, {contentData})", nameof(SendCommandToTeslaApi), id, commandName, contentData); - var accessToken = await GetAccessTokenAsync().ConfigureAwait(false); + var accessToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false); using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json"); @@ -126,7 +129,42 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } - public async Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFleetApiRegion region) + public async Task RefreshTokenAsync() + { + _logger.LogTrace("{method}()", nameof(RefreshTokenAsync)); + var tokenState = (await GetFleetApiTokenState().ConfigureAwait(false)).Value; + switch (tokenState) + { + case FleetApiTokenState.NotNeeded: + _logger.LogDebug("Refreshing token not needed."); + return; + case FleetApiTokenState.NotRequested: + _logger.LogDebug("No token has been requested, yet."); + return; + case FleetApiTokenState.NotReceived: + break; + case FleetApiTokenState.Expired: + break; + case FleetApiTokenState.UpToDate: + break; + default: + throw new ArgumentOutOfRangeException(); + } + var token = await _teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); + if (token == null) + { + using var httpClient = new HttpClient(); + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var url = _configurationWrapper.BackendApiBaseUrl() + $"Tsc/DeliverAuthToken?installationId={installationId}"; + var response = await httpClient.GetAsync(url).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); + await AddNewTokenAsync(newToken).ConfigureAwait(false); + } + var dbToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false); + } + + public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token) { var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); @@ -136,8 +174,8 @@ public async Task AddNewTokenAsync(DtoTeslaFleetApiRefreshToken token, TeslaFlee AccessToken = token.AccessToken, RefreshToken = token.RefreshToken, IdToken = token.IdToken, - ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), - Region = region, + ExpiresAtUtc = _dateTimeProvider.UtcNow().AddMinutes(2), + Region = token.Region, }); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } @@ -166,9 +204,9 @@ public async Task> GetFleetApiTokenState() return new DtoValue(FleetApiTokenState.NotReceived); } - private async Task GetAccessTokenAsync() + private async Task GetAccessTokenAndRefreshWhenNeededAsync() { - _logger.LogTrace("{method}()", nameof(GetAccessTokenAsync)); + _logger.LogTrace("{method}()", nameof(GetAccessTokenAndRefreshWhenNeededAsync)); var token = await _teslaSolarChargerContext.TeslaTokens .OrderByDescending(t => t.ExpiresAtUtc) .FirstAsync().ConfigureAwait(false); From a46bfa1581d39271b91c98c89f364568fb16c1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 3 Dec 2023 17:52:11 +0100 Subject: [PATCH 13/27] feat(BackendApiServicE): remove token on creating new one --- TeslaSolarCharger/Server/Services/BackendApiService.cs | 3 +++ TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index a19f34dd7..61551e6e7 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -34,6 +34,9 @@ public BackendApiService(ILogger logger, ITscConfigurationSer public async Task> StartTeslaOAuth(string locale) { _logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); + var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); var backendApiBaseUrl = _configurationWrapper.BackendApiBaseUrl(); using var httpClient = new HttpClient(); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 64c3142ec..0fceeb85c 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -146,6 +146,7 @@ public async Task RefreshTokenAsync() case FleetApiTokenState.Expired: break; case FleetApiTokenState.UpToDate: + _logger.LogDebug("Token is up to date."); break; default: throw new ArgumentOutOfRangeException(); @@ -226,6 +227,7 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); token.AccessToken = newToken.AccessToken; token.RefreshToken = newToken.RefreshToken; From 628c514ae4c446cae15563e79c0bef0de452c485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 3 Dec 2023 19:45:40 +0100 Subject: [PATCH 14/27] feat(TscConfigurationService): add option to post installation information --- .../Server/Contracts/ICoreService.cs | 1 + .../Server/Controllers/HelloController.cs | 5 +++- .../TscBackend/DtoInstallationInformation.cs | 8 ++++++ TeslaSolarCharger/Server/Program.cs | 1 + .../Server/Services/CoreService.cs | 26 ++++++++++++++++++- .../Server/Services/NewVersionCheckService.cs | 5 ++++ .../Services/TscConfigurationService.cs | 3 +++ 7 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs diff --git a/TeslaSolarCharger/Server/Contracts/ICoreService.cs b/TeslaSolarCharger/Server/Contracts/ICoreService.cs index 8b5572642..84903e1ad 100644 --- a/TeslaSolarCharger/Server/Contracts/ICoreService.cs +++ b/TeslaSolarCharger/Server/Contracts/ICoreService.cs @@ -19,5 +19,6 @@ public interface ICoreService DtoValue TeslaApiRequestsSinceStartup(); DtoValue ShouldDisplayApiRequestCounter(); Task> GetPriceData(DateTimeOffset from, DateTimeOffset to); + Task PostInstallationInformation(string reason); Task GetInstallationId(); } diff --git a/TeslaSolarCharger/Server/Controllers/HelloController.cs b/TeslaSolarCharger/Server/Controllers/HelloController.cs index 2e9a6a23d..b28605ce9 100644 --- a/TeslaSolarCharger/Server/Controllers/HelloController.cs +++ b/TeslaSolarCharger/Server/Controllers/HelloController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using TeslaSolarCharger.GridPriceProvider.Data; using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.SharedBackend.Abstracts; @@ -9,10 +10,12 @@ namespace TeslaSolarCharger.Server.Controllers public class HelloController : ApiBaseController { private readonly ICoreService _coreService; + private readonly ITscConfigurationService _tscConfigurationService; - public HelloController(ICoreService coreService) + public HelloController(ICoreService coreService, ITscConfigurationService tscConfigurationService) { _coreService = coreService; + _tscConfigurationService = tscConfigurationService; } [HttpGet] diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs new file mode 100644 index 000000000..df05e1ee1 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Server.Dtos.TscBackend; + +public class DtoInstallationInformation +{ + public string InstallationId { get; set; } + public string Version { get; set; } + public string InfoReason { get; set; } +} diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 118c2f2e7..41bd9c7bd 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -59,6 +59,7 @@ var coreService = app.Services.GetRequiredService(); coreService.LogVersion(); + await coreService.PostInstallationInformation("Startup").ConfigureAwait(false); await coreService.BackupDatabaseIfNeeded().ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/CoreService.cs b/TeslaSolarCharger/Server/Services/CoreService.cs index 5851910c1..7c019657c 100644 --- a/TeslaSolarCharger/Server/Services/CoreService.cs +++ b/TeslaSolarCharger/Server/Services/CoreService.cs @@ -4,6 +4,7 @@ using TeslaSolarCharger.GridPriceProvider.Services.Interfaces; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Scheduling; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; @@ -174,9 +175,32 @@ public Task> GetPriceData(DateTimeOffset from, DateTimeOffset return _fixedPriceService.GetPriceData(from, to, null); } + public async Task PostInstallationInformation(string reason) + { + try + { + var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var currentVersion = await GetCurrentVersion().ConfigureAwait(false); + var installationInformation = new DtoInstallationInformation + { + InstallationId = installationId.ToString(), + Version = currentVersion ?? "unknown", + InfoReason = reason, + }; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, installationInformation).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Could not post installation information"); + } + + } + public async Task GetInstallationId() { - _logger.LogTrace("{method}()", nameof(GetInstallationId)); var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); return installationId.ToString(); } diff --git a/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs b/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs index 7dca13011..55b43ed23 100644 --- a/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs +++ b/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs @@ -1,4 +1,7 @@ using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Dtos.TscBackend; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -20,11 +23,13 @@ public async Task CheckForNewVersion() { _logger.LogTrace("{method}()", nameof(CheckForNewVersion)); var currentVersion = await _coreService.GetCurrentVersion().ConfigureAwait(false); + await _coreService.PostInstallationInformation("CheckForNewVersion").ConfigureAwait(false); if (string.IsNullOrEmpty(currentVersion)) { _settings.IsNewVersionAvailable = false; return; } + try { diff --git a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs index d8ec2627e..ea311e138 100644 --- a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs @@ -1,6 +1,9 @@ using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Contracts; +using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.SharedBackend.Contracts; namespace TeslaSolarCharger.Server.Services; From 794901398a02d95234d7a896f6f0cb1a25930507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 3 Dec 2023 20:49:48 +0100 Subject: [PATCH 15/27] feat(TeslaFleetApiService): log errors to backend --- .../Server/Contracts/ICoreService.cs | 1 - .../TeslaFleetApi/DtoVehicleCommandResult.cs | 4 +- .../Dtos/TscBackend/DtoErrorInformation.cs | 11 ++++ TeslaSolarCharger/Server/Program.cs | 4 +- .../Server/Services/BackendApiService.cs | 61 +++++++++++++++++++ .../Services/Contracts/IBackendApiService.cs | 2 + .../Server/Services/CoreService.cs | 24 +------- .../Server/Services/NewVersionCheckService.cs | 7 ++- .../Server/Services/TeslaFleetApiService.cs | 27 +++++++- 9 files changed, 111 insertions(+), 30 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/DtoErrorInformation.cs diff --git a/TeslaSolarCharger/Server/Contracts/ICoreService.cs b/TeslaSolarCharger/Server/Contracts/ICoreService.cs index 84903e1ad..8b5572642 100644 --- a/TeslaSolarCharger/Server/Contracts/ICoreService.cs +++ b/TeslaSolarCharger/Server/Contracts/ICoreService.cs @@ -19,6 +19,5 @@ public interface ICoreService DtoValue TeslaApiRequestsSinceStartup(); DtoValue ShouldDisplayApiRequestCounter(); Task> GetPriceData(DateTimeOffset from, DateTimeOffset to); - Task PostInstallationInformation(string reason); Task GetInstallationId(); } diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs index 2ed828c5c..dddbf2b38 100644 --- a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs @@ -2,6 +2,6 @@ public class DtoVehicleCommandResult { - bool Result { get; set; } - string Reason { get; set; } + public bool Result { get; set; } + public string Reason { get; set; } } diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoErrorInformation.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoErrorInformation.cs new file mode 100644 index 000000000..efd1595e9 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoErrorInformation.cs @@ -0,0 +1,11 @@ +namespace TeslaSolarCharger.Server.Dtos.TscBackend; + +public class DtoErrorInformation +{ + public string InstallationId { get; set; } + public string Source { get; set; } + public string MethodName { get; set; } + public string Message { get; set; } + public string Version { get; set; } + public string? StackTrace { get; set; } +} diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 41bd9c7bd..a933d2973 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -59,7 +59,9 @@ var coreService = app.Services.GetRequiredService(); coreService.LogVersion(); - await coreService.PostInstallationInformation("Startup").ConfigureAwait(false); + + var backendApiService = app.Services.GetRequiredService(); + await backendApiService.PostInstallationInformation("Startup").ConfigureAwait(false); await coreService.BackupDatabaseIfNeeded().ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 61551e6e7..81b01e331 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using System.Diagnostics; +using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Dtos.TscBackend; @@ -71,4 +73,63 @@ internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation $"https://auth.tesla.com/oauth2/v3/authorize?&client_id={Uri.EscapeDataString(oAuthInformation.ClientId)}&locale={Uri.EscapeDataString(locale)}&prompt={Uri.EscapeDataString(oAuthInformation.Prompt)}&redirect_uri={Uri.EscapeDataString(oAuthInformation.RedirectUri)}&response_type={Uri.EscapeDataString(oAuthInformation.ResponseType)}&scope={Uri.EscapeDataString(oAuthInformation.Scope)}&state={Uri.EscapeDataString(oAuthInformation.State)}"; return url; } + + public async Task PostInstallationInformation(string reason) + { + try + { + var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var currentVersion = await GetCurrentVersion().ConfigureAwait(false); + var installationInformation = new DtoInstallationInformation + { + InstallationId = installationId.ToString(), + Version = currentVersion ?? "unknown", + InfoReason = reason, + }; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, installationInformation).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Could not post installation information"); + } + + } + + public async Task PostErrorInformation(string source, string methodName, string message, string? stackTrace = null) + { + try + { + var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyError"; + var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var currentVersion = await GetCurrentVersion().ConfigureAwait(false); + var errorInformation = new DtoErrorInformation() + { + InstallationId = installationId.ToString(), + Source = source, + MethodName = methodName, + Message = message, + Version = currentVersion ?? "unknown", + StackTrace = stackTrace, + }; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, errorInformation).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Could not post error information"); + } + + } + + public Task GetCurrentVersion() + { + _logger.LogTrace("{method}()", nameof(GetCurrentVersion)); + var assembly = Assembly.GetExecutingAssembly(); + var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + return Task.FromResult(fileVersionInfo.ProductVersion); + } } diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 55901b449..c5449df0f 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -5,4 +5,6 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface IBackendApiService { Task> StartTeslaOAuth(string locale); + Task PostInstallationInformation(string reason); + Task PostErrorInformation(string source, string methodName, string message, string? stackTrace = null); } diff --git a/TeslaSolarCharger/Server/Services/CoreService.cs b/TeslaSolarCharger/Server/Services/CoreService.cs index 7c019657c..95c4c50bb 100644 --- a/TeslaSolarCharger/Server/Services/CoreService.cs +++ b/TeslaSolarCharger/Server/Services/CoreService.cs @@ -175,29 +175,7 @@ public Task> GetPriceData(DateTimeOffset from, DateTimeOffset return _fixedPriceService.GetPriceData(from, to, null); } - public async Task PostInstallationInformation(string reason) - { - try - { - var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; - var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var currentVersion = await GetCurrentVersion().ConfigureAwait(false); - var installationInformation = new DtoInstallationInformation - { - InstallationId = installationId.ToString(), - Version = currentVersion ?? "unknown", - InfoReason = reason, - }; - using var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - var response = await httpClient.PostAsJsonAsync(url, installationInformation).ConfigureAwait(false); - } - catch (Exception e) - { - _logger.LogError(e, "Could not post installation information"); - } - - } + public async Task GetInstallationId() { diff --git a/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs b/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs index 55b43ed23..4116c5729 100644 --- a/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs +++ b/TeslaSolarCharger/Server/Services/NewVersionCheckService.cs @@ -11,19 +11,22 @@ public class NewVersionCheckService : INewVersionCheckService private readonly ILogger _logger; private readonly ICoreService _coreService; private readonly ISettings _settings; + private readonly IBackendApiService _backendApiService; - public NewVersionCheckService(ILogger logger, ICoreService coreService, ISettings settings) + public NewVersionCheckService(ILogger logger, ICoreService coreService, ISettings settings, + IBackendApiService backendApiService) { _logger = logger; _coreService = coreService; _settings = settings; + _backendApiService = backendApiService; } public async Task CheckForNewVersion() { _logger.LogTrace("{method}()", nameof(CheckForNewVersion)); var currentVersion = await _coreService.GetCurrentVersion().ConfigureAwait(false); - await _coreService.PostInstallationInformation("CheckForNewVersion").ConfigureAwait(false); + await _backendApiService.PostInstallationInformation("CheckForNewVersion").ConfigureAwait(false); if (string.IsNullOrEmpty(currentVersion)) { _settings.IsNewVersionAvailable = false; diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 0fceeb85c..31811dd34 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -28,6 +28,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly ITeslamateApiService _teslamateApiService; private readonly IConstants _constants; private readonly ITscConfigurationService _tscConfigurationService; + private readonly IBackendApiService _backendApiService; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; @@ -36,7 +37,8 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper, - ITeslamateApiService teslamateApiService, IConstants constants, ITscConfigurationService tscConfigurationService) + ITeslamateApiService teslamateApiService, IConstants constants, ITscConfigurationService tscConfigurationService, + IBackendApiService backendApiService) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; @@ -46,6 +48,7 @@ public TeslaFleetApiService(ILogger logger, ITeslaSolarCha _teslamateApiService = teslamateApiService; _constants = constants; _tscConfigurationService = tscConfigurationService; + _backendApiService = backendApiService; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) @@ -125,7 +128,18 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) var requestUri = $"https://fleet-api.prd.{regionCode}.vn.cloud.tesla.com/api/1/vehicles/{id}/{commandName}"; var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), + $"Sending command to Tesla API resulted in non succes status code: {commandName}, {contentData}. Response string: {responseString}").ConfigureAwait(false); + } _logger.LogDebug("Response: {responseString}", responseString); + var result = JsonConvert.DeserializeObject(responseString); + if (result?.Result == false) + { + await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), + $"Result of command request is false: {commandName}, {contentData}. Response string: {responseString}").ConfigureAwait(false); + } return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); } @@ -159,6 +173,12 @@ public async Task RefreshTokenAsync() var url = _configurationWrapper.BackendApiBaseUrl() + $"Tsc/DeliverAuthToken?installationId={installationId}"; var response = await httpClient.GetAsync(url).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), + $"Getting token from TscBackend. Response string: {responseString}").ConfigureAwait(false); + } + response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); await AddNewTokenAsync(newToken).ConfigureAwait(false); } @@ -227,6 +247,11 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), + $"Refreshing token did result in non success status code. Response string: {responseString}").ConfigureAwait(false); + } response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); token.AccessToken = newToken.AccessToken; From 88bf97bc6b51e6c72c8e81389c49d76067e4aaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 10:23:34 +0100 Subject: [PATCH 16/27] feat(TeslaFleetApiService): add status codes to error logs --- TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 31811dd34..5d1de30f2 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -131,7 +131,7 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) if (!response.IsSuccessStatusCode) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), - $"Sending command to Tesla API resulted in non succes status code: {commandName}, {contentData}. Response string: {responseString}").ConfigureAwait(false); + $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}").ConfigureAwait(false); } _logger.LogDebug("Response: {responseString}", responseString); var result = JsonConvert.DeserializeObject(responseString); @@ -176,7 +176,7 @@ public async Task RefreshTokenAsync() if (!response.IsSuccessStatusCode) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), - $"Getting token from TscBackend. Response string: {responseString}").ConfigureAwait(false); + $"Getting token from TscBackend. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false); } response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); @@ -250,7 +250,7 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() if (!response.IsSuccessStatusCode) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), - $"Refreshing token did result in non success status code. Response string: {responseString}").ConfigureAwait(false); + $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false); } response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); From f63b982d7e84d9739e74412dad13c275eb98f4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 12:31:50 +0100 Subject: [PATCH 17/27] feat(IssueValidationService): display issues based on token state --- .../Client/Pages/BaseConfiguration.razor | 5 +++ TeslaSolarCharger/Client/Pages/Index.razor | 6 ++- .../Resources/PossibleIssues/IssueKeys.cs | 40 +++++++++-------- .../PossibleIssues/PossibleIssues.cs | 32 ++++++++++++-- .../Server/Services/IssueValidationService.cs | 44 ++++++++++++++++--- .../Server/Services/TeslaFleetApiService.cs | 6 ++- .../Shared/Enums/FleetApiTokenState.cs | 1 + 7 files changed, 104 insertions(+), 30 deletions(-) diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index d12583f0e..3a5e73821 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -48,6 +48,11 @@ else You already have requested a token but did not receive it yet. It can take up to five minutes to receive the token. If the token did not arrive within five minutes please try again: break; + case FleetApiTokenState.TokenRequestExpired: +
+ Your tiken request has expired. Please generate a new token: +
+ break; case FleetApiTokenState.Expired:
Your token has expired. This could happen if you changed your Tesla password or did not use TeslaSolarCharger for too long. Please generate a new token: diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index ce7d9e843..529372702 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -358,9 +358,11 @@ else {

Version: @_version

} -
@_installationId
- CurrentCulture: @CultureInfo.CurrentCulture + Installation ID: @_installationId +
+
+ Language settings: @CultureInfo.CurrentCulture
diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index fbbe6ac69..ced0f3705 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -2,22 +2,26 @@ public class IssueKeys { - public string MqttNotConnected = "MqttNotConnected"; - public string CarSocLimitNotReadable = "CarSocLimitNotReadable"; - public string CarSocNotReadable = "CarSocNotReadable"; - public string GridPowerNotAvailable = "GridPowerNotAvailable"; - public string InverterPowerNotAvailable = "InverterPowerNotAvailable"; - public string HomeBatterySocNotAvailable = "HomeBatterySocNotAvailable"; - public string HomeBatterySocNotPlausible = "HomeBatterySocNotPlausible"; - public string HomeBatteryPowerNotAvailable = "HomeBatteryPowerNotAvailable"; - public string HomeBatteryMinimumSocNotConfigured = "HomeBatteryMinimumSocNotConfigured"; - public string HomeBatteryChargingPowerNotConfigured = "HomeBatteryChargingPowerNotConfigured"; - public string TeslaMateApiNotAvailable = "TeslaMateApiNotAvailable"; - public string DatabaseNotAvailable = "DatabaseNotAvailable"; - public string GeofenceNotAvailable = "GeofenceNotAvailable"; - public string CarIdNotAvailable = "CarIdNotAvailable"; - public string VersionNotUpToDate = "VersionNotUpToDate"; - public string CorrectionFactorZero = "CorrectionFactorZero"; - public string ServerTimeZoneDifferentFromClient = "ServerTimeZoneDifferentFromClient"; - public string NewTeslaApi = "NewTeslaApi"; + public string MqttNotConnected => "MqttNotConnected"; + public string CarSocLimitNotReadable => "CarSocLimitNotReadable"; + public string CarSocNotReadable => "CarSocNotReadable"; + public string GridPowerNotAvailable => "GridPowerNotAvailable"; + public string InverterPowerNotAvailable => "InverterPowerNotAvailable"; + public string HomeBatterySocNotAvailable => "HomeBatterySocNotAvailable"; + public string HomeBatterySocNotPlausible => "HomeBatterySocNotPlausible"; + public string HomeBatteryPowerNotAvailable => "HomeBatteryPowerNotAvailable"; + public string HomeBatteryMinimumSocNotConfigured => "HomeBatteryMinimumSocNotConfigured"; + public string HomeBatteryChargingPowerNotConfigured => "HomeBatteryChargingPowerNotConfigured"; + public string TeslaMateApiNotAvailable => "TeslaMateApiNotAvailable"; + public string DatabaseNotAvailable => "DatabaseNotAvailable"; + public string GeofenceNotAvailable => "GeofenceNotAvailable"; + public string CarIdNotAvailable => "CarIdNotAvailable"; + public string VersionNotUpToDate => "VersionNotUpToDate"; + public string CorrectionFactorZero => "CorrectionFactorZero"; + public string ServerTimeZoneDifferentFromClient => "ServerTimeZoneDifferentFromClient"; + public string NewTeslaApiNotUsed => "NewTeslaApiNotUsed"; + public string FleetApiTokenNotRequested => "FleetApiTokenNotRequested"; + public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; + public string FleetApiTokenNotReceived => "FleetApiTokenNotReceived"; + public string FleetApiTokenExpired => "FleetApiTokenExpired"; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index a25fdb6f0..fc9c9d7d6 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -135,10 +135,36 @@ public PossibleIssues(IssueKeys issueKeys) ) }, { - issueKeys.NewTeslaApi, CreateIssue("New cars are currently not supported", + issueKeys.NewTeslaApiNotUsed, CreateIssue("New cars need a new Tesla API. As this is in a very early beta state I highly recommend not using it if your car supports the old API!", IssueType.Information, - "If there are any updates on this topic, I will post them here.", - "Sorry for this information beeing not removable - it will be possible in a future update." + "To use the new API add UseFleetApi=true as environment variable in your docker-compose.yml", + "Sorry for this information beeing not removable - as switching to the new API in January 2024 as default you won't see this information from then on." + ) + }, + { + issueKeys.FleetApiTokenNotRequested, CreateIssue("You did not request a Tesla Token, yet.", + IssueType.Error, + "Open the Base Configuration and request a new token." + ) + }, + { + issueKeys.FleetApiTokenNotReceived, CreateIssue("The Tesla token was not received, yet.", + IssueType.Error, + "Getting the Token can take up to five minutes after submitting your password.", + "If waiting five minutes does not help, open the Base Configuration and request a new token." + ) + }, + { + issueKeys.FleetApiTokenRequestExpired, CreateIssue("The Tesla token could not be received.", + IssueType.Error, + "Open the Base Configuration and request a new token.", + "If this issue keeps occuring, feel free to open an issue on Github including the last 5 chars of your installation ID (bottom of the page). Do NOT include the whole ID." + ) + }, + { + issueKeys.FleetApiTokenExpired, CreateIssue("Your Tesla token has expired, this can occur when you changed your password or did not use the TeslaSolarCharger for too long..", + IssueType.Error, + "Open the Base Configuration and request a new token." ) }, }; diff --git a/TeslaSolarCharger/Server/Services/IssueValidationService.cs b/TeslaSolarCharger/Server/Services/IssueValidationService.cs index 1d6c5eee8..280c11227 100644 --- a/TeslaSolarCharger/Server/Services/IssueValidationService.cs +++ b/TeslaSolarCharger/Server/Services/IssueValidationService.cs @@ -3,6 +3,7 @@ using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Resources.PossibleIssues; +using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Dtos.BaseConfiguration; @@ -23,11 +24,12 @@ public class IssueValidationService : IIssueValidationService private readonly ITeslamateContext _teslamateContext; private readonly IConstants _constants; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ITeslaFleetApiService _teslaFleetApiService; public IssueValidationService(ILogger logger, ISettings settings, ITeslaMateMqttService teslaMateMqttService, IPossibleIssues possibleIssues, IssueKeys issueKeys, IConfigurationWrapper configurationWrapper, ITeslamateContext teslamateContext, - IConstants constants, IDateTimeProvider dateTimeProvider) + IConstants constants, IDateTimeProvider dateTimeProvider, ITeslaFleetApiService teslaFleetApiService) { _logger = logger; _settings = settings; @@ -38,23 +40,53 @@ public IssueValidationService(ILogger logger, ISettings _teslamateContext = teslamateContext; _constants = constants; _dateTimeProvider = dateTimeProvider; + _teslaFleetApiService = teslaFleetApiService; } public async Task> RefreshIssues(TimeSpan clientTimeZoneId) { _logger.LogTrace("{method}()", nameof(RefreshIssues)); - var issueList = new List + var issueList = new List(); + if (!_configurationWrapper.UseFleetApi()) { - _possibleIssues.GetIssueByKey(_issueKeys.NewTeslaApi), - }; + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.NewTeslaApiNotUsed)); + } issueList.AddRange(GetServerConfigurationIssues(clientTimeZoneId)); if (Debugger.IsAttached) { - return issueList; + //return issueList; } issueList.AddRange(GetMqttIssues()); issueList.AddRange(PvValueIssues()); - issueList.AddRange(await GetTeslaMateApiIssues().ConfigureAwait(false)); + if (!_configurationWrapper.UseFleetApi()) + { + issueList.AddRange(await GetTeslaMateApiIssues().ConfigureAwait(false)); + } + else + { + var tokenState = (await _teslaFleetApiService.GetFleetApiTokenState().ConfigureAwait(false)).Value; + switch (tokenState) + { + case FleetApiTokenState.NotNeeded: + break; + case FleetApiTokenState.NotRequested: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotRequested)); + break; + case FleetApiTokenState.TokenRequestExpired: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenRequestExpired)); + break; + case FleetApiTokenState.NotReceived: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotReceived)); + break; + case FleetApiTokenState.Expired: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenExpired)); + break; + case FleetApiTokenState.UpToDate: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } issueList.AddRange(await GetDatabaseIssues().ConfigureAwait(false)); issueList.AddRange(SofwareIssues()); issueList.AddRange(ConfigurationIssues()); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 5d1de30f2..9ab95cd86 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -221,7 +221,11 @@ public async Task> GetFleetApiTokenState() return new DtoValue(FleetApiTokenState.NotRequested); } var tokenRequestedDate = DateTime.Parse(tokenRequestedDateString, null, DateTimeStyles.RoundtripKind); - //ToDo: return not requested if request is older than x -> Currently not nown as not known how old a code can be to create a token out of it. + var currentDate = _dateTimeProvider.UtcNow(); + if (tokenRequestedDate < currentDate.AddMinutes(-5)) + { + return new DtoValue(FleetApiTokenState.TokenRequestExpired); + } return new DtoValue(FleetApiTokenState.NotReceived); } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index e8e1e9773..e246a4b62 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -4,6 +4,7 @@ public enum FleetApiTokenState { NotNeeded, NotRequested, + TokenRequestExpired, NotReceived, Expired, UpToDate, From a1ce5b07a18d835ceb98487bb4b73b47ebf63574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 12:37:58 +0100 Subject: [PATCH 18/27] feat(TeslaFleetApiService): do not retry getting a tiken if request has expired. --- TeslaSolarCharger/Client/Pages/BaseConfiguration.razor | 2 +- TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 3a5e73821..3e7771e99 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -50,7 +50,7 @@ else break; case FleetApiTokenState.TokenRequestExpired:
- Your tiken request has expired. Please generate a new token: + Your token request has expired. Please generate a new token:
break; case FleetApiTokenState.Expired: diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 9ab95cd86..fcd8c932d 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -155,6 +155,9 @@ public async Task RefreshTokenAsync() case FleetApiTokenState.NotRequested: _logger.LogDebug("No token has been requested, yet."); return; + case FleetApiTokenState.TokenRequestExpired: + _logger.LogError("Your toke request has expired, create a new one."); + return; case FleetApiTokenState.NotReceived: break; case FleetApiTokenState.Expired: From c60049b33410e7ad47ac0787ef9dc3d50a514181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 12:57:00 +0100 Subject: [PATCH 19/27] feat(TeslaFleetApiService): add scheduledChargingSupport --- .../Server/Services/TeslaFleetApiService.cs | 109 +++++++++++++++++- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index fcd8c932d..373a1f57a 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -13,6 +13,8 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Dtos.Contracts; +using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.SharedBackend.Contracts; @@ -29,16 +31,18 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly IConstants _constants; private readonly ITscConfigurationService _tscConfigurationService; private readonly IBackendApiService _backendApiService; + private readonly ISettings _settings; private readonly string _chargeStartComand = "command/charge_start"; private readonly string _chargeStopComand = "command/charge_stop"; private readonly string _setChargingAmps = "command/set_charging_amps"; + private readonly string _setScheduledCharging = "command/set_scheduled_charging"; private readonly string _wakeUpComand = "wake_up"; public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IDateTimeProvider dateTimeProvider, ITeslamateContext teslamateContext, IConfigurationWrapper configurationWrapper, ITeslamateApiService teslamateApiService, IConstants constants, ITscConfigurationService tscConfigurationService, - IBackendApiService backendApiService) + IBackendApiService backendApiService, ISettings settings) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; @@ -49,6 +53,7 @@ public TeslaFleetApiService(ILogger logger, ITeslaSolarCha _constants = constants; _tscConfigurationService = tscConfigurationService; _backendApiService = backendApiService; + _settings = settings; } public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) @@ -91,10 +96,106 @@ public async Task SetAmp(int carId, int amps) var result = await SendCommandToTeslaApi(id, _setChargingAmps, commandData).ConfigureAwait(false); } - public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) + public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) { - _logger.LogError("This is currently not supported with Fleet API"); - return Task.CompletedTask; + _logger.LogTrace("{method}({param1}, {param2})", nameof(SetScheduledCharging), carId, chargingStartTime); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var car = _settings.Cars.First(c => c.Id == carId); + if (!IsChargingScheduleChangeNeeded(chargingStartTime, _dateTimeProvider.DateTimeOffSetNow(), car, out var parameters)) + { + _logger.LogDebug("No change in updating scheduled charging needed."); + return; + } + + await WakeUpCarIfNeeded(carId, car.CarState.State).ConfigureAwait(false); + + var result = await SendCommandToTeslaApi(id, _setScheduledCharging, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); + //assume update was sucessfull as update is not working after mosquitto restart (or wrong cached State) + if (parameters["enable"] == "false") + { + car.CarState.ScheduledChargingStartTime = null; + } + } + + internal bool IsChargingScheduleChangeNeeded(DateTimeOffset? chargingStartTime, DateTimeOffset currentDate, Car car, out Dictionary parameters) + { + _logger.LogTrace("{method}({startTime}, {currentDate}, {carId}, {parameters})", nameof(IsChargingScheduleChangeNeeded), chargingStartTime, currentDate, car.Id, nameof(parameters)); + parameters = new Dictionary(); + if (chargingStartTime != null) + { + _logger.LogTrace("{chargingStartTime} is not null", nameof(chargingStartTime)); + chargingStartTime = RoundToNextQuarterHour(chargingStartTime.Value); + } + if (car.CarState.ScheduledChargingStartTime == chargingStartTime) + { + _logger.LogDebug("Correct charging start time already set."); + return false; + } + + if (chargingStartTime == null) + { + _logger.LogDebug("Set chargingStartTime to null."); + parameters = new Dictionary() + { + { "enable", "false" }, + { "time", 0.ToString() }, + }; + return true; + } + + var localStartTime = chargingStartTime.Value.ToLocalTime().TimeOfDay; + var minutesFromMidNight = (int)localStartTime.TotalMinutes; + var timeUntilChargeStart = chargingStartTime.Value - currentDate; + var scheduledChargeShouldBeSet = true; + + if (car.CarState.ScheduledChargingStartTime == chargingStartTime) + { + _logger.LogDebug("Correct charging start time already set."); + return true; + } + + //ToDo: maybe disable scheduled charge in this case. + if (timeUntilChargeStart <= TimeSpan.Zero || timeUntilChargeStart.TotalHours > 24) + { + _logger.LogDebug("Charge schedule should not be changed, as time until charge start is higher than 24 hours or lower than zero."); + return false; + } + + if (car.CarState.ScheduledChargingStartTime == null && !scheduledChargeShouldBeSet) + { + _logger.LogDebug("No charge schedule set and no charge schedule should be set."); + return true; + } + _logger.LogDebug("Normal parameter set."); + parameters = new Dictionary() + { + { "enable", scheduledChargeShouldBeSet ? "true" : "false" }, + { "time", minutesFromMidNight.ToString() }, + }; + _logger.LogTrace("{@parameters}", parameters); + return true; + } + + internal DateTimeOffset RoundToNextQuarterHour(DateTimeOffset chargingStartTime) + { + var maximumTeslaChargeStartAccuracyMinutes = 15; + var minutes = chargingStartTime.Minute; // Aktuelle Minute des DateTimeOffset-Objekts + + // Runden auf die nächste viertel Stunde + var roundedMinutes = (int)Math.Ceiling((double)minutes / maximumTeslaChargeStartAccuracyMinutes) * + maximumTeslaChargeStartAccuracyMinutes; + var additionalHours = 0; + if (roundedMinutes == 60) + { + roundedMinutes = 0; + additionalHours = 1; + } + + var newNotRoundedDateTime = chargingStartTime.AddHours(additionalHours); + chargingStartTime = new DateTimeOffset(newNotRoundedDateTime.Year, newNotRoundedDateTime.Month, + newNotRoundedDateTime.Day, newNotRoundedDateTime.Hour, roundedMinutes, 0, newNotRoundedDateTime.Offset); + _logger.LogDebug("Rounded charging Start time: {chargingStartTime}", chargingStartTime); + return chargingStartTime; } private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) From cca31e0b87200de971bf6473169fd29b8504d42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 20:07:07 +0100 Subject: [PATCH 20/27] feat(FleetApiService): can handle unauthorized on token refresh --- .../TeslaSolarChargerContext.cs | 4 + ...44_MakeConfigurationKeysUnique.Designer.cs | 229 ++++++++++++++++++ ...31204133244_MakeConfigurationKeysUnique.cs | 28 +++ .../TeslaSolarChargerContextModelSnapshot.cs | 3 + .../Contracts/IConstants.cs | 1 + .../Values/Constants.cs | 1 + .../Client/Pages/BaseConfiguration.razor | 7 +- .../Resources/PossibleIssues/IssueKeys.cs | 1 + .../PossibleIssues/PossibleIssues.cs | 9 +- .../Server/Services/BackendApiService.cs | 4 + .../Server/Services/IssueValidationService.cs | 3 + .../Server/Services/TeslaFleetApiService.cs | 27 +++ .../Shared/Enums/FleetApiTokenState.cs | 1 + 13 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.cs diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index c4f9c6e2b..86d618e2d 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -43,6 +43,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .Property(c => c.EnergyProvider) .HasDefaultValue(EnergyProvider.OldTeslaSolarChargerConfig); + + modelBuilder.Entity() + .HasIndex(c => c.Key) + .IsUnique(); } #pragma warning disable CS8618 diff --git a/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.Designer.cs b/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.Designer.cs new file mode 100644 index 000000000..dde528dc4 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.Designer.cs @@ -0,0 +1,229 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20231204133244_MakeConfigurationKeysUnique")] + partial class MakeConfigurationKeysUnique + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("IdToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Region") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TeslaTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("TscConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.cs b/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.cs new file mode 100644 index 000000000..146ea81d8 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20231204133244_MakeConfigurationKeysUnique.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class MakeConfigurationKeysUnique : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_TscConfigurations_Key", + table: "TscConfigurations", + column: "Key", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_TscConfigurations_Key", + table: "TscConfigurations"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index d11b2cbb7..3bc817e36 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -199,6 +199,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Key") + .IsUnique(); + b.ToTable("TscConfigurations"); }); diff --git a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs index c3db4ee27..1c14e736b 100644 --- a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs +++ b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs @@ -13,4 +13,5 @@ public interface IConstants string InstallationIdKey { get; } string FleetApiTokenRequested { get; } + string TokenRefreshUnauthorized { get; } } diff --git a/TeslaSolarCharger.SharedBackend/Values/Constants.cs b/TeslaSolarCharger.SharedBackend/Values/Constants.cs index 806806b0d..76a08590f 100644 --- a/TeslaSolarCharger.SharedBackend/Values/Constants.cs +++ b/TeslaSolarCharger.SharedBackend/Values/Constants.cs @@ -12,4 +12,5 @@ public class Constants : IConstants public string InstallationIdKey => "InstallationId"; public string FleetApiTokenRequested => "FleetApiTokenRequested"; + public string TokenRefreshUnauthorized => "TokenRefreshUnauthorized"; } diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 3e7771e99..d8785ebd3 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -40,7 +40,7 @@ else { case FleetApiTokenState.NotRequested:
- You have to generate a token in order to use the Tesla Fleet API. + You have to generate a token in order to use the Tesla Fleet API. Important: You have to allow access to all scopes.
break; case FleetApiTokenState.NotReceived: @@ -53,6 +53,11 @@ else Your token request has expired. Please generate a new token: break; + case FleetApiTokenState.TokenUnauthorized: +
+ Your token is unauthorized, Please generate a new token and allow access to all scopes. If you did not allow all scopes, you need to remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After removing the app you need to wait 24 hours before requesting a new token as all future token requests lead to unauthorized. This seems to be a bug with Tesla's API at the moment. +
+ break; case FleetApiTokenState.Expired:
Your token has expired. This could happen if you changed your Tesla password or did not use TeslaSolarCharger for too long. Please generate a new token: diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index ced0f3705..f447c075f 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -21,6 +21,7 @@ public class IssueKeys public string ServerTimeZoneDifferentFromClient => "ServerTimeZoneDifferentFromClient"; public string NewTeslaApiNotUsed => "NewTeslaApiNotUsed"; public string FleetApiTokenNotRequested => "FleetApiTokenNotRequested"; + public string FleetApiTokenUnauthorized => "FleetApiTokenUnauthorized"; public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; public string FleetApiTokenNotReceived => "FleetApiTokenNotReceived"; public string FleetApiTokenExpired => "FleetApiTokenExpired"; diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index fc9c9d7d6..77454c799 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -144,7 +144,14 @@ public PossibleIssues(IssueKeys issueKeys) { issueKeys.FleetApiTokenNotRequested, CreateIssue("You did not request a Tesla Token, yet.", IssueType.Error, - "Open the Base Configuration and request a new token." + "Open the Base Configuration and request a new token. Important: You need to allow acces to all selectable scopes." + ) + }, + { + issueKeys.FleetApiTokenUnauthorized, CreateIssue("Your Tesla token is unauthorized, this could be due to a changed Tesla account password, or you did not select all scopes when granting the Tesla Solar Charger access.", + IssueType.Error, + "Open the Base Configuration, request a new token and select all available scopes.", + "If you did not allow all scopes, you need to remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After removing the app you need to wait 24 hours before requesting a new token as all future token requests lead to unauthorized. This seems to be a bug with Tesla's API at the moment." ) }, { diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 81b01e331..242b9c346 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -38,6 +38,10 @@ public async Task> StartTeslaOAuth(string locale) _logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); + var isTokenUnauthorizedConfigEntries = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.TokenRefreshUnauthorized) + .ToListAsync().ConfigureAwait(false); + _teslaSolarChargerContext.TscConfigurations.RemoveRange(isTokenUnauthorizedConfigEntries); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); var backendApiBaseUrl = _configurationWrapper.BackendApiBaseUrl(); diff --git a/TeslaSolarCharger/Server/Services/IssueValidationService.cs b/TeslaSolarCharger/Server/Services/IssueValidationService.cs index 280c11227..6c8077b3c 100644 --- a/TeslaSolarCharger/Server/Services/IssueValidationService.cs +++ b/TeslaSolarCharger/Server/Services/IssueValidationService.cs @@ -75,6 +75,9 @@ public async Task> RefreshIssues(TimeSpan clientTimeZoneId) case FleetApiTokenState.TokenRequestExpired: issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenRequestExpired)); break; + case FleetApiTokenState.TokenUnauthorized: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenUnauthorized)); + break; case FleetApiTokenState.NotReceived: issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotReceived)); break; diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 373a1f57a..13f3d0c2d 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -259,6 +259,8 @@ public async Task RefreshTokenAsync() case FleetApiTokenState.TokenRequestExpired: _logger.LogError("Your toke request has expired, create a new one."); return; + case FleetApiTokenState.TokenUnauthorized: + break; case FleetApiTokenState.NotReceived: break; case FleetApiTokenState.Expired: @@ -311,6 +313,13 @@ public async Task> GetFleetApiTokenState() { return new DtoValue(FleetApiTokenState.NotNeeded); } + var isCurrentRefreshTokenUnauthorized = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.TokenRefreshUnauthorized) + .AnyAsync().ConfigureAwait(false); + if (isCurrentRefreshTokenUnauthorized) + { + return new DtoValue(FleetApiTokenState.TokenUnauthorized); + } var token = await _teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); if (token != null) { @@ -340,6 +349,14 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() .OrderByDescending(t => t.ExpiresAtUtc) .FirstAsync().ConfigureAwait(false); var minimumTokenLifeTime = TimeSpan.FromMinutes(5); + var isCurrentRefreshTokenUnauthorized = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.TokenRefreshUnauthorized) + .AnyAsync().ConfigureAwait(false); + if (isCurrentRefreshTokenUnauthorized) + { + _logger.LogError("Token is unauthorized"); + throw new InvalidDataException("Current Tesla Fleet Api Token is unauthorized"); + } if (token.ExpiresAtUtc < (_dateTimeProvider.UtcNow() + minimumTokenLifeTime)) { _logger.LogInformation("Token is expired. Getting new token."); @@ -359,6 +376,16 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false); + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + _logger.LogError("Either you have changed your Tesla password or you did not select all scopes, so TSC can't send commands to your car."); + _teslaSolarChargerContext.TeslaTokens.Remove(token); + _teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() + { + Key = _constants.TokenRefreshUnauthorized, + }); + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } } response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index e246a4b62..be575d702 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -5,6 +5,7 @@ public enum FleetApiTokenState NotNeeded, NotRequested, TokenRequestExpired, + TokenUnauthorized, NotReceived, Expired, UpToDate, From 5057493596a8e54252b4ca0c7a01adfa886c125a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 4 Dec 2023 20:26:23 +0100 Subject: [PATCH 21/27] feat(TeslaFleetApiService):L do correct deserialization of command result --- .../Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs | 15 +++++++++++++-- .../Server/Services/TeslaFleetApiService.cs | 6 ++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs index dddbf2b38..da5f0bd93 100644 --- a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs @@ -1,7 +1,18 @@ -namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; +using Newtonsoft.Json; + +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; public class DtoVehicleCommandResult { - public bool Result { get; set; } + [JsonProperty("reason")] public string Reason { get; set; } + + [JsonProperty("result")] + public bool Result { get; set; } +} + +public class VehicleCommandResponse +{ + [JsonProperty("response")] + public DtoVehicleCommandResult Response { get; set; } } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 13f3d0c2d..f8dcf2fbb 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -235,13 +235,15 @@ await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), name $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}").ConfigureAwait(false); } _logger.LogDebug("Response: {responseString}", responseString); - var result = JsonConvert.DeserializeObject(responseString); + var root = JsonConvert.DeserializeObject(responseString); + var result = root?.Response; if (result?.Result == false) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Result of command request is false: {commandName}, {contentData}. Response string: {responseString}").ConfigureAwait(false); } - return await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + + return result ?? null; } public async Task RefreshTokenAsync() From 91561c71d08486afcc79bb88f7b47d152861a574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 14:40:06 +0100 Subject: [PATCH 22/27] feat(TeslaFleetApiService): add Tesla response string on unauthorized --- TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index f8dcf2fbb..75d70692b 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -385,6 +385,7 @@ await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), name _teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() { Key = _constants.TokenRefreshUnauthorized, + Value = responseString, }); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } From ec4f1a304606cdadef1a732bba2425912b82edf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 17:48:51 +0100 Subject: [PATCH 23/27] feat(FleetApiController): Add Dev only Set Charge Limit endpoint --- .../Server/Contracts/ITeslaService.cs | 1 + .../Server/Controllers/FleetApiController.cs | 26 ++++++++++++++++++- .../Server/Services/TeslaFleetApiService.cs | 18 ++++++++++++- .../Server/Services/TeslamateApiService.cs | 5 ++++ .../Shared/Contracts/IConfigurationWrapper.cs | 1 + .../Shared/Wrappers/ConfigurationWrapper.cs | 6 +++++ 6 files changed, 55 insertions(+), 2 deletions(-) diff --git a/TeslaSolarCharger/Server/Contracts/ITeslaService.cs b/TeslaSolarCharger/Server/Contracts/ITeslaService.cs index 42f648a74..ea364af13 100644 --- a/TeslaSolarCharger/Server/Contracts/ITeslaService.cs +++ b/TeslaSolarCharger/Server/Contracts/ITeslaService.cs @@ -9,4 +9,5 @@ public interface ITeslaService Task StopCharging(int carId); Task SetAmp(int carId, int amps); Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime); + Task SetChargeLimit(int carId, int limitSoC); } diff --git a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs index 650fd7175..29b645810 100644 --- a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.SharedBackend.Abstracts; @@ -10,11 +12,16 @@ public class FleetApiController : ApiBaseController { private readonly ITeslaFleetApiService _fleetApiService; private readonly IBackendApiService _backendApiService; + private readonly ITeslaService _teslaService; + private readonly IConfigurationWrapper _configurationWrapper; - public FleetApiController(ITeslaFleetApiService fleetApiService, IBackendApiService backendApiService) + public FleetApiController(ITeslaFleetApiService fleetApiService, IBackendApiService backendApiService, ITeslaService teslaService, + IConfigurationWrapper configurationWrapper) { _fleetApiService = fleetApiService; _backendApiService = backendApiService; + _teslaService = teslaService; + _configurationWrapper = configurationWrapper; } [HttpGet] @@ -22,4 +29,21 @@ public FleetApiController(ITeslaFleetApiService fleetApiService, IBackendApiServ [HttpGet] public Task> GetOauthUrl(string locale) => _backendApiService.StartTeslaOAuth(locale); + + [HttpGet] + public Task RefreshFleetApiToken() => _fleetApiService.RefreshTokenAsync(); + + /// + /// Note: This endpoint is only available in development environment + /// + /// Is thrown when not beeing in dev Mode + [HttpGet] + public Task SetChargeLimit(int carId, int percent) + { + if (!_configurationWrapper.IsDevelopmentEnvironment()) + { + throw new InvalidOperationException("This method is only available in development environment"); + } + return _teslaService.SetChargeLimit(carId, percent); + } } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 75d70692b..b7e005efe 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,8 +1,9 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using System.Globalization; using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaMate; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Contracts; @@ -17,6 +18,7 @@ using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.SharedBackend.Contracts; +using Car = TeslaSolarCharger.Shared.Dtos.Settings.Car; namespace TeslaSolarCharger.Server.Services; @@ -37,6 +39,7 @@ public class TeslaFleetApiService : ITeslaService, ITeslaFleetApiService private readonly string _chargeStopComand = "command/charge_stop"; private readonly string _setChargingAmps = "command/set_charging_amps"; private readonly string _setScheduledCharging = "command/set_scheduled_charging"; + private readonly string _setSocLimit = "command/set_charge_limit"; private readonly string _wakeUpComand = "wake_up"; public TeslaFleetApiService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, @@ -117,6 +120,19 @@ public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartT } } + public async Task SetChargeLimit(int carId, int limitSoC) + { + _logger.LogTrace("{method}({param1}, {param2})", nameof(SetChargeLimit), carId, limitSoC); + var id = await _teslamateContext.Cars.Where(c => c.Id == carId).Select(c => c.Eid).FirstAsync().ConfigureAwait(false); + var car = _settings.Cars.First(c => c.Id == carId); + await WakeUpCarIfNeeded(carId, car.CarState.State).ConfigureAwait(false); + var parameters = new Dictionary() + { + { "percent", limitSoC.ToString() }, + }; + await SendCommandToTeslaApi(id, _setSocLimit, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); + } + internal bool IsChargingScheduleChangeNeeded(DateTimeOffset? chargingStartTime, DateTimeOffset currentDate, Car car, out Dictionary parameters) { _logger.LogTrace("{method}({startTime}, {currentDate}, {carId}, {parameters})", nameof(IsChargingScheduleChangeNeeded), chargingStartTime, currentDate, car.Id, nameof(parameters)); diff --git a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs index db196438b..19b9d52f2 100644 --- a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs @@ -150,6 +150,11 @@ public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartT _logger.LogTrace("result: {resultContent}", result.Content.ReadAsStringAsync().Result); } + public Task SetChargeLimit(int carId, int limitSoC) + { + throw new NotImplementedException(); + } + internal bool IsChargingScheduleChangeNeeded(DateTimeOffset? chargingStartTime, DateTimeOffset currentDate, Car car, out Dictionary parameters) { _logger.LogTrace("{method}({startTime}, {currentDate}, {carId}, {parameters})", nameof(IsChargingScheduleChangeNeeded), chargingStartTime, currentDate, car.Id, nameof(parameters)); diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 297a353ad..e23caf90f 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -89,4 +89,5 @@ public interface IConfigurationWrapper string FleetApiClientId(); bool UseFleetApi(); string BackendApiBaseUrl(); + bool IsDevelopmentEnvironment(); } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index 783411645..3582e8a88 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -113,6 +113,12 @@ public string BackendApiBaseUrl() return value; } + public bool IsDevelopmentEnvironment() + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + return environment == "Development"; + } + public string FleetApiClientId() { var environmentVariableName = "FleetApiClientId"; From af1bab0f34b57eb6d77704a02c68d83a2a67e01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 17:49:44 +0100 Subject: [PATCH 24/27] feat(TeslaMateApiService): use Expires in to avoid first refresh immediatly --- .../Dtos/TeslaFleetApi/DtoGenericTeslaResponse.cs | 9 +++++++++ .../Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs | 6 ------ .../Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs | 1 + .../Server/Services/TeslaFleetApiService.cs | 11 ++++++----- 4 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoGenericTeslaResponse.cs diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoGenericTeslaResponse.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoGenericTeslaResponse.cs new file mode 100644 index 000000000..af25751f3 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoGenericTeslaResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; + +public class DtoGenericTeslaResponse where T : class +{ + [JsonProperty("response")] + public T Response { get; set; } +} diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs index da5f0bd93..9e92583c6 100644 --- a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleCommandResult.cs @@ -10,9 +10,3 @@ public class DtoVehicleCommandResult [JsonProperty("result")] public bool Result { get; set; } } - -public class VehicleCommandResponse -{ - [JsonProperty("response")] - public DtoVehicleCommandResult Response { get; set; } -} diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs index 89034191d..1d9facb72 100644 --- a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs @@ -8,4 +8,5 @@ public class DtoTeslaTscDeliveryToken public string RefreshToken { get; set; } public string IdToken { get; set; } public TeslaFleetApiRegion Region { get; set; } + public int ExpiresIn { get; set; } } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index b7e005efe..861ee8435 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using System.Globalization; using System.Net.Http.Headers; @@ -251,7 +251,7 @@ await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), name $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}").ConfigureAwait(false); } _logger.LogDebug("Response: {responseString}", responseString); - var root = JsonConvert.DeserializeObject(responseString); + var root = JsonConvert.DeserializeObject>(responseString); var result = root?.Response; if (result?.Result == false) { @@ -275,10 +275,11 @@ public async Task RefreshTokenAsync() _logger.LogDebug("No token has been requested, yet."); return; case FleetApiTokenState.TokenRequestExpired: - _logger.LogError("Your toke request has expired, create a new one."); + _logger.LogError("Your token request has expired, create a new one."); return; case FleetApiTokenState.TokenUnauthorized: - break; + _logger.LogError("Your refresh token is unauthorized, create a new token."); + return; case FleetApiTokenState.NotReceived: break; case FleetApiTokenState.Expired: @@ -319,7 +320,7 @@ public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token) AccessToken = token.AccessToken, RefreshToken = token.RefreshToken, IdToken = token.IdToken, - ExpiresAtUtc = _dateTimeProvider.UtcNow().AddMinutes(2), + ExpiresAtUtc = _dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), Region = token.Region, }); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); From adae6f6d8ef885798f4eea40b922422c9daeae0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 18:41:12 +0100 Subject: [PATCH 25/27] feat(TeslaApiService): can handle not enough scopes --- .../Contracts/IConstants.cs | 1 + .../Values/Constants.cs | 1 + .../Client/Pages/BaseConfiguration.razor | 7 +- .../Dtos/TeslaFleetApi/DtoWakeUpResult.cs | 57 ++++++++++++++++ .../Resources/PossibleIssues/IssueKeys.cs | 1 + .../PossibleIssues/PossibleIssues.cs | 10 ++- .../Server/Services/BackendApiService.cs | 7 +- .../Server/Services/IssueValidationService.cs | 3 + .../Server/Services/TeslaFleetApiService.cs | 65 +++++++++++++++---- .../Shared/Enums/FleetApiTokenState.cs | 1 + 10 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoWakeUpResult.cs diff --git a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs index 1c14e736b..7c669fd2d 100644 --- a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs +++ b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs @@ -14,4 +14,5 @@ public interface IConstants string InstallationIdKey { get; } string FleetApiTokenRequested { get; } string TokenRefreshUnauthorized { get; } + string TokenMissingScopes { get; } } diff --git a/TeslaSolarCharger.SharedBackend/Values/Constants.cs b/TeslaSolarCharger.SharedBackend/Values/Constants.cs index 76a08590f..b38907b48 100644 --- a/TeslaSolarCharger.SharedBackend/Values/Constants.cs +++ b/TeslaSolarCharger.SharedBackend/Values/Constants.cs @@ -13,4 +13,5 @@ public class Constants : IConstants public string InstallationIdKey => "InstallationId"; public string FleetApiTokenRequested => "FleetApiTokenRequested"; public string TokenRefreshUnauthorized => "TokenRefreshUnauthorized"; + public string TokenMissingScopes => "TokenMissingScopes"; } diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index d8785ebd3..464a08452 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -55,7 +55,12 @@ else break; case FleetApiTokenState.TokenUnauthorized:
- Your token is unauthorized, Please generate a new token and allow access to all scopes. If you did not allow all scopes, you need to remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After removing the app you need to wait 24 hours before requesting a new token as all future token requests lead to unauthorized. This seems to be a bug with Tesla's API at the moment. + Your token is unauthorized, Please generate a new token, allow access to all scopes and enable mobile access in your car. +
+ break; + case FleetApiTokenState.MissingScopes: +
+ Your token has missing scopes. Remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After that generate a new token and allow access to all scopes.
break; case FleetApiTokenState.Expired: diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoWakeUpResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoWakeUpResult.cs new file mode 100644 index 000000000..034b6571b --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoWakeUpResult.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; + +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; + +public class DtoWakeUpResult +{ + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("user_id")] + public int UserId { get; set; } + + [JsonProperty("vehicle_id")] + public int VehicleId { get; set; } + + [JsonProperty("vin")] + public string Vin { get; set; } + + [JsonProperty("color")] + public object Color { get; set; } + + [JsonProperty("access_type")] + public string AccessType { get; set; } + + [JsonProperty("granular_access")] + public GranularAccess GranularAccess { get; set; } + + [JsonProperty("tokens")] + public List Tokens { get; set; } + + [JsonProperty("state")] + public object State { get; set; } + + [JsonProperty("in_service")] + public bool InService { get; set; } + + [JsonProperty("id_s")] + public string IdS { get; set; } + + [JsonProperty("calendar_enabled")] + public bool CalendarEnabled { get; set; } + + [JsonProperty("api_version")] + public object ApiVersion { get; set; } + + [JsonProperty("backseat_token")] + public object BackseatToken { get; set; } + + [JsonProperty("backseat_token_updated_at")] + public object BackseatTokenUpdatedAt { get; set; } +} + +public class GranularAccess +{ + [JsonProperty("hide_private")] + public bool HidePrivate { get; set; } +} diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index f447c075f..a17e1c3df 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -22,6 +22,7 @@ public class IssueKeys public string NewTeslaApiNotUsed => "NewTeslaApiNotUsed"; public string FleetApiTokenNotRequested => "FleetApiTokenNotRequested"; public string FleetApiTokenUnauthorized => "FleetApiTokenUnauthorized"; + public string FleetApiTokenMissingScopes => "FleetApiTokenMissingScopes"; public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; public string FleetApiTokenNotReceived => "FleetApiTokenNotReceived"; public string FleetApiTokenExpired => "FleetApiTokenExpired"; diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 77454c799..0b6255f2d 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -148,10 +148,16 @@ public PossibleIssues(IssueKeys issueKeys) ) }, { - issueKeys.FleetApiTokenUnauthorized, CreateIssue("Your Tesla token is unauthorized, this could be due to a changed Tesla account password, or you did not select all scopes when granting the Tesla Solar Charger access.", + issueKeys.FleetApiTokenUnauthorized, CreateIssue("Your Tesla token is unauthorized, this could be due to a changed Tesla account password, or your you disabled mobile access in your car.", IssueType.Error, "Open the Base Configuration, request a new token and select all available scopes.", - "If you did not allow all scopes, you need to remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After removing the app you need to wait 24 hours before requesting a new token as all future token requests lead to unauthorized. This seems to be a bug with Tesla's API at the moment." + "Enable mobile access in your car." + ) + }, + { + issueKeys.FleetApiTokenMissingScopes, CreateIssue("Your Tesla token has missing scopes.", + IssueType.Error, + "Remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After that request a new token in the Base Configuration and select all available scopes." ) }, { diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 242b9c346..82f5785d3 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -38,10 +38,11 @@ public async Task> StartTeslaOAuth(string locale) _logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); - var isTokenUnauthorizedConfigEntries = await _teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == _constants.TokenRefreshUnauthorized) + var cconfigEntriesToRemove = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.TokenRefreshUnauthorized + || c.Key == _constants.TokenMissingScopes) .ToListAsync().ConfigureAwait(false); - _teslaSolarChargerContext.TscConfigurations.RemoveRange(isTokenUnauthorizedConfigEntries); + _teslaSolarChargerContext.TscConfigurations.RemoveRange(cconfigEntriesToRemove); await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); var backendApiBaseUrl = _configurationWrapper.BackendApiBaseUrl(); diff --git a/TeslaSolarCharger/Server/Services/IssueValidationService.cs b/TeslaSolarCharger/Server/Services/IssueValidationService.cs index 6c8077b3c..15565ea0c 100644 --- a/TeslaSolarCharger/Server/Services/IssueValidationService.cs +++ b/TeslaSolarCharger/Server/Services/IssueValidationService.cs @@ -78,6 +78,9 @@ public async Task> RefreshIssues(TimeSpan clientTimeZoneId) case FleetApiTokenState.TokenUnauthorized: issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenUnauthorized)); break; + case FleetApiTokenState.MissingScopes: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenMissingScopes)); + break; case FleetApiTokenState.NotReceived: issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotReceived)); break; diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 861ee8435..d5ff11b52 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using System.Globalization; +using System.Net; using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaMate; @@ -249,16 +250,21 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}").ConfigureAwait(false); + await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, accessToken, responseString).ConfigureAwait(false); } _logger.LogDebug("Response: {responseString}", responseString); - var root = JsonConvert.DeserializeObject>(responseString); - var result = root?.Response; + //Wake up command returns another result which is irrelevant + if (commandName == _wakeUpComand) + { + return null; + } + var teslaCommandResultResponse = JsonConvert.DeserializeObject>(responseString); + var result = teslaCommandResultResponse?.Response; if (result?.Result == false) { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Result of command request is false: {commandName}, {contentData}. Response string: {responseString}").ConfigureAwait(false); } - return result ?? null; } @@ -339,6 +345,13 @@ public async Task> GetFleetApiTokenState() { return new DtoValue(FleetApiTokenState.TokenUnauthorized); } + var hasCurrentTokenMissingScopes = await _teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == _constants.TokenMissingScopes) + .AnyAsync().ConfigureAwait(false); + if (hasCurrentTokenMissingScopes) + { + return new DtoValue(FleetApiTokenState.MissingScopes); + } var token = await _teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); if (token != null) { @@ -395,17 +408,7 @@ private async Task GetAccessTokenAndRefreshWhenNeededAsync() { await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false); - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _logger.LogError("Either you have changed your Tesla password or you did not select all scopes, so TSC can't send commands to your car."); - _teslaSolarChargerContext.TeslaTokens.Remove(token); - _teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() - { - Key = _constants.TokenRefreshUnauthorized, - Value = responseString, - }); - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - } + await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, token, responseString).ConfigureAwait(false); } response.EnsureSuccessStatusCode(); var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); @@ -419,4 +422,38 @@ await _backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), name return token; } + private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode, TeslaToken token, + string responseString) + { + _logger.LogTrace("{method}({statusCode}, {token}, {responseString})", nameof(HandleNonSuccessTeslaApiStatusCodes), statusCode, token, responseString); + switch (statusCode) + { + case HttpStatusCode.Unauthorized: + _logger.LogError( + "Your token or refresh token is invalid. Very likely you have changed your Tesla password."); + _teslaSolarChargerContext.TeslaTokens.Remove(token); + _teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() + { + Key = _constants.TokenRefreshUnauthorized, + Value = responseString, + }); + break; + case HttpStatusCode.Forbidden: + _logger.LogError("You did not select all scopes, so TSC can't send commands to your car."); + _teslaSolarChargerContext.TeslaTokens.Remove(token); + _teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() + { + Key = _constants.TokenMissingScopes, + Value = responseString, + }); + break; + default: + _logger.LogWarning( + "Staus Code {statusCode} is currently not handled, look into https://developer.tesla.com/docs/fleet-api#response-codes to check status code information", + statusCode); + return; + } + + await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index be575d702..215c423c6 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -6,6 +6,7 @@ public enum FleetApiTokenState NotRequested, TokenRequestExpired, TokenUnauthorized, + MissingScopes, NotReceived, Expired, UpToDate, From 83346fa8bf9937e25de2ce69e1911664359223eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 19:18:56 +0100 Subject: [PATCH 26/27] fix(BaseConfigurationRazor): fix link to Tesla third party apps --- TeslaSolarCharger/Client/Pages/BaseConfiguration.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 464a08452..6fd498a21 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -60,7 +60,7 @@ else break; case FleetApiTokenState.MissingScopes:
- Your token has missing scopes. Remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After that generate a new token and allow access to all scopes. + Your token has missing scopes. Remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After that generate a new token and allow access to all scopes.
break; case FleetApiTokenState.Expired: From 4b49c166ee86da6684a9e8cd2f966989b6ea9ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 6 Dec 2023 19:45:20 +0100 Subject: [PATCH 27/27] feat(README): add privacy notes --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 769922d79..406613aee 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ TeslaSolarCharger is a service to set one or multiple Teslas' charging current u - [How to use](#how-to-use) - [Charge Modes](#charge-modes) - [Generate logfiles](#generate-logfiles) +- [Privacy notes](#privacy-notes) ## How to install @@ -1031,3 +1032,9 @@ If you get an error like `Error: No such container:` you can look up the contain ```bash docker ps ``` + + +As the new Tesla Fleet API requires a domain and external Token creation from version 2.23.0 onwards, TSC transfers some data to the owner of this repository. By using this software, you accept the transfer of this data. As this is open source, you can see which data is transferred. For now (6th December 2023), the following data is transferred: +- Your access code is used to get the access token from Tesla (Note: the token itself is only stored locally in your TSC installation. It is only transferred via my server, but the token only exists in memory on the server itself. It is not stored in a database or log file) +- Your installation ID (GUID) is at the bottom of the page. Do not post this GUID in public forums, as it is used to deliver the Tesla access token to your installation. Note: There is only a five-minute time window between requesting and providing the token using the installation ID. After these 5 minutes, all requests are blocked.) +- Your installed version.