From 076f18d85331e59e1da9f0e03be1768fe64b460a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 8 Jun 2024 07:53:22 +0200 Subject: [PATCH 1/3] feat(TeslaApi): Can handle fleet API --- .../Entities/TeslaSolarCharger/Car.cs | 1 + ...08190920_AddUseBleColumnToCars.Designer.cs | 726 ++++++++++++++++++ .../20240608190920_AddUseBleColumnToCars.cs | 29 + .../TeslaSolarChargerContextModelSnapshot.cs | 3 + .../Dtos/DtoFleetApiRequest.cs | 1 + .../Client/Pages/CarSettings.razor | 1 + .../Server/Controllers/BleTestController.cs | 17 + .../Server/Dtos/Ble/DtoBleRequest.cs | 8 + .../Server/Dtos/Ble/DtoBleResult.cs | 10 + .../Server/ServiceCollectionExtensions.cs | 1 + .../Server/Services/ConfigJsonService.cs | 1 + .../Server/Services/Contracts/IBleService.cs | 10 + .../Server/Services/TeslaBleService.cs | 116 +++ .../Server/Services/TeslaFleetApiService.cs | 51 +- TeslaSolarCharger/Server/appsettings.json | 1 + .../Shared/Contracts/IConfigurationWrapper.cs | 1 + .../Shared/Dtos/CarBasicConfiguration.cs | 2 + .../Shared/Wrappers/ConfigurationWrapper.cs | 7 + 18 files changed, 971 insertions(+), 15 deletions(-) create mode 100644 TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.cs create mode 100644 TeslaSolarCharger/Server/Controllers/BleTestController.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Ble/DtoBleRequest.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Ble/DtoBleResult.cs create mode 100644 TeslaSolarCharger/Server/Services/Contracts/IBleService.cs create mode 100644 TeslaSolarCharger/Server/Services/TeslaBleService.cs diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs index 73462c0a9..b72019b18 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs @@ -42,6 +42,7 @@ public class Car public CarStateEnum? State { get; set; } public bool VehicleCommandProtocolRequired { get; set; } public DateTime? RateLimitedUntil { get; set; } + public bool UseBle { get; set; } public List ChargingProcesses { get; set; } = new List(); } diff --git a/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.Designer.cs new file mode 100644 index 000000000..f37a73747 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.Designer.cs @@ -0,0 +1,726 @@ +// +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("20240608190920_AddUseBleColumnToCars")] + partial class AddUseBleColumnToCars + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + 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.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargeMode") + .HasColumnType("INTEGER"); + + b.Property("ChargerActualCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerPhases") + .HasColumnType("INTEGER"); + + b.Property("ChargerPilotCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerRequestedCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerVoltage") + .HasColumnType("INTEGER"); + + b.Property("ChargingPriority") + .HasColumnType("INTEGER"); + + b.Property("ClimateOn") + .HasColumnType("INTEGER"); + + b.Property("IgnoreLatestTimeToReachSocDate") + .HasColumnType("INTEGER"); + + b.Property("LatestTimeToReachSoC") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("MaximumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumSoc") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PluggedIn") + .HasColumnType("INTEGER"); + + b.Property("RateLimitedUntil") + .HasColumnType("TEXT"); + + b.Property("ShouldBeManaged") + .HasColumnType("INTEGER"); + + b.Property("ShouldSetChargeStartTimes") + .HasColumnType("INTEGER"); + + b.Property("SoC") + .HasColumnType("INTEGER"); + + b.Property("SocLimit") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TeslaFleetApiState") + .HasColumnType("INTEGER"); + + b.Property("TeslaMateCarId") + .HasColumnType("INTEGER"); + + b.Property("UsableEnergy") + .HasColumnType("INTEGER"); + + b.Property("UseBle") + .HasColumnType("INTEGER"); + + b.Property("VehicleCommandProtocolRequired") + .HasColumnType("INTEGER"); + + b.Property("Vin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeslaMateCarId") + .IsUnique(); + + b.HasIndex("Vin") + .IsUnique(); + + b.ToTable("Cars"); + }); + + 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.ChargingDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("GridPower") + .HasColumnType("INTEGER"); + + b.Property("HomeBatteryPower") + .HasColumnType("INTEGER"); + + b.Property("SolarPower") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChargingProcessId"); + + b.ToTable("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("Cost") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("OldHandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UsedGridEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedHomeBatteryEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergyKwh") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.ToTable("ChargingProcesses"); + }); + + 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.ModbusConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectDelayMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("Endianess") + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ReadTimeoutMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("UnitIdentifier") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ModbusConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("INTEGER"); + + b.Property("BitStartIndex") + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("InvertedByModbusResultConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("ModbusConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RegisterType") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvertedByModbusResultConfigurationId"); + + b.HasIndex("ModbusConfigurationId"); + + b.ToTable("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MqttConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("MqttConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MqttConfigurationId"); + + b.ToTable("MqttResultConfigurations"); + }); + + 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.RestValueConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HttpMethod") + .HasColumnType("INTEGER"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("RestValueConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfigurationHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId", "Key") + .IsUnique(); + + b.ToTable("RestValueConfigurationHeaders"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId"); + + b.ToTable("RestValueResultConfigurations"); + }); + + 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.Property("UnauthorizedCounter") + .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.ChargingDetail", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", "ChargingProcess") + .WithMany("ChargingDetails") + .HasForeignKey("ChargingProcessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChargingProcess"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", "Car") + .WithMany("ChargingProcesses") + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", "InvertedByModbusResultConfiguration") + .WithMany() + .HasForeignKey("InvertedByModbusResultConfigurationId"); + + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", "ModbusConfiguration") + .WithMany("ModbusResultConfigurations") + .HasForeignKey("ModbusConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InvertedByModbusResultConfiguration"); + + b.Navigation("ModbusConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", "MqttConfiguration") + .WithMany("MqttResultConfigurations") + .HasForeignKey("MqttConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MqttConfiguration"); + }); + + 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.RestValueConfigurationHeader", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("Headers") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("RestValueResultConfigurations") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Navigation("ChargingProcesses"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Navigation("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", b => + { + b.Navigation("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Navigation("MqttResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", b => + { + b.Navigation("Headers"); + + b.Navigation("RestValueResultConfigurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.cs b/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.cs new file mode 100644 index 000000000..53411b6b4 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240608190920_AddUseBleColumnToCars.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddUseBleColumnToCars : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseBle", + table: "Cars", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseBle", + table: "Cars"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index a22f91835..fccc3024a 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -125,6 +125,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UsableEnergy") .HasColumnType("INTEGER"); + b.Property("UseBle") + .HasColumnType("INTEGER"); + b.Property("VehicleCommandProtocolRequired") .HasColumnType("INTEGER"); diff --git a/TeslaSolarCharger.SharedBackend/Dtos/DtoFleetApiRequest.cs b/TeslaSolarCharger.SharedBackend/Dtos/DtoFleetApiRequest.cs index ecdce4aa2..383bb4680 100644 --- a/TeslaSolarCharger.SharedBackend/Dtos/DtoFleetApiRequest.cs +++ b/TeslaSolarCharger.SharedBackend/Dtos/DtoFleetApiRequest.cs @@ -4,4 +4,5 @@ public class DtoFleetApiRequest { public string RequestUrl { get; set; } public bool NeedsProxy { get; set; } + public bool BleCompatible { get; set; } } diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index a7f378309..6aaf84cb7 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -25,6 +25,7 @@ else + } diff --git a/TeslaSolarCharger/Server/Controllers/BleTestController.cs b/TeslaSolarCharger/Server/Controllers/BleTestController.cs new file mode 100644 index 000000000..110755659 --- /dev/null +++ b/TeslaSolarCharger/Server/Controllers/BleTestController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.SharedBackend.Abstracts; + +namespace TeslaSolarCharger.Server.Controllers; + +public class BleTestController (IBleService bleService) : ApiBaseController +{ + [HttpGet] + public Task StartCharging(string vin) => bleService.StartCharging(vin); + + [HttpGet] + public Task StopCharging(string vin) => bleService.StopCharging(vin); + + [HttpGet] + public Task SetAmp(string vin, int amps) => bleService.SetAmp(vin, amps); +} diff --git a/TeslaSolarCharger/Server/Dtos/Ble/DtoBleRequest.cs b/TeslaSolarCharger/Server/Dtos/Ble/DtoBleRequest.cs new file mode 100644 index 000000000..75aedd86c --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Ble/DtoBleRequest.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Server.Dtos.Ble; + +public class DtoBleRequest +{ + public string Vin { get; set; } + public string CommandName { get; set; } + public List Parameters { get; set; } = new(); +} diff --git a/TeslaSolarCharger/Server/Dtos/Ble/DtoBleResult.cs b/TeslaSolarCharger/Server/Dtos/Ble/DtoBleResult.cs new file mode 100644 index 000000000..c077f89e0 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Ble/DtoBleResult.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace TeslaSolarCharger.Server.Dtos.Ble; + +public class DtoBleResult +{ + public HttpStatusCode StatusCode { get; set; } + public string Message { get; set; } + public bool Success { get; set; } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index b99e00a19..d00b4e755 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -103,6 +103,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs index a035316e9..45a3f3a52 100644 --- a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs +++ b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs @@ -204,6 +204,7 @@ public async Task UpdateCarBasicConfiguration(int carId, CarBasicConfiguration c databaseCar.ChargingPriority = carBasicConfiguration.ChargingPriority; databaseCar.ShouldBeManaged = carBasicConfiguration.ShouldBeManaged; databaseCar.ShouldSetChargeStartTimes = carBasicConfiguration.ShouldSetChargeStartTimes; + databaseCar.UseBle = carBasicConfiguration.UseBle; await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var settingsCar = settings.Cars.First(c => c.Id == carId); settingsCar.Name = carBasicConfiguration.Name; diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs new file mode 100644 index 000000000..99dcf44a5 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs @@ -0,0 +1,10 @@ +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface IBleService +{ + Task StartCharging(string vin); + Task StopCharging(string vin); + Task SetAmp(string vin, int amps); +} diff --git a/TeslaSolarCharger/Server/Services/TeslaBleService.cs b/TeslaSolarCharger/Server/Services/TeslaBleService.cs new file mode 100644 index 000000000..737b5034b --- /dev/null +++ b/TeslaSolarCharger/Server/Services/TeslaBleService.cs @@ -0,0 +1,116 @@ +using Newtonsoft.Json; +using System.Web; +using TeslaSolarCharger.Server.Dtos.Ble; +using TeslaSolarCharger.Server.Services.ApiServices.Contracts; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos.Contracts; +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Server.Services; + +public class TeslaBleService(ILogger logger, + IConfigurationWrapper configurationWrapper, + ITeslamateApiService teslamateApiService, + ISettings settings) : IBleService +{ + public async Task StartCharging(string vin) + { + logger.LogTrace("{method}({vin})", nameof(StartCharging), vin); + var request = new DtoBleRequest + { + Vin = vin, + CommandName = "charging-start", + }; + var result = await SendCommandToBle(request).ConfigureAwait(false); + } + + public Task WakeUpCar(int carId) + { + throw new NotImplementedException(); + } + + public async Task StopCharging(string vin) + { + var request = new DtoBleRequest + { + Vin = vin, + CommandName = "charging-stop", + }; + var result = await SendCommandToBle(request).ConfigureAwait(false); + } + + public async Task SetAmp(string vin, int amps) + { + logger.LogTrace("{method}({vin}, {amps})", nameof(SetAmp), vin, amps); + var request = new DtoBleRequest + { + Vin = vin, + CommandName = "charging-set-amps", + Parameters = new List { amps.ToString() }, + }; + var result = await SendCommandToBle(request).ConfigureAwait(false); + } + + public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) + { + throw new NotImplementedException(); + } + + public Task SetChargeLimit(int carId, int limitSoC) + { + throw new NotImplementedException(); + } + + private async Task SendCommandToBle(DtoBleRequest request) + { + logger.LogTrace("{method}({@request})", nameof(SendCommandToBle), request); + var bleBaseUrl = configurationWrapper.BleBaseUrl(); + if (!bleBaseUrl.EndsWith("/")) + { + bleBaseUrl += "/"; + } + bleBaseUrl += "Command/ExecuteCommand"; + var queryString = HttpUtility.ParseQueryString(string.Empty); + queryString.Add("vin", request.Vin); + queryString.Add("command", request.CommandName); + var url = $"{bleBaseUrl}?{queryString}"; + logger.LogTrace("Ble Url: {bleUrl}", url); + logger.LogTrace("Parameters: {@parameters}", request.Parameters); + using var client = new HttpClient(); + var response = await client.PostAsJsonAsync(url, request.Parameters).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + logger.LogError("Failed to send command to BLE. StatusCode: {statusCode} {responseContent}", response.StatusCode, responseContent); + throw new InvalidOperationException(); + } + var result = JsonConvert.DeserializeObject(responseContent); + return result ?? throw new InvalidDataException($"Could not parse {responseContent} to {nameof(DtoBleResult)}"); + } + + 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"); + var teslaMateCarId = settings.Cars.First(c => c.Id == carId).TeslaMateCarId; + if (teslaMateCarId != default) + { + await teslamateApiService.ResumeLogging(teslaMateCarId.Value).ConfigureAwait(false); + } + break; + } + } + + private string GetVinByCarId(int carId) + { + var vin = settings.Cars.First(c => c.Id == carId).Vin; + return vin; + } +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 869e251f5..95fdb092d 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -33,23 +33,27 @@ public class TeslaFleetApiService( ITscConfigurationService tscConfigurationService, IBackendApiService backendApiService, ISettings settings, - IConfigJsonService configJsonService) + IConfigJsonService configJsonService, + IBleService bleService) : ITeslaService, ITeslaFleetApiService { private DtoFleetApiRequest ChargeStartRequest => new() { RequestUrl = "command/charge_start", NeedsProxy = true, + BleCompatible = true, }; private DtoFleetApiRequest ChargeStopRequest => new() { RequestUrl = "command/charge_stop", NeedsProxy = true, + BleCompatible = true, }; private DtoFleetApiRequest SetChargingAmpsRequest => new() { RequestUrl = "command/set_charging_amps", NeedsProxy = true, + BleCompatible = true, }; private DtoFleetApiRequest SetScheduledChargingRequest => new() { @@ -128,19 +132,8 @@ public async Task SetAmp(int carId, int amps) } var vin = GetVinByCarId(carId); var commandData = $"{{\"charging_amps\":{amps}}}"; - var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData).ConfigureAwait(false); - if (amps < 5 && car.LastSetAmp >= 5 - || amps >= 5 && car.LastSetAmp < 5) - { - logger.LogDebug("Double set amp to be able to jump over or below 5A"); - await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); - result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData).ConfigureAwait(false); - } - - if (result?.Response?.Result == true) - { - car.LastSetAmp = amps; - } + var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData, amps).ConfigureAwait(false); + car.LastSetAmp = amps; } public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) @@ -462,9 +455,37 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) } } - private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}") where T : class + private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}", int? amp = null) where T : class { logger.LogTrace("{method}({vin}, {@fleetApiRequest}, {contentData})", nameof(SendCommandToTeslaApi), vin, fleetApiRequest, contentData); + if (fleetApiRequest.BleCompatible) + { + var isCarBleEnabled = await teslaSolarChargerContext.Cars + .Where(c => c.Vin == vin) + .Select(c => c.UseBle) + .FirstAsync(); + if (isCarBleEnabled) + { + var bleAddress = configurationWrapper.BleBaseUrl(); + if (!string.IsNullOrEmpty(bleAddress)) + { + if (fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) + { + await bleService.StartCharging(vin); + } + else if (fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) + { + await bleService.StopCharging(vin); + } + else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) + { + await bleService.SetAmp(vin, amp!.Value); + } + + return new DtoGenericTeslaResponse(){}; + } + } + } var accessToken = await GetAccessToken().ConfigureAwait(false); using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 153cd5251..0e6591648 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -69,6 +69,7 @@ "GetVehicleDataFromTesla": false, "GetVehicleDataFromTeslaDebug": false, "AwattarBaseUrl": "https://api.awattar.de/v1/marketdata", + "BleBaseUrl": "http://raspible:7210/api", "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": { diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 7e7dba1f7..8a1a9750c 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -99,4 +99,5 @@ public interface IConfigurationWrapper bool GetVehicleDataFromTesla(); bool GetVehicleDataFromTeslaDebug(); int? MaxInverterAcPower(); + string BleBaseUrl(); } diff --git a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs index 6a6230905..d25125c28 100644 --- a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs +++ b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs @@ -38,6 +38,8 @@ public CarBasicConfiguration(int id, string? name) public bool ShouldBeManaged { get; set; } = true; [HelperText("Enable this to use planned charges of your Tesla App. This ensures starting a planned charge even if the car can't be woken up via Tesla App.")] public bool ShouldSetChargeStartTimes { get; set; } + [HelperText("Use BLE communication to go around Tesla rate limits. Note: A BLE device (e.g. Raspberry Pi) with installed TeslaSolarChargerBle Container needs to be near your car.")] + public bool UseBle { get; set; } } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index 988d5bd5f..a8c288cfd 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -169,6 +169,13 @@ public string FleetApiClientId() return value; } + public string BleBaseUrl() + { + var environmentVariableName = "BleBaseUrl"; + var value = GetNotNullableConfigurationValue(environmentVariableName); + return value; + } + public TimeSpan ChargingValueJobUpdateIntervall() { var minimum = TimeSpan.FromSeconds(20); From 7e0d82c3e634899858b134115d45d259ecb96266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 8 Jun 2024 21:19:11 +0200 Subject: [PATCH 2/3] feat(IndexRazor): add info to increase car power adjustment intervall --- TeslaSolarCharger/Client/Pages/Index.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index 287500ce7..8f25c6d1c 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -152,7 +152,7 @@ else
Car currently rate limited.

