diff --git a/Plugins.Modbus/Plugins.Modbus.csproj b/Plugins.Modbus/Plugins.Modbus.csproj index a6f0fde9e..77ca746aa 100644 --- a/Plugins.Modbus/Plugins.Modbus.csproj +++ b/Plugins.Modbus/Plugins.Modbus.csproj @@ -13,7 +13,7 @@ - + diff --git a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj index d1bd51cb2..a11f03de0 100644 --- a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj +++ b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj @@ -14,7 +14,7 @@ - + diff --git a/Plugins.SolarEdge/Plugins.SolarEdge.csproj b/Plugins.SolarEdge/Plugins.SolarEdge.csproj index e8b3eb423..f22cae188 100644 --- a/Plugins.SolarEdge/Plugins.SolarEdge.csproj +++ b/Plugins.SolarEdge/Plugins.SolarEdge.csproj @@ -13,7 +13,7 @@ - + diff --git a/Plugins.Solax/Plugins.Solax.csproj b/Plugins.Solax/Plugins.Solax.csproj index 5d3cd7725..87f606ff9 100644 --- a/Plugins.Solax/Plugins.Solax.csproj +++ b/Plugins.Solax/Plugins.Solax.csproj @@ -12,7 +12,7 @@ - + diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs index b72019b18..527d0a3b8 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs @@ -43,6 +43,7 @@ public class Car public bool VehicleCommandProtocolRequired { get; set; } public DateTime? RateLimitedUntil { get; set; } public bool UseBle { get; set; } + public int ApiRefreshIntervalSeconds { get; set; } public List ChargingProcesses { get; set; } = new List(); } diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index 546ff1052..e11bbbdb4 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -102,6 +102,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasIndex(h => new { h.RestValueConfigurationId, h.Key }) .IsUnique(); + + modelBuilder.Entity() + .Property(c => c.ApiRefreshIntervalSeconds) + .HasDefaultValue(500); } #pragma warning disable CS8618 diff --git a/TeslaSolarCharger.Model/Migrations/20240615153122_AddApiRefreshIntervalSecondsToCars.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240615153122_AddApiRefreshIntervalSecondsToCars.Designer.cs new file mode 100644 index 000000000..ca640aa8e --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240615153122_AddApiRefreshIntervalSecondsToCars.Designer.cs @@ -0,0 +1,731 @@ +// +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("20240615153122_AddApiRefreshIntervalSecondsToCars")] + partial class AddApiRefreshIntervalSecondsToCars + { + /// + 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("ApiRefreshIntervalSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(500); + + 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/20240615153122_AddApiRefreshIntervalSecondsToCars.cs b/TeslaSolarCharger.Model/Migrations/20240615153122_AddApiRefreshIntervalSecondsToCars.cs new file mode 100644 index 000000000..dbdcd3ae0 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240615153122_AddApiRefreshIntervalSecondsToCars.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddApiRefreshIntervalSecondsToCars : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApiRefreshIntervalSeconds", + table: "Cars", + type: "INTEGER", + nullable: false, + defaultValue: 500); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApiRefreshIntervalSeconds", + table: "Cars"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index fccc3024a..f98548602 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -47,6 +47,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ApiRefreshIntervalSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(500); + b.Property("ChargeMode") .HasColumnType("INTEGER"); diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index e929d8302..f0e19fd76 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -27,6 +27,8 @@ else + +
diff --git a/TeslaSolarCharger/Server/Services/ChargingService.cs b/TeslaSolarCharger/Server/Services/ChargingService.cs index 5df91384e..5827baaba 100644 --- a/TeslaSolarCharger/Server/Services/ChargingService.cs +++ b/TeslaSolarCharger/Server/Services/ChargingService.cs @@ -74,17 +74,6 @@ public async Task SetNewChargingValues() LogErrorForCarsWithUnknownSocLimit(_settings.CarsToManage); - //Set to maximum current so will charge on full speed on auto wakeup - foreach (var car in _settings.CarsToManage) - { - if (car is { IsHomeGeofence: true, State: CarStateEnum.Online } - && car.ChargerRequestedCurrent != car.MaximumAmpere - && car.ChargeMode != ChargeMode.DoNothing) - { - await _teslaService.SetAmp(car.Id, car.MaximumAmpere).ConfigureAwait(false); - } - } - var relevantCarIds = GetRelevantCarIds(); _logger.LogDebug("Relevant car ids: {@ids}", relevantCarIds); @@ -336,6 +325,7 @@ public List GetRelevantCarIds() private async Task ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue maxAmpIncrease) { _logger.LogTrace("{method}({param1}, {param2}, {param3})", nameof(ChangeCarAmp), dtoCar.Id, ampToChange, maxAmpIncrease.Value); + var actualCurrent = dtoCar.ChargerActualCurrent; if (maxAmpIncrease.Value < ampToChange) { _logger.LogDebug("Reduce current increase from {ampToChange}A to {maxAmpIncrease}A due to limited combined charging current.", @@ -343,16 +333,16 @@ private async Task ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue 0 && dtoCar.ChargerRequestedCurrent > dtoCar.ChargerActualCurrent && dtoCar.ChargerActualCurrent > 0) + if (ampToChange > 0 && dtoCar.ChargerRequestedCurrent > actualCurrent && actualCurrent > 0) { //ampToChange = 0; _logger.LogWarning("Car does not use full request."); } var finalAmpsToSet = (dtoCar.ChargerRequestedCurrent ?? 0) + ampToChange; - if (dtoCar.ChargerActualCurrent == 0) + if (actualCurrent == 0) { - finalAmpsToSet = (int)(dtoCar.ChargerActualCurrent + ampToChange); + finalAmpsToSet = (int)(actualCurrent + ampToChange); } _logger.LogDebug("Amps to set: {amps}", finalAmpsToSet); @@ -386,7 +376,7 @@ private async Task ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue maxAmpIncrease.Value ? ((dtoCar.ChargerActualCurrent ?? 0) + maxAmpIncrease.Value) : maxAmpPerCar; + var ampToSet = (maxAmpPerCar - dtoCar.ChargerRequestedCurrent) > maxAmpIncrease.Value ? ((actualCurrent ?? 0) + maxAmpIncrease.Value) : maxAmpPerCar; _logger.LogDebug("Set current to {ampToSet} after considering max car Current {maxAmpPerCar} and maxAmpIncrease {maxAmpIncrease}", ampToSet, maxAmpPerCar, maxAmpIncrease.Value); if (dtoCar.State != CarStateEnum.Charging) { @@ -399,12 +389,12 @@ private async Task ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue ChangeCarAmp(DtoCar dtoCar, int ampToChange, DtoValue c.Id == carId); settingsCar.Name = carBasicConfiguration.Name; diff --git a/TeslaSolarCharger/Server/Services/TeslaBleService.cs b/TeslaSolarCharger/Server/Services/TeslaBleService.cs index 60474ee4d..f80429459 100644 --- a/TeslaSolarCharger/Server/Services/TeslaBleService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaBleService.cs @@ -54,6 +54,15 @@ public async Task SetAmp(string vin, int amps) Parameters = new List { amps.ToString() }, }; var result = await SendCommandToBle(request).ConfigureAwait(false); + + var car = settings.Cars.First(c => c.Vin == vin); + // Double send if over or under 5 amps as Tesla does not change immedediatly + if (car.ChargerRequestedCurrent >= 5 && amps < 5 || car.ChargerRequestedCurrent < 5 && amps >= 5) + { + await Task.Delay(5000).ConfigureAwait(false); + result = await SendCommandToBle(request).ConfigureAwait(false); + } + return result; } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 95fdb092d..4459ca24f 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -13,6 +13,7 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Dtos.Ble; using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; @@ -102,6 +103,12 @@ public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) await SetAmp(carId, startAmp).ConfigureAwait(false); var result = await SendCommandToTeslaApi(vin, ChargeStartRequest, HttpMethod.Post).ConfigureAwait(false); + if (result?.Response?.Result == true) + { + var car = settings.Cars.First(c => c.Id == carId); + car.State = CarStateEnum.Charging; + car.ChargerActualCurrent = startAmp; + } } @@ -112,6 +119,8 @@ public async Task WakeUpCar(int carId) var result = await SendCommandToTeslaApi(vin, WakeUpRequest, HttpMethod.Post).ConfigureAwait(false); await teslamateApiService.ResumeLogging(carId).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(20)).ConfigureAwait(false); + var car = settings.Cars.First(c => c.Id == carId); + car.State = CarStateEnum.Online; } public async Task StopCharging(int carId) @@ -119,6 +128,12 @@ public async Task StopCharging(int carId) logger.LogTrace("{method}({carId})", nameof(StopCharging), carId); var vin = GetVinByCarId(carId); var result = await SendCommandToTeslaApi(vin, ChargeStopRequest, HttpMethod.Post).ConfigureAwait(false); + if (result?.Response?.Result == true) + { + var car = settings.Cars.First(c => c.Id == carId); + car.State = CarStateEnum.Online; + car.ChargerActualCurrent = 0; + } } public async Task SetAmp(int carId, int amps) @@ -134,6 +149,12 @@ public async Task SetAmp(int carId, int amps) var commandData = $"{{\"charging_amps\":{amps}}}"; var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData, amps).ConfigureAwait(false); car.LastSetAmp = amps; + if (result?.Response?.Result == true) + { + car.ChargerRequestedCurrent = amps; + car.ChargerActualCurrent = car.State == CarStateEnum.Charging ? amps : 0; + } + } public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) @@ -221,50 +242,60 @@ public async Task OpenChargePortDoor(int carId) public async Task RefreshCarData() { logger.LogTrace("{method}()", nameof(RefreshCarData)); - if ((!configurationWrapper.GetVehicleDataFromTesla()) && (!configurationWrapper.GetVehicleDataFromTeslaDebug())) + if ((!configurationWrapper.GetVehicleDataFromTesla())) { logger.LogDebug("Vehicle Data are coming from TeslaMate. Do not refresh car states via Fleet API"); return; } - logger.LogTrace("Actually refreshing car data"); var carIds = settings.CarsToManage.Select(c => c.Id).ToList(); foreach (var carId in carIds) { - var vin = GetVinByCarId(carId); + var car = settings.Cars.First(c => c.Id == carId); + var currentUtcDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if (car.LastApiDataRefresh.AddSeconds(car.ApiRefreshIntervalSeconds) > currentUtcDate) + { + logger.LogDebug("Do not refresh car data for car {carId} to prevent rate limits", car.Id); + continue; + } try { - var vehicle = await SendCommandToTeslaApi(vin, VehicleRequest, HttpMethod.Get).ConfigureAwait(false); - var vehicleResult = vehicle?.Response; - logger.LogTrace("Got vehicle {@vehicle}", vehicle); - if (vehicleResult == default) - { - await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), - $"Could not deserialize vehicle: {JsonConvert.SerializeObject(vehicle)}").ConfigureAwait(false); - logger.LogError("Could not deserialize vehicle for car {carId}: {@vehicle}", carId, vehicle); - continue; - } - var vehicleState = vehicleResult.State; - if (configurationWrapper.GetVehicleDataFromTesla()) - { - if (vehicleState == "asleep") - { - settings.Cars.First(c => c.Id == carId).State = CarStateEnum.Asleep; - } - else if (vehicleState == "offline") - { - settings.Cars.First(c => c.Id == carId).State = CarStateEnum.Offline; - } - } - - if (vehicleState is "asleep" or "offline") - { - logger.LogDebug("Do not call current vehicle data as car is {state}", vehicleState); - continue; - } - var vehicleData = await SendCommandToTeslaApi(vin, VehicleDataRequest, HttpMethod.Get) + //var vehicle = await SendCommandToTeslaApi(car.Vin, VehicleRequest, HttpMethod.Get).ConfigureAwait(false); + //var vehicleResult = vehicle?.Response; + //logger.LogTrace("Got vehicle {@vehicle}", vehicle); + //if (vehicleResult == default) + //{ + // await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), + // $"Could not deserialize vehicle: {JsonConvert.SerializeObject(vehicle)}").ConfigureAwait(false); + // logger.LogError("Could not deserialize vehicle for car {carId}: {@vehicle}", carId, vehicle); + // continue; + //} + //var vehicleState = vehicleResult.State; + //if (configurationWrapper.GetVehicleDataFromTesla()) + //{ + // if (vehicleState == "asleep") + // { + // car.State = CarStateEnum.Asleep; + // } + // else if (vehicleState == "offline") + // { + // car.State = CarStateEnum.Offline; + // } + //} + + //if (vehicleState is "asleep" or "offline") + //{ + // logger.LogDebug("Do not call current vehicle data as car is {state}", vehicleState); + // continue; + //} + var vehicleData = await SendCommandToTeslaApi(car.Vin, VehicleDataRequest, HttpMethod.Get) .ConfigureAwait(false); + car.LastApiDataRefresh = currentUtcDate; logger.LogTrace("Got vehicleData {@vehicleData}", vehicleData); var vehicleDataResult = vehicleData?.Response; + if (vehicleData?.Error?.Contains("offline") == true) + { + car.State = CarStateEnum.Offline; + } if (vehicleDataResult == default) { await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), @@ -275,7 +306,7 @@ await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameo if (configurationWrapper.GetVehicleDataFromTesla()) { - var car = settings.Cars.First(c => c.Id == carId); + car.Name = vehicleDataResult.VehicleState.VehicleName; car.SoC = vehicleDataResult.ChargeState.BatteryLevel; car.SocLimit = vehicleDataResult.ChargeState.ChargeLimitSoc; @@ -469,20 +500,49 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) var bleAddress = configurationWrapper.BleBaseUrl(); if (!string.IsNullOrEmpty(bleAddress)) { + var car = settings.Cars.First(c => c.Vin == vin); + var result = new DtoBleResult(); if (fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) { - await bleService.StartCharging(vin); + result = await bleService.StartCharging(vin); + if (result.Success) + { + car.State = CarStateEnum.Charging; + car.ChargerActualCurrent = car.ChargerRequestedCurrent; + } } else if (fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) { - await bleService.StopCharging(vin); + result = await bleService.StopCharging(vin); + if (result.Success) + { + car.State = CarStateEnum.Online; + car.ChargerActualCurrent = 0; + } } else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) { - await bleService.SetAmp(vin, amp!.Value); + result = await bleService.SetAmp(vin, amp!.Value); + if (result.Success) + { + car.ChargerRequestedCurrent = amp!.Value; + car.ChargerActualCurrent = car.State == CarStateEnum.Charging ? amp!.Value : 0; + } + } + + if (typeof(T) == typeof(DtoVehicleCommandResult)) + { + var comamndResult = new DtoGenericTeslaResponse() { }; + comamndResult.Response = (T)(object) new DtoVehicleCommandResult() + { + Result = result.Success, + Reason = result.Message, + }; + return comamndResult; } - return new DtoGenericTeslaResponse(){}; + return new DtoGenericTeslaResponse(); + } } } @@ -509,11 +569,6 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) }; var response = await httpClient.SendAsync(request).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (configurationWrapper.GetVehicleDataFromTeslaDebug()) - { - await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), - $"Logged Response string: {responseString}").ConfigureAwait(false); - } logger.LogTrace("Response status code: {statusCode}", response.StatusCode); logger.LogTrace("Response string: {responseString}", responseString); logger.LogTrace("Response headers: {@headers}", response.Headers); diff --git a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj index 7b981f96b..5f5ae17cb 100644 --- a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj +++ b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj @@ -61,7 +61,7 @@ - + diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 194fa9555..2850f10bb 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -58,7 +58,7 @@ "TeslaMateDbPassword": "secret", "TeslaMateApiBaseUrl": "http://teslamateapi:8080", "GeoFence": "Home", - "DisplayApiRequestCounter": false, + "DisplayApiRequestCounter": true, "IgnoreSslErrors": false, "UseFleetApi": true, "FleetApiClientId": "f29f71d6285a-4873-8b6b-80f15854892e", @@ -66,7 +66,7 @@ "TeslaFleetApiBaseUrl": "https://www.teslasolarcharger.de/teslaproxy/", "UseFleetApiProxy": false, "LogLocationData": false, - "GetVehicleDataFromTesla": false, + "GetVehicleDataFromTesla": true, "GetVehicleDataFromTeslaDebug": false, "AwattarBaseUrl": "https://api.awattar.de/v1/marketdata", "BleBaseUrl": null, diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 1323491d3..772ce813b 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -97,7 +97,6 @@ public interface IConfigurationWrapper string AutoBackupsZipDirectory(); bool LogLocationData(); bool GetVehicleDataFromTesla(); - bool GetVehicleDataFromTeslaDebug(); int? MaxInverterAcPower(); string? BleBaseUrl(); } diff --git a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs index d25125c28..e7f60b29a 100644 --- a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs +++ b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs @@ -40,6 +40,11 @@ public CarBasicConfiguration(int id, string? name) 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; } + [HelperText("Limits requests to car as getting values is rate limited.")] + [Postfix("s")] + [Range(11, int.MaxValue)] + public int ApiRefreshIntervalSeconds { get; set; } + + - } diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs index 174dcdb8e..874971028 100644 --- a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs +++ b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs @@ -86,5 +86,7 @@ private int? ChargingPower public CarStateEnum? State { get; set; } public bool? Healthy { get; set; } public bool ReducedChargeSpeedWarning { get; set; } + public DateTimeOffset LastApiDataRefresh { get; set; } + public int ApiRefreshIntervalSeconds { get; set; } public List PlannedChargingSlots { get; set; } = new List(); }