- Your car is rate limited by Tesla and TSC won't be able to set the charging power until @(car.RateLimitedUntil.Value.ToLocalTime().ToString("g")). For now there is no solution to this as Tesla implemented the rate limits without an option to pay for more. So even after the rate limit is over, TSC only has 50 requests. After the 50 requests within 24 hours, the rate limit is present again for the next 24 hours.
+ Your car is rate limited by Tesla and TSC won't be able to set the charging power until @(car.RateLimitedUntil.Value.ToLocalTime().ToString("g")). For now there is no solution to this as Tesla implemented the rate limits without an option to pay for more. So even after the rate limit is over, TSC only has 50 requests. After the 50 requests within 24 hours, the rate limit is present again for the next 24 hours. As a workaround you can go to basic configuration, scroll down to advanced settings and set the Car power adjustment intervall to 1800 seconds. This leads to 50 calls per day max.

From 11aa768b2f499b1b70570a0966038d907bb72db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 8 Jun 2024 21:33:09 +0200 Subject: [PATCH 3/3] feat(CarSettings): can pair key --- .../Client/Pages/CarSettings.razor | 19 +++++++++++++++ ...{BleTestController.cs => BleController.cs} | 5 +++- .../Server/Services/Contracts/IBleService.cs | 1 + .../Server/Services/TeslaBleService.cs | 24 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) rename TeslaSolarCharger/Server/Controllers/{BleTestController.cs => BleController.cs} (74%) diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index 6aaf84cb7..4cd9b5254 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -27,6 +27,16 @@ else +
+ Note: Before TSC can update charge speed via BLE you must pair the car with TSC. +
+ Ble Pair +
+ @if (_pairingResults.ContainsKey(carBasicConfiguration.Vin)) + { + @_pairingResults[carBasicConfiguration.Vin] + } +
} } @@ -35,6 +45,8 @@ else private List? _carBasicConfigurations; private readonly List _savingCarIds = new(); + private Dictionary _pairingResults = new Dictionary(); + protected override async Task OnInitializedAsync() { _carBasicConfigurations = await HttpClient.GetFromJsonAsync>("/api/Config/GetCarBasicConfigurations").ConfigureAwait(false); @@ -54,4 +66,11 @@ else } _savingCarIds.RemoveAll(i => i == carId); } + + private async Task PairCar(string vin) + { + var result = await HttpClient.GetStringAsync($"/api/Ble/PairKey?vin={vin}").ConfigureAwait(false); + _pairingResults[vin] = result; + } + } \ No newline at end of file diff --git a/TeslaSolarCharger/Server/Controllers/BleTestController.cs b/TeslaSolarCharger/Server/Controllers/BleController.cs similarity index 74% rename from TeslaSolarCharger/Server/Controllers/BleTestController.cs rename to TeslaSolarCharger/Server/Controllers/BleController.cs index 110755659..fbefd78e8 100644 --- a/TeslaSolarCharger/Server/Controllers/BleTestController.cs +++ b/TeslaSolarCharger/Server/Controllers/BleController.cs @@ -4,8 +4,11 @@ namespace TeslaSolarCharger.Server.Controllers; -public class BleTestController (IBleService bleService) : ApiBaseController +public class BleController (IBleService bleService) : ApiBaseController { + [HttpGet] + public Task PairKey(string vin) => bleService.PairKey(vin); + [HttpGet] public Task StartCharging(string vin) => bleService.StartCharging(vin); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs index 99dcf44a5..71e7da197 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBleService.cs @@ -7,4 +7,5 @@ public interface IBleService Task StartCharging(string vin); Task StopCharging(string vin); Task SetAmp(string vin, int amps); + Task PairKey(string vin); } diff --git a/TeslaSolarCharger/Server/Services/TeslaBleService.cs b/TeslaSolarCharger/Server/Services/TeslaBleService.cs index 737b5034b..e5f282164 100644 --- a/TeslaSolarCharger/Server/Services/TeslaBleService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaBleService.cs @@ -52,6 +52,30 @@ public async Task SetAmp(string vin, int amps) var result = await SendCommandToBle(request).ConfigureAwait(false); } + public async Task PairKey(string vin) + { + logger.LogTrace("{method}({vin})", nameof(PairKey), vin); + var bleBaseUrl = configurationWrapper.BleBaseUrl(); + if (!bleBaseUrl.EndsWith("/")) + { + bleBaseUrl += "/"; + } + bleBaseUrl += "Pairing/PairCar"; + var queryString = HttpUtility.ParseQueryString(string.Empty); + queryString.Add("vin", vin); + var url = $"{bleBaseUrl}?{queryString}"; + logger.LogTrace("Ble Url: {bleUrl}", url); + using var client = new HttpClient(); + var response = await client.GetAsync(url).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + logger.LogError("Failed to send command to BLE. StatusCode: {statusCode} {responseContent}", response.StatusCode, responseContent); + throw new InvalidOperationException(); + } + return responseContent; + } + public Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) { throw new NotImplementedException();