diff --git a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj index a11f03de0..11c8d1b6b 100644 --- a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj +++ b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj @@ -11,7 +11,7 @@ - + diff --git a/README.md b/README.md index 4c1b28930..8ddad226f 100644 --- a/README.md +++ b/README.md @@ -1213,9 +1213,12 @@ If you get an error like `Error: No such container:` you can look up the contain 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: +## Privacy notes +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 (4th July 2024), 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. - Error and warning logs +- Your VIN and if using the Fleet API the data for each request (e.g. change-charging-amp to 7A) +- A statistic of your Fleet API and BLE API usage (e.g. changed car amps 58 times including Timestamps of the request) +- Your configuration regarding using BLE API, the configured Fleet API Refresh Interval, if getting Data from TeslaMate is enabled \ No newline at end of file diff --git a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs index b97667725..d55c3d00a 100644 --- a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs @@ -27,5 +27,6 @@ public interface ITeslaSolarChargerContext DbSet ModbusResultConfigurations { get; set; } DbSet MqttConfigurations { get; set; } DbSet MqttResultConfigurations { get; set; } + DbSet BackendNotifications { get; set; } void RejectChanges(); } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendNotification.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendNotification.cs new file mode 100644 index 000000000..73603d837 --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendNotification.cs @@ -0,0 +1,17 @@ +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class BackendNotification +{ + public int Id { get; set; } + public int BackendIssueId { get; set; } + public BackendNotificationType Type { get; set; } + public string Headline { get; set; } + public string DetailText { get; set; } + public DateTime? ValidFromDate { get; set; } + public DateTime? ValidToDate { get; set; } + public string? ValidFromVersion { get; set; } + public string? ValidToVersion { get; set; } + public bool IsConfirmed { get; set; } +} diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs index 527d0a3b8..9555ce231 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs @@ -44,6 +44,7 @@ public class Car public DateTime? RateLimitedUntil { get; set; } public bool UseBle { get; set; } public int ApiRefreshIntervalSeconds { get; set; } + public string? BleApiBaseUrl { get; set; } public List ChargingProcesses { get; set; } = new List(); } diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index e11bbbdb4..4149fe90b 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -26,6 +26,7 @@ public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext public DbSet ModbusResultConfigurations { get; set; } = null!; public DbSet MqttConfigurations { get; set; } = null!; public DbSet MqttResultConfigurations { get; set; } = null!; + public DbSet BackendNotifications { get; set; } = null!; // ReSharper disable once UnassignedGetOnlyAutoProperty public string DbPath { get; } diff --git a/TeslaSolarCharger.Model/Migrations/20240704163234_AddBleBaseUrlToCars.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240704163234_AddBleBaseUrlToCars.Designer.cs new file mode 100644 index 000000000..df84c5f1c --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240704163234_AddBleBaseUrlToCars.Designer.cs @@ -0,0 +1,734 @@ +// +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("20240704163234_AddBleBaseUrlToCars")] + partial class AddBleBaseUrlToCars + { + /// + 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("BleApiBaseUrl") + .HasColumnType("TEXT"); + + 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/20240704163234_AddBleBaseUrlToCars.cs b/TeslaSolarCharger.Model/Migrations/20240704163234_AddBleBaseUrlToCars.cs new file mode 100644 index 000000000..33d328883 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240704163234_AddBleBaseUrlToCars.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddBleBaseUrlToCars : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BleApiBaseUrl", + table: "Cars", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BleApiBaseUrl", + table: "Cars"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20240707154316_AddBackendNotifications.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240707154316_AddBackendNotifications.Designer.cs new file mode 100644 index 000000000..40e5a89d7 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240707154316_AddBackendNotifications.Designer.cs @@ -0,0 +1,774 @@ +// +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("20240707154316_AddBackendNotifications")] + partial class AddBackendNotifications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BackendIssueId") + .HasColumnType("INTEGER"); + + b.Property("DetailText") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("ValidFromDate") + .HasColumnType("TEXT"); + + b.Property("ValidFromVersion") + .HasColumnType("TEXT"); + + b.Property("ValidToDate") + .HasColumnType("TEXT"); + + b.Property("ValidToVersion") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendNotifications"); + }); + + 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("BleApiBaseUrl") + .HasColumnType("TEXT"); + + 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/20240707154316_AddBackendNotifications.cs b/TeslaSolarCharger.Model/Migrations/20240707154316_AddBackendNotifications.cs new file mode 100644 index 000000000..f129bdcaa --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240707154316_AddBackendNotifications.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class AddBackendNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BackendNotifications", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BackendIssueId = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + Headline = table.Column(type: "TEXT", nullable: false), + DetailText = table.Column(type: "TEXT", nullable: false), + ValidFromDate = table.Column(type: "TEXT", nullable: true), + ValidToDate = table.Column(type: "TEXT", nullable: true), + ValidFromVersion = table.Column(type: "TEXT", nullable: true), + ValidToVersion = table.Column(type: "TEXT", nullable: true), + IsConfirmed = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BackendNotifications", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BackendNotifications"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index f98548602..322091422 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -17,6 +17,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BackendIssueId") + .HasColumnType("INTEGER"); + + b.Property("DetailText") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("ValidFromDate") + .HasColumnType("TEXT"); + + b.Property("ValidFromVersion") + .HasColumnType("TEXT"); + + b.Property("ValidToDate") + .HasColumnType("TEXT"); + + b.Property("ValidToVersion") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendNotifications"); + }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => { b.Property("Id") @@ -52,6 +92,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasDefaultValue(500); + b.Property("BleApiBaseUrl") + .HasColumnType("TEXT"); + b.Property("ChargeMode") .HasColumnType("INTEGER"); diff --git a/TeslaSolarCharger.Services/Services/Modbus/ModbusClientHandlingService.cs b/TeslaSolarCharger.Services/Services/Modbus/ModbusClientHandlingService.cs index 51c686b53..014a96425 100644 --- a/TeslaSolarCharger.Services/Services/Modbus/ModbusClientHandlingService.cs +++ b/TeslaSolarCharger.Services/Services/Modbus/ModbusClientHandlingService.cs @@ -69,13 +69,15 @@ private async Task GetConnectedModbusTcpClient(string host, in var key = CreateModbusTcpClientKey(ipAddress.ToString(), port); if(_modbusClients.TryGetValue(key, out var modbusClient)) { + logger.LogTrace("Found Modbus client, check if connected."); if (!modbusClient.IsConnected) { + logger.LogTrace("Modbus client not connected, try to connect."); await ConnectModbusClient(modbusClient, ipAddress, port, endianess, connectDelay, connectTimeout); } return modbusClient; } - + logger.LogTrace("Did not find Modbus client, create new one."); var client = serviceProvider.GetRequiredService(); _modbusClients.Add(key, client); await ConnectModbusClient(client, ipAddress, port, endianess, connectDelay, connectTimeout); diff --git a/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor b/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor new file mode 100644 index 000000000..ff22e0b60 --- /dev/null +++ b/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor @@ -0,0 +1,63 @@ +@using TeslaSolarCharger.Shared.Dtos +@using TeslaSolarCharger.Shared.Dtos +@using TeslaSolarCharger.Shared.Enums + +@inject HttpClient HttpClient + + +@if (_backendNotifications.Any()) +{ +

Developer's information

+} +@foreach(var notification in _backendNotifications) +{ +
+ +

@notification.Headline

+ @((MarkupString)notification.DetailText) +
+
+} + + + +@code { + private List _backendNotifications = new List(); + + protected override async Task OnInitializedAsync() + { + await ReloadNotifications(); + } + + private async Task ReloadNotifications() + { + var notifications = await HttpClient.GetFromJsonAsync>("api/BackendNotification/GetRelevantBackendNotifications"); + if(notifications == null) + { + _backendNotifications = new List(); + return; + } + _backendNotifications = notifications; + } + + private Severity GetSeverity(BackendNotificationType notificationType) + { + return notificationType switch + { + BackendNotificationType.Warning => Severity.Error, + BackendNotificationType.Error => Severity.Error, + _ => Severity.Info + }; + } + + private async Task ConfirmNotifiaction(int notificationId) + { + _backendNotifications.RemoveAll(n => n.Id == notificationId); + await HttpClient.PostAsync($"api/BackendNotification/MarkBackendNotificationAsConfirmed?id={notificationId}", null); + } + +} \ No newline at end of file diff --git a/TeslaSolarCharger/Client/Components/BackendIssueValidation.razor b/TeslaSolarCharger/Client/Components/BackendIssueValidation.razor index d94ca236a..ee4ec173a 100644 --- a/TeslaSolarCharger/Client/Components/BackendIssueValidation.razor +++ b/TeslaSolarCharger/Client/Components/BackendIssueValidation.razor @@ -57,7 +57,7 @@ private async Task UpdateIssues() { var timeZoneOffset = TimeZoneInfo.Local.GetUtcOffset(DateTimeProvider.Now()); - _issues = await HttpClient.GetFromJsonAsync>($"api/Issue/RefreshIssues?utcTimeZoneOffset={timeZoneOffset}").ConfigureAwait(false); + _issues = await HttpClient.GetFromJsonAsync>($"api/Issue/RefreshIssues?utcTimeZoneOffset={timeZoneOffset}"); this.StateHasChanged(); } diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index e57d9f27d..bd8e612d5 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -75,15 +75,6 @@ else
- - - - - -
}

TeslaMate:

diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index f0e19fd76..5eeb1137d 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -30,6 +30,7 @@ else +

BLE Pairing and test

diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index 5f2b6673c..94ae72270 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -36,6 +36,7 @@ } } + @if (_pvValues != null) @@ -113,12 +114,6 @@ @_pvValues.CarCombinedChargingPowerAtHome W
- @if (_shouldDisplayApiRequestCounter == true) - { -
- Tesla API requests since last startup: @_apiRequestCount -
- } } @@ -461,8 +456,6 @@ else private HashSet _collapsedCarDetails = new HashSet(); private DateTime? _serverTime; private string? _serverTimeZoneDisplayName; - private bool? _shouldDisplayApiRequestCounter; - private int? _apiRequestCount; private string _installationId = ""; private bool? _usingFleetApi; private Dictionary _isFleetApiWorkingForCar = new(); @@ -478,8 +471,6 @@ else await RefreshPvValues().ConfigureAwait(false); await RefreshServerTime().ConfigureAwait(false); await RefreshServerTimeZone().ConfigureAwait(false); - await CheckIfApiRequestCounterShouldBeDisplayed().ConfigureAwait(false); - await RefreshApiRequestCount().ConfigureAwait(false); var dtoSolarChargerInstallation = await HttpClient.GetFromJsonAsync>("api/Hello/IsSolarEdgeInstallation").ConfigureAwait(false); _isSolarEdgeInstallation = dtoSolarChargerInstallation?.Value; var usingFleetApi = await HttpClient.GetFromJsonAsync>("api/FleetApi/IsFleetApiEnabled").ConfigureAwait(false); @@ -497,11 +488,6 @@ else _timer.Start(); } - private async Task CheckIfApiRequestCounterShouldBeDisplayed() - { - _shouldDisplayApiRequestCounter = (await HttpClient.GetFromJsonAsync>("api/Hello/ShouldDisplayApiRequestCounter").ConfigureAwait(false))?.Value; - } - private async Task RefreshCarBaseStates() { _carBaseStates = await HttpClient.GetFromJsonAsync>("api/Index/GetCarBaseStatesOfEnabledCars").ConfigureAwait(false); @@ -609,11 +595,6 @@ else _serverTime = dtoTimeValue; } - private async Task RefreshApiRequestCount() - { - _apiRequestCount = (await HttpClient.GetFromJsonAsync>("api/Hello/TeslaApiRequestsSinceStartup").ConfigureAwait(false))?.Value; - } - private async Task RefreshServerTimeZone() { var dtoServerTimeZoneId = await HttpClient.GetFromJsonAsync>("api/Hello/GetServerTimeZoneDisplayName").ConfigureAwait(false); @@ -675,10 +656,6 @@ else await RefreshCarBaseStates().ConfigureAwait(false); await RefreshAllVisableCarDetails().ConfigureAwait(false); await RefreshServerTime().ConfigureAwait(false); - if (_shouldDisplayApiRequestCounter == true) - { - await RefreshApiRequestCount().ConfigureAwait(false); - } _couldNotRefreshStates = false; } diff --git a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj index cf5006194..621e5a984 100644 --- a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj +++ b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj @@ -44,4 +44,10 @@
+ + + true + + + diff --git a/TeslaSolarCharger/Server/Contracts/IChargingCostService.cs b/TeslaSolarCharger/Server/Contracts/IChargingCostService.cs index 06460f5a2..8bfffcb01 100644 --- a/TeslaSolarCharger/Server/Contracts/IChargingCostService.cs +++ b/TeslaSolarCharger/Server/Contracts/IChargingCostService.cs @@ -13,4 +13,6 @@ public interface IChargingCostService Task> GetSpotPrices(); Task ConvertToNewChargingProcessStructure(); Task AddFirstChargePrice(); + Task FixConvertedChargingDetailSolarPower(); + Task UpdateChargingProcessesAfterChargingDetailsFix(); } diff --git a/TeslaSolarCharger/Server/Contracts/IConfigJsonService.cs b/TeslaSolarCharger/Server/Contracts/IConfigJsonService.cs index c3677bbd9..129538d81 100644 --- a/TeslaSolarCharger/Server/Contracts/IConfigJsonService.cs +++ b/TeslaSolarCharger/Server/Contracts/IConfigJsonService.cs @@ -18,4 +18,5 @@ public interface IConfigJsonService Task> GetCarBasicConfigurations(); ISettings GetSettings(); Task AddCarsToSettings(); + Task AddBleBaseUrlToAllCars(); } diff --git a/TeslaSolarCharger/Server/Contracts/ICoreService.cs b/TeslaSolarCharger/Server/Contracts/ICoreService.cs index d88c6db74..406816f0e 100644 --- a/TeslaSolarCharger/Server/Contracts/ICoreService.cs +++ b/TeslaSolarCharger/Server/Contracts/ICoreService.cs @@ -16,8 +16,6 @@ public interface ICoreService Task KillAllServices(); Task StopJobs(); Task DisconnectMqttServices(); - DtoValue TeslaApiRequestsSinceStartup(); - DtoValue ShouldDisplayApiRequestCounter(); Task> GetPriceData(DateTimeOffset from, DateTimeOffset to); Task GetInstallationId(); Dictionary GetRawRestRequestResults(); diff --git a/TeslaSolarCharger/Server/Controllers/BackendNotificationController.cs b/TeslaSolarCharger/Server/Controllers/BackendNotificationController.cs new file mode 100644 index 000000000..eea089eb1 --- /dev/null +++ b/TeslaSolarCharger/Server/Controllers/BackendNotificationController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.SharedBackend.Abstracts; + +namespace TeslaSolarCharger.Server.Controllers; + +public class BackendNotificationController(IBackendNotificationService service) : ApiBaseController +{ + [HttpGet] + public Task> GetRelevantBackendNotifications() + { + return service.GetRelevantBackendNotifications(); + } + + [HttpPost] + public Task MarkBackendNotificationAsConfirmed(int id) + { + return service.MarkBackendNotificationAsConfirmed(id); + } +} diff --git a/TeslaSolarCharger/Server/Controllers/HelloController.cs b/TeslaSolarCharger/Server/Controllers/HelloController.cs index 51712fa3b..dd73d5c57 100644 --- a/TeslaSolarCharger/Server/Controllers/HelloController.cs +++ b/TeslaSolarCharger/Server/Controllers/HelloController.cs @@ -48,12 +48,6 @@ public HelloController(ICoreService coreService, ITscConfigurationService tscCon return _coreService.GetCurrentVersion(); } - [HttpGet] - public Task> TeslaApiRequestsSinceStartup() => Task.FromResult(_coreService.TeslaApiRequestsSinceStartup()); - - [HttpGet] - public Task> ShouldDisplayApiRequestCounter() => Task.FromResult(_coreService.ShouldDisplayApiRequestCounter()); - [HttpGet] public Task> GetPriceData(DateTimeOffset from, DateTimeOffset to) => _coreService.GetPriceData(from, to); diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaApiCallStatistic.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaApiCallStatistic.cs new file mode 100644 index 000000000..6b7f857d6 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaApiCallStatistic.cs @@ -0,0 +1,19 @@ +namespace TeslaSolarCharger.Server.Dtos.TscBackend; + +public class DtoTeslaApiCallStatistic +{ + public DateOnly Date { get; set; } + public Guid InstallationId { get; set; } + public bool GetDataFromTesla { get; set; } + public DateTime StartupTime { get; set; } + public string Vin { get; set; } + public bool UseBle { get; set; } + public int ApiRefreshInterval { get; set; } + public List WakeUpCalls { get; set; } = new List(); + public List VehicleDataCalls { get; set; } = new List(); + public List VehicleCalls { get; set; } = new List(); + public List ChargeStartCalls { get; set; } = new List(); + public List ChargeStopCalls { get; set; } = new List(); + public List SetChargingAmpsCall { get; set; } = new List(); + public List OtherCommandCalls { get; set; } = new List(); +} diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 78dc042a9..46ba0b091 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -165,9 +165,12 @@ async Task DoStartupStuff(WebApplication webApplication, ILogger logger var configJsonService = webApplication.Services.GetRequiredService(); await configJsonService.ConvertOldCarsToNewCar().ConfigureAwait(false); + await configJsonService.AddBleBaseUrlToAllCars().ConfigureAwait(false); //This needs to be done after converting old cars to new cars as IDs might change await chargingCostService.ConvertToNewChargingProcessStructure().ConfigureAwait(false); + await chargingCostService.FixConvertedChargingDetailSolarPower().ConfigureAwait(false); await chargingCostService.AddFirstChargePrice().ConfigureAwait(false); + await chargingCostService.UpdateChargingProcessesAfterChargingDetailsFix().ConfigureAwait(false); await configJsonService.UpdateAverageGridVoltage().ConfigureAwait(false); var carConfigurationService = webApplication.Services.GetRequiredService(); @@ -202,5 +205,7 @@ await backendApiService.PostErrorInformation(nameof(Program), "Startup", { var settings = webApplication.Services.GetRequiredService(); settings.IsStartupCompleted = true; + var dateTimeProvider = webApplication.Services.GetRequiredService(); + settings.StartupTime = dateTimeProvider.UtcNow(); } } diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index e46ffdf4d..c399912dc 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -61,6 +61,7 @@ public async Task StartJobs() var fleetApiTokenRefreshJob = JobBuilder.Create().Build(); var vehicleDataRefreshJob = JobBuilder.Create().Build(); var teslaMateChargeCostUpdateJob = JobBuilder.Create().Build(); + var apiCallCounterResetJob = JobBuilder.Create().Build(); var currentDate = _dateTimeProvider.DateTimeOffSetNow(); var chargingTriggerStartTime = currentDate.AddSeconds(5); @@ -109,6 +110,20 @@ public async Task StartJobs() var teslaMateChargeCostUpdateTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatHourlyForever(24)).Build(); + var random = new Random(); + var hour = random.Next(0, 5); + var minute = random.Next(0, 59); + + var triggerAtNight = TriggerBuilder.Create() + .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(hour, minute).InTimeZone(TimeZoneInfo.Utc))// Run every day at 0:00 UTC + .StartNow() + .Build(); + + var triggerNow = TriggerBuilder + .Create() + .StartAt(DateTimeOffset.Now.AddSeconds(15)) + .Build(); + var triggersAndJobs = new Dictionary> { {chargingValueJob, new HashSet { chargingValueTrigger }}, @@ -122,6 +137,7 @@ public async Task StartJobs() {fleetApiTokenRefreshJob, new HashSet {fleetApiTokenRefreshTrigger}}, {vehicleDataRefreshJob, new HashSet {vehicleDataRefreshTrigger}}, {teslaMateChargeCostUpdateJob, new HashSet {teslaMateChargeCostUpdateTrigger}}, + {apiCallCounterResetJob, new HashSet {triggerAtNight, triggerNow}}, }; await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs new file mode 100644 index 000000000..90670eec4 --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs @@ -0,0 +1,15 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +public class ApiCallCounterResetJob(ILogger logger, ITeslaFleetApiService service, IBackendApiService backendApiService) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + await backendApiService.PostTeslaApiCallStatistics().ConfigureAwait(false); + service.ResetApiRequestCounters(); + await backendApiService.GetNewBackendNotifications().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index d00b4e755..569a8a6a7 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -50,6 +50,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() @@ -104,6 +105,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs index 9b5c5ba7e..f5244d53d 100644 --- a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs +++ b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs @@ -55,8 +55,7 @@ public DtoPvValues GetPvValues() { _logger.LogTrace("{method}()", nameof(GetPvValues)); int? powerBuffer = _configurationWrapper.PowerBuffer(true); - if (_configurationWrapper.FrontendConfiguration()?.InverterValueSource == SolarValueSource.None - && _configurationWrapper.FrontendConfiguration()?.GridValueSource == SolarValueSource.None) + if (_settings.InverterPower == null && _settings.Overage == null) { powerBuffer = null; } @@ -259,7 +258,18 @@ public DtoCarTopicValues GetCarDetails(int carId) { continue; } - if (property.PropertyType == typeof(DateTimeOffset?) + + if (property.PropertyType == typeof(List)) + { + var list = (List?) property.GetValue(carState, null); + var currentDate = _dateTimeProvider.UtcNow().Date; + dtoCarTopicValues.NonDateValues.Add(new DtoCarTopicValue() + { + Topic = AddSpacesBeforeCapitalLetters(property.Name), + Value = list?.Where(d => d > currentDate).Count().ToString(), + }); + } + else if (property.PropertyType == typeof(DateTimeOffset?) || property.PropertyType == typeof(DateTimeOffset)) { dtoCarTopicValues.DateValues.Add(new DtoCarDateTopics() @@ -286,6 +296,7 @@ public DtoCarTopicValues GetCarDetails(int carId) }); } } + return dtoCarTopicValues; } diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index f0da01379..a74a44bd0 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -8,72 +8,62 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Resources.Contracts; using TeslaSolarCharger.SharedBackend.Contracts; namespace TeslaSolarCharger.Server.Services; -public class BackendApiService : IBackendApiService +public class BackendApiService( + ILogger logger, + ITscConfigurationService tscConfigurationService, + IConfigurationWrapper configurationWrapper, + ITeslaSolarChargerContext teslaSolarChargerContext, + IConstants constants, + IDateTimeProvider dateTimeProvider, + ISettings settings) + : 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, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, - IDateTimeProvider dateTimeProvider) - { - _logger = logger; - _tscConfigurationService = tscConfigurationService; - _configurationWrapper = configurationWrapper; - _teslaSolarChargerContext = teslaSolarChargerContext; - _constants = constants; - _dateTimeProvider = dateTimeProvider; - } - public async Task> StartTeslaOAuth(string locale, string baseUrl) { - _logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); - var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); - _teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); - var cconfigEntriesToRemove = await _teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == _constants.TokenMissingScopes) + logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); + var currentTokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); + teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); + var cconfigEntriesToRemove = await teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == constants.TokenMissingScopes) .ToListAsync().ConfigureAwait(false); - _teslaSolarChargerContext.TscConfigurations.RemoveRange(cconfigEntriesToRemove); - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var backendApiBaseUrl = _configurationWrapper.BackendApiBaseUrl(); + teslaSolarChargerContext.TscConfigurations.RemoveRange(cconfigEntriesToRemove); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var backendApiBaseUrl = configurationWrapper.BackendApiBaseUrl(); using var httpClient = new HttpClient(); var requestUri = $"{backendApiBaseUrl}Tsc/StartTeslaOAuth?installationId={Uri.EscapeDataString(installationId.ToString())}&baseUrl={Uri.EscapeDataString(baseUrl)}"; 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) + 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"), + Key = constants.FleetApiTokenRequested, + Value = dateTimeProvider.UtcNow().ToString("O"), }; - _teslaSolarChargerContext.TscConfigurations.Add(config); + teslaSolarChargerContext.TscConfigurations.Add(config); } else { - tokenRequested.Value = _dateTimeProvider.UtcNow().ToString("O"); + tokenRequested.Value = dateTimeProvider.UtcNow().ToString("O"); } - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); return new DtoValue(requestUrl); } internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation, string locale) { - _logger.LogTrace("{method}({@oAuthInformation})", nameof(GenerateAuthUrl), oAuthInformation); + 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; @@ -83,8 +73,8 @@ public async Task PostInstallationInformation(string reason) { try { - var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; - var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var url = configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); var currentVersion = await GetCurrentVersion().ConfigureAwait(false); var installationInformation = new DtoInstallationInformation { @@ -98,7 +88,7 @@ public async Task PostInstallationInformation(string reason) } catch (Exception e) { - _logger.LogError(e, "Could not post installation information"); + logger.LogError(e, "Could not post installation information"); } } @@ -107,8 +97,8 @@ public async Task PostErrorInformation(string source, string methodName, string { try { - var url = _configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyError"; - var installationId = await _tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var url = configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyError"; + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); var currentVersion = await GetCurrentVersion().ConfigureAwait(false); var errorInformation = new DtoErrorInformation() { @@ -125,16 +115,103 @@ public async Task PostErrorInformation(string source, string methodName, string } catch (Exception e) { - _logger.LogError(e, "Could not post error information"); + logger.LogError(e, "Could not post error information"); } } public Task GetCurrentVersion() { - _logger.LogTrace("{method}()", nameof(GetCurrentVersion)); + logger.LogTrace("{method}()", nameof(GetCurrentVersion)); var assembly = Assembly.GetExecutingAssembly(); var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); return Task.FromResult(fileVersionInfo.ProductVersion); } + + public async Task PostTeslaApiCallStatistics() + { + logger.LogTrace("{method}()", nameof(PostTeslaApiCallStatistics)); + var shouldTransferDate = configurationWrapper.SendTeslaApiStatsToBackend(); + var currentDate = dateTimeProvider.UtcNow().Date; + if (!shouldTransferDate) + { + logger.LogWarning("You manually disabled tesla API stats transfer to the backend. This means your usage won't be considered in future optimizations."); + return; + } + + Func predicate = d => d > (currentDate.AddDays(-1)) && (d < currentDate); + var cars = settings.Cars.Where(c => c.WakeUpCalls.Count(predicate) > 0 + || c.VehicleDataCalls.Count(predicate) > 0 + || c.VehicleCalls.Count(predicate) > 0 + || c.ChargeStartCalls.Count(predicate) > 0 + || c.ChargeStopCalls.Count(predicate) > 0 + || c.SetChargingAmpsCall.Count(predicate) > 0 + || c.OtherCommandCalls.Count(predicate) > 0).ToList(); + + var getVehicleDataFromTesla = configurationWrapper.GetVehicleDataFromTesla(); + foreach (var car in cars) + { + var statistics = new DtoTeslaApiCallStatistic + { + Date = DateOnly.FromDateTime(currentDate.AddDays(-1)), + InstallationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false), + StartupTime = settings.StartupTime, + GetDataFromTesla = getVehicleDataFromTesla, + ApiRefreshInterval = car.ApiRefreshIntervalSeconds, + UseBle = car.UseBle, + Vin = car.Vin, + WakeUpCalls = car.WakeUpCalls.Where(predicate).ToList(), + VehicleDataCalls = car.VehicleDataCalls.Where(predicate).ToList(), + VehicleCalls = car.VehicleCalls.Where(predicate).ToList(), + ChargeStartCalls = car.ChargeStartCalls.Where(predicate).ToList(), + ChargeStopCalls = car.ChargeStopCalls.Where(predicate).ToList(), + SetChargingAmpsCall = car.SetChargingAmpsCall.Where(predicate).ToList(), + OtherCommandCalls = car.OtherCommandCalls.Where(predicate).ToList(), + }; + var url = configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyTeslaApiCallStatistics"; + try + { + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, statistics).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogError(e, "Could not post tesla api call statistics"); + } + + } + + } + + public async Task GetNewBackendNotifications() + { + logger.LogTrace("{method}()", nameof(GetNewBackendNotifications)); + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var lastKnownNotificationId = await teslaSolarChargerContext.BackendNotifications + .OrderByDescending(n => n.BackendIssueId) + .Select(n => n.BackendIssueId) + .FirstOrDefaultAsync().ConfigureAwait(false); + var url = configurationWrapper.BackendApiBaseUrl() + $"Tsc/GetBackendNotifications?installationId={installationId}&lastKnownNotificationId={lastKnownNotificationId}"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.GetStringAsync(url).ConfigureAwait(false); + var notifications = JsonConvert.DeserializeObject>(response) ?? throw new InvalidDataException("Could not parse notifications"); + + foreach (var dtoBackendNotification in notifications) + { + teslaSolarChargerContext.BackendNotifications.Add(new BackendNotification + { + BackendIssueId = dtoBackendNotification.Id, + Type = dtoBackendNotification.Type, + Headline = dtoBackendNotification.Headline, + DetailText = dtoBackendNotification.DetailText, + ValidFromDate = dtoBackendNotification.ValidFromDate, + ValidToDate = dtoBackendNotification.ValidToDate, + ValidFromVersion = dtoBackendNotification.ValidFromVersion, + ValidToVersion = dtoBackendNotification.ValidToVersion, + }); + } + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } } diff --git a/TeslaSolarCharger/Server/Services/BackendNotificationService.cs b/TeslaSolarCharger/Server/Services/BackendNotificationService.cs new file mode 100644 index 000000000..515746a06 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/BackendNotificationService.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Server.Services; + +public class BackendNotificationService (ILogger logger, + ITeslaSolarChargerContext context, + IDateTimeProvider dateTimeProvider, + IBackendApiService backendApiService) : IBackendNotificationService +{ + public async Task> GetRelevantBackendNotifications() + { + logger.LogTrace("{method}()", nameof(GetRelevantBackendNotifications)); + var currentDate = dateTimeProvider.UtcNow(); + var versionString = await backendApiService.GetCurrentVersion(); + Version? version = null; + if (Version.TryParse(versionString, out var parsedVersion)) + { + version = parsedVersion; + } + var notAcknoledgedDbNotifications = await context.BackendNotifications + .Where(n => !n.IsConfirmed) + .AsNoTracking() + .ToListAsync().ConfigureAwait(false); + var backendNotifications = new List(); + foreach (var dbNotification in notAcknoledgedDbNotifications) + { + if (dbNotification.ValidFromDate > currentDate) + { + continue; + } + if (dbNotification.ValidToDate < currentDate) + { + continue; + } + Version? notificationFromVersion = null; + if (Version.TryParse(versionString, out var parsedFromVersion)) + { + notificationFromVersion = parsedFromVersion; + } + if (notificationFromVersion > version) + { + continue; + } + Version? notificationToVersion = null; + if (Version.TryParse(versionString, out var parsedToVersion)) + { + notificationToVersion = parsedToVersion; + } + if (notificationToVersion < version) + { + continue; + } + backendNotifications.Add(new DtoBackendNotification + { + Id = dbNotification.Id, + Type = dbNotification.Type, + Headline = dbNotification.Headline, + DetailText = dbNotification.DetailText, + ValidFromDate = dbNotification.ValidFromDate, + ValidToDate = dbNotification.ValidToDate, + ValidFromVersion = dbNotification.ValidFromVersion, + ValidToVersion = dbNotification.ValidToVersion, + }); + } + + return backendNotifications; + } + + public async Task MarkBackendNotificationAsConfirmed(int id) + { + logger.LogTrace("{method}({id})", nameof(MarkBackendNotificationAsConfirmed), id); + var notification = await context.BackendNotifications.FindAsync(id).ConfigureAwait(false); + if (notification == null) + { + throw new ArgumentException("Notification not found."); + } + notification.IsConfirmed = true; + await context.SaveChangesAsync().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs index 1ea70dddc..6a907cebd 100644 --- a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs @@ -28,6 +28,7 @@ public async Task UpdateBaseConfigurationAsync(DtoBaseConfiguration baseConfigur { logger.LogTrace("{method}({@baseConfiguration})", nameof(UpdateBaseConfigurationAsync), baseConfiguration); var restartNeeded = await jobManager.StopJobs().ConfigureAwait(false); + await teslaMateMqttService.DisconnectClient("configuration change").ConfigureAwait(false); await configurationWrapper.UpdateBaseConfigurationAsync(baseConfiguration).ConfigureAwait(false); if (!configurationWrapper.GetVehicleDataFromTesla()) { diff --git a/TeslaSolarCharger/Server/Services/ChargingCostService.cs b/TeslaSolarCharger/Server/Services/ChargingCostService.cs index f26c2f393..1545efd6a 100644 --- a/TeslaSolarCharger/Server/Services/ChargingCostService.cs +++ b/TeslaSolarCharger/Server/Services/ChargingCostService.cs @@ -152,7 +152,7 @@ public async Task AddFirstChargePrice() logger.LogTrace("{method}()", nameof(AddFirstChargePrice)); var chargePrices = await teslaSolarChargerContext.ChargePrices .ToListAsync().ConfigureAwait(false); - if (chargePrices.Any(c => c.ValidSince < new DateTime(2022, 2, 1))) + if (IsFirstChargePriceSet(chargePrices)) { return; } @@ -169,6 +169,62 @@ public async Task AddFirstChargePrice() await UpdateChargePrice(chargePrice).ConfigureAwait(false); } + private static bool IsFirstChargePriceSet(List chargePrices) + { + return chargePrices.Any(c => c.ValidSince < new DateTime(2022, 2, 1)); + } + + public async Task FixConvertedChargingDetailSolarPower() + { + logger.LogTrace("{method}()", nameof(FixConvertedChargingDetailSolarPower)); + var chargingProcessesConverted = + await teslaSolarChargerContext.TscConfigurations.AnyAsync(c => c.Key == constants.ChargingDetailsSolarPowerShareFixed).ConfigureAwait(false); + if (chargingProcessesConverted) + { + return; + } + var convertedChargingProcesses = await teslaSolarChargerContext.ChargingProcesses + .Where(c => c.OldHandledChargeId != null) + .ToListAsync().ConfigureAwait(false); + + foreach (var convertedChargingProcess in convertedChargingProcesses) + { + var scope = serviceProvider.CreateScope(); + var scopedTscContext = scope.ServiceProvider.GetRequiredService(); + var chargingDetails = await scopedTscContext.ChargingDetails + .Where(cd => cd.ChargingProcessId == convertedChargingProcess.Id) + .ToListAsync().ConfigureAwait(false); + logger.LogDebug("Fix solar power share for charging processe {chargingProcessId}", convertedChargingProcess.Id); + foreach (var chargingDetail in chargingDetails) + { + if (chargingDetail.SolarPower < 0) + { + chargingDetail.GridPower += chargingDetail.SolarPower; + chargingDetail.SolarPower = 0; + } + } + await scopedTscContext.SaveChangesAsync().ConfigureAwait(false); + } + } + + public async Task UpdateChargingProcessesAfterChargingDetailsFix() + { + logger.LogTrace("{method}()", nameof(UpdateChargingProcessesAfterChargingDetailsFix)); + var chargingProcessesConverted = + await teslaSolarChargerContext.TscConfigurations.AnyAsync(c => c.Key == constants.ChargingDetailsSolarPowerShareFixed).ConfigureAwait(false); + if (chargingProcessesConverted) + { + return; + } + await tscOnlyChargingCostService.UpdateChargePricesOfAllChargingProcesses().ConfigureAwait(false); + teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() + { + Key = constants.ChargingDetailsSolarPowerShareFixed, + Value = "true", + }); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } + public async Task DeleteChargePriceById(int id) { var chargePrice = await teslaSolarChargerContext.ChargePrices diff --git a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs index a5f44541d..ee2a4c8fe 100644 --- a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs +++ b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs @@ -15,6 +15,7 @@ using TeslaSolarCharger.Shared.Dtos.IndexRazor.CarValues; using TeslaSolarCharger.Model.EntityFramework; using TeslaSolarCharger.SharedBackend.MappingExtensions; +using System; [assembly: InternalsVisibleTo("TeslaSolarCharger.Tests")] namespace TeslaSolarCharger.Server.Services; @@ -192,6 +193,38 @@ public async Task AddCarsToSettings() settings.Cars = await GetCars().ConfigureAwait(false); } + public async Task AddBleBaseUrlToAllCars() + { + logger.LogTrace("{method}()", nameof(AddBleBaseUrlToAllCars)); + var bleBaseUrlConverted = + await teslaSolarChargerContext.TscConfigurations.AnyAsync(c => c.Key == constants.BleBaseUrlConverted).ConfigureAwait(false); + if (bleBaseUrlConverted) + { + return; + } + var baseUrl = configurationWrapper.BleBaseUrl(); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return; + } + if (baseUrl.EndsWith("api/")) + { + baseUrl = baseUrl.Substring(0, baseUrl.Length - "api/".Length); + } + var databaseCars = await teslaSolarChargerContext.Cars.ToListAsync().ConfigureAwait(false); + foreach (var car in databaseCars) + { + car.BleApiBaseUrl = baseUrl; + } + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration() + { + Key = constants.BleBaseUrlConverted, + Value = "true", + }); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } + public async Task UpdateCarBasicConfiguration(int carId, CarBasicConfiguration carBasicConfiguration) { logger.LogTrace("{method}({carId}, {@carBasicConfiguration})", nameof(UpdateCarBasicConfiguration), carId, carBasicConfiguration); @@ -206,6 +239,7 @@ public async Task UpdateCarBasicConfiguration(int carId, CarBasicConfiguration c databaseCar.ShouldSetChargeStartTimes = carBasicConfiguration.ShouldSetChargeStartTimes; databaseCar.UseBle = carBasicConfiguration.UseBle; databaseCar.ApiRefreshIntervalSeconds = carBasicConfiguration.ApiRefreshIntervalSeconds; + databaseCar.BleApiBaseUrl = carBasicConfiguration.BleApiBaseUrl; await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var settingsCar = settings.Cars.First(c => c.Id == carId); settingsCar.Name = carBasicConfiguration.Name; @@ -216,6 +250,9 @@ public async Task UpdateCarBasicConfiguration(int carId, CarBasicConfiguration c settingsCar.ChargingPriority = carBasicConfiguration.ChargingPriority; settingsCar.ShouldBeManaged = carBasicConfiguration.ShouldBeManaged; settingsCar.ShouldSetChargeStartTimes = carBasicConfiguration.ShouldSetChargeStartTimes; + settingsCar.ApiRefreshIntervalSeconds = carBasicConfiguration.ApiRefreshIntervalSeconds; + settingsCar.UseBle = carBasicConfiguration.UseBle; + settingsCar.BleApiBaseUrl = carBasicConfiguration.BleApiBaseUrl; } public Task UpdateCarConfiguration(int carId, DepricatedCarConfiguration carConfiguration) diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index b53cc9d6b..1991ba00c 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -8,4 +8,6 @@ public interface IBackendApiService Task PostInstallationInformation(string reason); Task PostErrorInformation(string source, string methodName, string message, string? stackTrace = null); Task GetCurrentVersion(); + Task PostTeslaApiCallStatistics(); + Task GetNewBackendNotifications(); } diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendNotificationService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendNotificationService.cs new file mode 100644 index 000000000..df4a40af7 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendNotificationService.cs @@ -0,0 +1,9 @@ +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface IBackendNotificationService +{ + Task> GetRelevantBackendNotifications(); + Task MarkBackendNotificationAsConfirmed(int id); +} diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index 86aaa769e..0c1486179 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -17,4 +17,5 @@ public interface ITeslaFleetApiService Task RefreshTokensIfAllowedAndNeeded(); Task RefreshFleetApiRequestsAreAllowed(); + void ResetApiRequestCounters(); } diff --git a/TeslaSolarCharger/Server/Services/CoreService.cs b/TeslaSolarCharger/Server/Services/CoreService.cs index a77dfff21..85da89bde 100644 --- a/TeslaSolarCharger/Server/Services/CoreService.cs +++ b/TeslaSolarCharger/Server/Services/CoreService.cs @@ -145,18 +145,6 @@ public async Task DisconnectMqttServices() await _teslaMateMqttService.DisconnectClient("Application shutdown").ConfigureAwait(false); } - public DtoValue TeslaApiRequestsSinceStartup() - { - _logger.LogTrace("{method}()", nameof(TeslaApiRequestsSinceStartup)); - return new DtoValue(_settings.TeslaApiRequestCounter); - } - - public DtoValue ShouldDisplayApiRequestCounter() - { - _logger.LogTrace("{method}()", nameof(TeslaApiRequestsSinceStartup)); - return new DtoValue(_configurationWrapper.ShouldDisplayApiRequestCounter()); - } - public Task> GetPriceData(DateTimeOffset from, DateTimeOffset to) { _logger.LogTrace("{method}({from}, {to})", nameof(GetPriceData), from, to); diff --git a/TeslaSolarCharger/Server/Services/IssueValidationService.cs b/TeslaSolarCharger/Server/Services/IssueValidationService.cs index f2822c448..2ca5f62bb 100644 --- a/TeslaSolarCharger/Server/Services/IssueValidationService.cs +++ b/TeslaSolarCharger/Server/Services/IssueValidationService.cs @@ -15,49 +15,32 @@ namespace TeslaSolarCharger.Server.Services; -public class IssueValidationService : IIssueValidationService +public class IssueValidationService( + ILogger logger, + ISettings settings, + ITeslaMateMqttService teslaMateMqttService, + IPossibleIssues possibleIssues, + IssueKeys issueKeys, + IConfigurationWrapper configurationWrapper, + ITeslamateContext teslamateContext, + IConstants constants, + IDateTimeProvider dateTimeProvider, + ITeslaFleetApiService teslaFleetApiService) + : IIssueValidationService { - private readonly ILogger _logger; - private readonly ISettings _settings; - private readonly ITeslaMateMqttService _teslaMateMqttService; - private readonly IPossibleIssues _possibleIssues; - private readonly IssueKeys _issueKeys; - private readonly IConfigurationWrapper _configurationWrapper; - 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, ITeslaFleetApiService teslaFleetApiService) - { - _logger = logger; - _settings = settings; - _teslaMateMqttService = teslaMateMqttService; - _possibleIssues = possibleIssues; - _issueKeys = issueKeys; - _configurationWrapper = configurationWrapper; - _teslamateContext = teslamateContext; - _constants = constants; - _dateTimeProvider = dateTimeProvider; - _teslaFleetApiService = teslaFleetApiService; - } - public async Task> RefreshIssues(TimeSpan clientTimeZoneId) { - _logger.LogTrace("{method}()", nameof(RefreshIssues)); + logger.LogTrace("{method}()", nameof(RefreshIssues)); var issueList = new List(); - if (_settings.RestartNeeded) + if (settings.RestartNeeded) { - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.RestartNeeded)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.RestartNeeded)); return issueList; } - if (_settings.CrashedOnStartup) + if (settings.CrashedOnStartup) { - var crashedOnStartupIssue = _possibleIssues.GetIssueByKey(_issueKeys.CrashedOnStartup); - crashedOnStartupIssue.PossibleSolutions.Add($"Exeption Message: {_settings.StartupCrashMessage}"); + var crashedOnStartupIssue = possibleIssues.GetIssueByKey(issueKeys.CrashedOnStartup); + crashedOnStartupIssue.PossibleSolutions.Add($"Exeption Message: {settings.StartupCrashMessage}"); issueList.Add(crashedOnStartupIssue); return issueList; } @@ -68,37 +51,37 @@ public async Task> RefreshIssues(TimeSpan clientTimeZoneId) } issueList.AddRange(GetMqttIssues()); issueList.AddRange(PvValueIssues()); - if (!_configurationWrapper.UseFleetApi()) + if (!configurationWrapper.UseFleetApi()) { issueList.AddRange(await GetTeslaMateApiIssues().ConfigureAwait(false)); } else { - var tokenState = (await _teslaFleetApiService.GetFleetApiTokenState().ConfigureAwait(false)).Value; + var tokenState = (await teslaFleetApiService.GetFleetApiTokenState().ConfigureAwait(false)).Value; switch (tokenState) { case FleetApiTokenState.NotNeeded: break; case FleetApiTokenState.NotRequested: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotRequested)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenNotRequested)); break; case FleetApiTokenState.TokenRequestExpired: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenRequestExpired)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenRequestExpired)); break; case FleetApiTokenState.TokenUnauthorized: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenUnauthorized)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenUnauthorized)); break; case FleetApiTokenState.MissingScopes: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenMissingScopes)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenMissingScopes)); break; case FleetApiTokenState.NotReceived: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNotReceived)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenNotReceived)); break; case FleetApiTokenState.Expired: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenExpired)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenExpired)); break; case FleetApiTokenState.NoApiRequestsAllowed: - issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNoApiRequestsAllowed)); + issueList.Add(possibleIssues.GetIssueByKey(issueKeys.FleetApiTokenNoApiRequestsAllowed)); break; case FleetApiTokenState.UpToDate: break; @@ -114,16 +97,16 @@ public async Task> RefreshIssues(TimeSpan clientTimeZoneId) public async Task> ErrorCount() { - _logger.LogTrace("{method}()", nameof(ErrorCount)); - var issues = await RefreshIssues(TimeZoneInfo.Local.GetUtcOffset(_dateTimeProvider.Now())).ConfigureAwait(false); + logger.LogTrace("{method}()", nameof(ErrorCount)); + var issues = await RefreshIssues(TimeZoneInfo.Local.GetUtcOffset(dateTimeProvider.Now())).ConfigureAwait(false); var errorIssues = issues.Where(i => i.IssueType == IssueType.Error).ToList(); return new DtoValue(errorIssues.Count); } public async Task> WarningCount() { - _logger.LogTrace("{method}()", nameof(WarningCount)); - var issues = await RefreshIssues(TimeZoneInfo.Local.GetUtcOffset(_dateTimeProvider.Now())).ConfigureAwait(false); + logger.LogTrace("{method}()", nameof(WarningCount)); + var issues = await RefreshIssues(TimeZoneInfo.Local.GetUtcOffset(dateTimeProvider.Now())).ConfigureAwait(false); var warningIssues = issues.Where(i => i.IssueType == IssueType.Warning).ToList(); var warningCount = new DtoValue(warningIssues.Count); return warningCount; @@ -131,24 +114,24 @@ public async Task> WarningCount() private async Task> GetDatabaseIssues() { - _logger.LogTrace("{method}()", nameof(GetDatabaseIssues)); + logger.LogTrace("{method}()", nameof(GetDatabaseIssues)); var issues = new List(); try { // ReSharper disable once UnusedVariable - var carIds = await _teslamateContext.Cars.Select(car => car.Id).ToListAsync().ConfigureAwait(false); + var carIds = await teslamateContext.Cars.Select(car => car.Id).ToListAsync().ConfigureAwait(false); } catch (Exception) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.DatabaseNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.DatabaseNotAvailable)); return issues; } - var geofenceNames = _teslamateContext.Geofences.Select(ge => ge.Name).ToList(); - var configuredGeofence = _configurationWrapper.GeoFence(); + var geofenceNames = teslamateContext.Geofences.Select(ge => ge.Name).ToList(); + var configuredGeofence = configurationWrapper.GeoFence(); if (!geofenceNames.Any(g => g == configuredGeofence)) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.GeofenceNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.GeofenceNotAvailable)); } return issues; @@ -156,9 +139,9 @@ private async Task> GetDatabaseIssues() private async Task> GetTeslaMateApiIssues() { - _logger.LogTrace("{method}()", nameof(GetTeslaMateApiIssues)); + logger.LogTrace("{method}()", nameof(GetTeslaMateApiIssues)); var issues = new List(); - var teslaMateBaseUrl = _configurationWrapper.TeslaMateApiBaseUrl(); + var teslaMateBaseUrl = configurationWrapper.TeslaMateApiBaseUrl(); var getAllCarsUrl = $"{teslaMateBaseUrl}/api/v1/cars"; using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(1); @@ -167,33 +150,33 @@ private async Task> GetTeslaMateApiIssues() var resultString = await httpClient.GetStringAsync(getAllCarsUrl).ConfigureAwait(false); if (string.IsNullOrEmpty(resultString)) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.TeslaMateApiNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.TeslaMateApiNotAvailable)); } } catch (Exception) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.TeslaMateApiNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.TeslaMateApiNotAvailable)); } return issues; } private List GetMqttIssues() { - _logger.LogTrace("{method}()", nameof(GetMqttIssues)); + logger.LogTrace("{method}()", nameof(GetMqttIssues)); var issues = new List(); - if (!_teslaMateMqttService.IsMqttClientConnected && !_configurationWrapper.GetVehicleDataFromTesla()) + if (!teslaMateMqttService.IsMqttClientConnected && !configurationWrapper.GetVehicleDataFromTesla()) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.MqttNotConnected)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.MqttNotConnected)); } - if (_settings.CarsToManage.Any(c => (c.SocLimit == null || c.SocLimit < _constants.MinSocLimit))) + if (settings.CarsToManage.Any(c => (c.SocLimit == null || c.SocLimit < constants.MinSocLimit))) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.CarSocLimitNotReadable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.CarSocLimitNotReadable)); } - if (_settings.CarsToManage.Any(c => c.SoC == null)) + if (settings.CarsToManage.Any(c => c.SoC == null)) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.CarSocNotReadable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.CarSocNotReadable)); } return issues; @@ -201,44 +184,44 @@ private List GetMqttIssues() private List PvValueIssues() { - _logger.LogTrace("{method}()", nameof(GetMqttIssues)); + logger.LogTrace("{method}()", nameof(GetMqttIssues)); var issues = new List(); - var frontendConfiguration = _configurationWrapper.FrontendConfiguration() ?? new FrontendConfiguration(); + var frontendConfiguration = configurationWrapper.FrontendConfiguration() ?? new FrontendConfiguration(); var isGridPowerConfigured = frontendConfiguration.GridValueSource != SolarValueSource.None; - if (isGridPowerConfigured && _settings.Overage == null) + if (isGridPowerConfigured && settings.Overage == null) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.GridPowerNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.GridPowerNotAvailable)); } var isInverterPowerConfigured = frontendConfiguration.InverterValueSource != SolarValueSource.None; - if (isInverterPowerConfigured && _settings.InverterPower == null) + if (isInverterPowerConfigured && settings.InverterPower == null) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.InverterPowerNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.InverterPowerNotAvailable)); } var isHomeBatteryConfigured = frontendConfiguration.HomeBatteryValuesSource != SolarValueSource.None; - if (isHomeBatteryConfigured && _settings.HomeBatterySoc == null) + if (isHomeBatteryConfigured && settings.HomeBatterySoc == null) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.HomeBatterySocNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.HomeBatterySocNotAvailable)); } - if (isHomeBatteryConfigured && _settings.HomeBatterySoc is > 100 or < 0) + if (isHomeBatteryConfigured && settings.HomeBatterySoc is > 100 or < 0) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.HomeBatterySocNotPlausible)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.HomeBatterySocNotPlausible)); } - if (isHomeBatteryConfigured && _settings.HomeBatteryPower == null) + if (isHomeBatteryConfigured && settings.HomeBatteryPower == null) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.HomeBatteryPowerNotAvailable)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.HomeBatteryPowerNotAvailable)); } - if (isHomeBatteryConfigured && (_configurationWrapper.HomeBatteryMinSoc() == null)) + if (isHomeBatteryConfigured && (configurationWrapper.HomeBatteryMinSoc() == null)) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.HomeBatteryMinimumSocNotConfigured)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.HomeBatteryMinimumSocNotConfigured)); } - if (isHomeBatteryConfigured && (_configurationWrapper.HomeBatteryChargingPower() == null)) + if (isHomeBatteryConfigured && (configurationWrapper.HomeBatteryChargingPower() == null)) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.HomeBatteryChargingPowerNotConfigured)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.HomeBatteryChargingPowerNotConfigured)); } return issues; @@ -247,9 +230,9 @@ private List PvValueIssues() private List SofwareIssues() { var issues = new List(); - if (_settings.IsNewVersionAvailable) + if (settings.IsNewVersionAvailable) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.VersionNotUpToDate)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.VersionNotUpToDate)); } return issues; @@ -259,13 +242,13 @@ private List ConfigurationIssues() { var issues = new List(); - if (_configurationWrapper.CurrentPowerToGridCorrectionFactor() == (decimal)0.0 - || _configurationWrapper.HomeBatteryPowerCorrectionFactor() == (decimal)0.0 - || _configurationWrapper.HomeBatterySocCorrectionFactor() == (decimal)0.0 - || _configurationWrapper.CurrentInverterPowerCorrectionFactor() == (decimal)0.0 + if (configurationWrapper.CurrentPowerToGridCorrectionFactor() == (decimal)0.0 + || configurationWrapper.HomeBatteryPowerCorrectionFactor() == (decimal)0.0 + || configurationWrapper.HomeBatterySocCorrectionFactor() == (decimal)0.0 + || configurationWrapper.CurrentInverterPowerCorrectionFactor() == (decimal)0.0 ) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.CorrectionFactorZero)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.CorrectionFactorZero)); } return issues; } @@ -273,10 +256,10 @@ private List ConfigurationIssues() private List GetServerConfigurationIssues(TimeSpan clientTimeUtcOffset) { var issues = new List(); - var serverTimeUtcOffset = TimeZoneInfo.Local.GetUtcOffset(_dateTimeProvider.Now()); + var serverTimeUtcOffset = TimeZoneInfo.Local.GetUtcOffset(dateTimeProvider.Now()); if (clientTimeUtcOffset != serverTimeUtcOffset) { - issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.ServerTimeZoneDifferentFromClient)); + issues.Add(possibleIssues.GetIssueByKey(issueKeys.ServerTimeZoneDifferentFromClient)); } return issues; diff --git a/TeslaSolarCharger/Server/Services/TeslaBleService.cs b/TeslaSolarCharger/Server/Services/TeslaBleService.cs index 89b48a17c..ca3a060fe 100644 --- a/TeslaSolarCharger/Server/Services/TeslaBleService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaBleService.cs @@ -80,7 +80,7 @@ public async Task FlashLights(string vin) public async Task PairKey(string vin) { logger.LogTrace("{method}({vin})", nameof(PairKey), vin); - var bleBaseUrl = configurationWrapper.BleBaseUrl(); + var bleBaseUrl = GetBleBaseUrl(vin); if (string.IsNullOrWhiteSpace(bleBaseUrl)) { return new DtoBleResult() { Message = "BLE Base URL is not set. Set a BLE URL in your base configuration.", StatusCode = HttpStatusCode.BadRequest, Success = false, }; @@ -125,7 +125,7 @@ public Task SetChargeLimit(int carId, int limitSoC) private async Task SendCommandToBle(DtoBleRequest request) { logger.LogTrace("{method}({@request})", nameof(SendCommandToBle), request); - var bleBaseUrl = configurationWrapper.BleBaseUrl(); + var bleBaseUrl = GetBleBaseUrl(request.Vin); if (string.IsNullOrWhiteSpace(bleBaseUrl)) { return new DtoBleResult() @@ -190,6 +190,25 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) } } + private string? GetBleBaseUrl(string vin) + { + var car = settings.Cars.First(c => c.Vin == vin); + var bleUrl = car.BleApiBaseUrl; + if (string.IsNullOrWhiteSpace(bleUrl)) + { + return null; + } + if (!bleUrl.EndsWith("/")) + { + bleUrl += "/"; + } + if (!bleUrl.EndsWith("/api/")) + { + bleUrl += "api/"; + } + return bleUrl; + } + private string GetVinByCarId(int carId) { var vin = settings.Cars.First(c => c.Id == carId).Vin; diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index d34676bf8..d1009572f 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -260,34 +260,34 @@ public async Task RefreshCarData() } try { - //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 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; @@ -491,62 +491,58 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) 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); + AddRequestToCar(vin, fleetApiRequest); if (fleetApiRequest.BleCompatible) { - var isCarBleEnabled = await teslaSolarChargerContext.Cars - .Where(c => c.Vin == vin) - .Select(c => c.UseBle) - .FirstAsync(); + var car = settings.Cars.First(c => c.Vin == vin); + var isCarBleEnabled = car.UseBle; if (isCarBleEnabled) { - var bleAddress = configurationWrapper.BleBaseUrl(); - if (!string.IsNullOrEmpty(bleAddress)) + + var result = new DtoBleResult(); + if (fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) { - var car = settings.Cars.First(c => c.Vin == vin); - var result = new DtoBleResult(); - if (fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) + result = await bleService.StartCharging(vin); + if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) { - result = await bleService.StartCharging(vin); - if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) - { - car.State = CarStateEnum.Charging; - car.ChargerActualCurrent = car.ChargerRequestedCurrent; - car.ChargerVoltage = settings.AverageHomeGridVoltage ?? 230; - } + car.State = CarStateEnum.Charging; + car.ChargerActualCurrent = car.ChargerRequestedCurrent; + car.ChargerVoltage = settings.AverageHomeGridVoltage ?? 230; } - else if (fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) + } + else if (fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) + { + result = await bleService.StopCharging(vin); + if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) { - result = await bleService.StopCharging(vin); - if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) - { - car.State = CarStateEnum.Online; - car.ChargerActualCurrent = 0; - } + car.State = CarStateEnum.Online; + car.ChargerActualCurrent = 0; } - else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) + } + else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) + { + result = await bleService.SetAmp(vin, amp!.Value); + if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) { - result = await bleService.SetAmp(vin, amp!.Value); - if (result.Success && configurationWrapper.GetVehicleDataFromTesla()) - { - car.ChargerRequestedCurrent = amp!.Value; - car.ChargerActualCurrent = car.State == CarStateEnum.Charging ? amp!.Value : 0; - } + car.ChargerRequestedCurrent = amp!.Value; + car.ChargerActualCurrent = car.State == CarStateEnum.Charging ? amp!.Value : 0; } + } - if (typeof(T) == typeof(DtoVehicleCommandResult)) + if (typeof(T) == typeof(DtoVehicleCommandResult)) + { + var comamndResult = new DtoGenericTeslaResponse() { }; + comamndResult.Response = (T)(object) new DtoVehicleCommandResult() { - var comamndResult = new DtoGenericTeslaResponse() { }; - comamndResult.Response = (T)(object) new DtoVehicleCommandResult() - { - Result = result.Success, - Reason = result.Message, - }; - return comamndResult; - } + Result = result.Success, + Reason = result.Message, + }; + return comamndResult; + } - return new DtoGenericTeslaResponse(); + return new DtoGenericTeslaResponse(); - } + } } var accessToken = await GetAccessToken().ConfigureAwait(false); @@ -563,7 +559,6 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) var fleetApiProxyRequired = await IsFleetApiProxyEnabled(vin).ConfigureAwait(false); var baseUrl = GetFleetApiBaseUrl(accessToken.Region, fleetApiRequest.NeedsProxy, fleetApiProxyRequired.Value); var requestUri = $"{baseUrl}api/1/vehicles/{vin}/{fleetApiRequest.RequestUrl}"; - settings.TeslaApiRequestCounter++; var request = new HttpRequestMessage() { Content = content, @@ -601,6 +596,70 @@ await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameo return teslaCommandResultResponse; } + public void ResetApiRequestCounters() + { + logger.LogTrace("{method}()", nameof(ResetApiRequestCounters)); + var currentUtcDate = dateTimeProvider.UtcNow().Date; + foreach (var car in settings.Cars) + { + car.WakeUpCalls.RemoveAll(d => d < currentUtcDate); + car.VehicleDataCalls.RemoveAll(d => d < currentUtcDate); + car.VehicleCalls.RemoveAll(d => d < currentUtcDate); + car.ChargeStartCalls.RemoveAll(d => d < currentUtcDate); + car.ChargeStopCalls.RemoveAll(d => d < currentUtcDate); + car.SetChargingAmpsCall.RemoveAll(d => d < currentUtcDate); + car.OtherCommandCalls.RemoveAll(d => d < currentUtcDate); + } + } + + private void AddRequestToCar(string vin, DtoFleetApiRequest fleetApiRequest) + { + logger.LogTrace("{method}({@fleetApiRequest})", nameof(AddRequestToCar), fleetApiRequest); + var car = settings.Cars.FirstOrDefault(c => c.Vin == vin); + if (car == default) + { + logger.LogError("Could find car for request logging"); + return; + } + var currentDate = dateTimeProvider.UtcNow(); + if (fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) + { + car.ChargeStartCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) + { + car.ChargeStopCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) + { + car.SetChargingAmpsCall.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == SetScheduledChargingRequest.RequestUrl) + { + car.OtherCommandCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == SetChargeLimitRequest.RequestUrl) + { + car.OtherCommandCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == OpenChargePortDoorRequest.RequestUrl) + { + car.OtherCommandCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == WakeUpRequest.RequestUrl) + { + car.WakeUpCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == VehicleRequest.RequestUrl) + { + car.VehicleCalls.Add(currentDate); + } + else if (fleetApiRequest.RequestUrl == VehicleDataRequest.RequestUrl) + { + car.VehicleDataCalls.Add(currentDate); + } + } + private async Task RateLimitedUntil(string vin) { logger.LogTrace("{method}", nameof(RateLimitedUntil)); diff --git a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs index f9c8647f0..29e6c9019 100644 --- a/TeslaSolarCharger/Server/Services/TeslamateApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslamateApiService.cs @@ -249,7 +249,6 @@ private async Task SendPostToTeslaMate(string url, Dictiona _logger.LogTrace("{method}({param1}, {param2})", nameof(SendPostToTeslaMate), url, parameters); var jsonString = JsonConvert.SerializeObject(parameters); var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); - _settings.TeslaApiRequestCounter++; using var httpClient = new HttpClient(); var response = await httpClient.PostAsync(url, content).ConfigureAwait(false); if (!response.IsSuccessStatusCode) diff --git a/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs index a318e0157..76e0c806a 100644 --- a/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs +++ b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs @@ -60,11 +60,12 @@ public async Task FinalizeFinishedChargingProcesses() public async Task UpdateChargePricesOfAllChargingProcesses() { logger.LogTrace("{method}()", nameof(UpdateChargePricesOfAllChargingProcesses)); - var openChargingProcesses = await context.ChargingProcesses + var finalizedChargingProcesses = await context.ChargingProcesses .Where(cp => cp.EndDate != null) .ToListAsync().ConfigureAwait(false); - foreach (var chargingProcess in openChargingProcesses) + foreach (var chargingProcess in finalizedChargingProcesses) { + settings.ChargePricesUpdateText = $"Updating charging processes {finalizedChargingProcesses.IndexOf(chargingProcess)}/{finalizedChargingProcesses.Count}"; try { await FinalizeChargingProcess(chargingProcess); @@ -74,6 +75,8 @@ public async Task UpdateChargePricesOfAllChargingProcesses() logger.LogError(ex, "Error while updating charge prices of charging process with ID {chargingProcessId}.", chargingProcess.Id); } } + + settings.ChargePricesUpdateText = null; } public async Task> GetChargeSummaries() @@ -158,8 +161,7 @@ private async Task FinalizeChargingProcess(ChargingProcess chargingProcess) decimal usedGridEnergyWh = 0; decimal cost = 0; chargingProcess.EndDate = chargingDetails.Last().TimeStamp; - var prices = await GetPricesInTimeSpan(chargingProcess.StartDate, chargingProcess.EndDate.Value); - //When a charging process is stopped and resumed later, the last charging detail is too old and should not be used because it would use the last value dring the whole time althoug the car was not charging + var prices = await GetPricesInTimeSpan(chargingDetails.First().TimeStamp, chargingProcess.EndDate.Value); //When a charging process is stopped and resumed later, the last charging detail is too old and should not be used because it would use the last value dring the whole time althoug the car was not charging var maxChargingDetailsDuration = TimeSpan.FromSeconds(constants.ChargingDetailsAddTriggerEveryXSeconds).Add(TimeSpan.FromSeconds(10)); for (var index = 1; index < chargingDetails.Count; index++) { @@ -215,6 +217,8 @@ private async Task> GetPricesInTimeSpan(DateTime from, DateTime to) case EnergyProvider.FixedPrice: priceDataService = serviceProvider.GetRequiredService(); prices = (await priceDataService.GetPriceData(fromDateTimeOffset, toDateTimeOffset, chargePrice.EnergyProviderConfiguration).ConfigureAwait(false)).ToList(); + prices = AddDefaultChargePrices(prices, fromDateTimeOffset, toDateTimeOffset, chargePrice.GridPrice, chargePrice.SolarPrice); + return prices; case EnergyProvider.Awattar: break; @@ -233,6 +237,52 @@ private async Task> GetPricesInTimeSpan(DateTime from, DateTime to) throw new NotImplementedException($"Energyprovider {chargePrice.EnergyProvider} is not implemented."); } + private List AddDefaultChargePrices(List prices, DateTimeOffset from, DateTimeOffset to, decimal defaultValue, decimal defaultSolarPrice) + { + var updatedPrices = new List(); + + // Sort the list by ValidFrom + prices = prices.OrderBy(p => p.ValidFrom).ToList(); + + // Initialize the start of the uncovered period + var currentStart = from; + + foreach (var price in prices) + { + // If there's a gap between currentStart and the next price.ValidFrom + if (currentStart < price.ValidFrom) + { + updatedPrices.Add(new Price + { + Value = defaultValue, + SolarPrice = defaultSolarPrice, + ValidFrom = currentStart, + ValidTo = price.ValidFrom, + }); + } + + // Update currentStart to the end of the current price's ValidTo + currentStart = price.ValidTo; + } + + // Check for a gap after the last price.ValidTo to the 'to' date + if (currentStart < to) + { + updatedPrices.Add(new Price + { + Value = defaultValue, + SolarPrice = defaultSolarPrice, + ValidFrom = currentStart, + ValidTo = to, + }); + } + + // Add all original prices to the updated list + updatedPrices.AddRange(prices); + + return updatedPrices.OrderBy(p => p.ValidFrom).ToList(); + } + public async Task AddChargingDetailsForAllCars() { logger.LogTrace("{method}()", nameof(AddChargingDetailsForAllCars)); diff --git a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj index 5f5ae17cb..68e534b67 100644 --- a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj +++ b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj @@ -58,11 +58,11 @@ - + - + diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 2850f10bb..08fcce79d 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -11,8 +11,6 @@ "Microsoft": "Warning", "System": "Error", "Microsoft.EntityFrameworkCore.Database.Command": "Warning", - "Quartz": "Warning", - "TeslaSolarCharger.Server.Scheduling": "Information", "TeslaSolarCharger.Shared.Wrappers.ConfigurationWrapper": "Information", "TeslaSolarCharger.Model.EntityFramework.DbConnectionStringHelper": "Information" } @@ -58,7 +56,6 @@ "TeslaMateDbPassword": "secret", "TeslaMateApiBaseUrl": "http://teslamateapi:8080", "GeoFence": "Home", - "DisplayApiRequestCounter": true, "IgnoreSslErrors": false, "UseFleetApi": true, "FleetApiClientId": "f29f71d6285a-4873-8b6b-80f15854892e", @@ -66,6 +63,7 @@ "TeslaFleetApiBaseUrl": "https://www.teslasolarcharger.de/teslaproxy/", "UseFleetApiProxy": false, "LogLocationData": false, + "SendTeslaApiStatsToBackend": true, "GetVehicleDataFromTesla": true, "GetVehicleDataFromTeslaDebug": false, "AwattarBaseUrl": "https://api.awattar.de/v1/marketdata", diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 772ce813b..32d48869f 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -81,7 +81,6 @@ public interface IConfigurationWrapper FrontendConfiguration? FrontendConfiguration(); bool AllowCors(); - bool ShouldDisplayApiRequestCounter(); bool ShouldIgnoreSslErrors(); string BackupCopyDestinationDirectory(); string GetSqliteFileNameWithoutPath(); @@ -99,4 +98,5 @@ public interface IConfigurationWrapper bool GetVehicleDataFromTesla(); int? MaxInverterAcPower(); string? BleBaseUrl(); + bool SendTeslaApiStatsToBackend(); } diff --git a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs index e7f60b29a..a53e5c270 100644 --- a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs +++ b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs @@ -44,7 +44,7 @@ public CarBasicConfiguration(int id, string? name) [Postfix("s")] [Range(11, int.MaxValue)] public int ApiRefreshIntervalSeconds { get; set; } - - + [HelperText("Needed to send commands via BLE to the car. An example value would be `http://raspible:7210/`")] + public string? BleApiBaseUrl { get; set; } } diff --git a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs index e9c945435..71d04ef87 100644 --- a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs +++ b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs @@ -14,7 +14,6 @@ public interface ISettings bool IsNewVersionAvailable { get; set; } DateTimeOffset LastPvValueUpdate { get; set; } int? AverageHomeGridVoltage { get; set; } - int TeslaApiRequestCounter { get; set; } bool CrashedOnStartup { get; set; } string? StartupCrashMessage { get; set; } bool AllowUnlimitedFleetApiRequests { get; set; } @@ -27,4 +26,5 @@ public interface ISettings Dictionary CalculatedRestValues { get; set; } bool IsStartupCompleted { get; set; } string? ChargePricesUpdateText { get; set; } + DateTime StartupTime { get; set; } } diff --git a/TeslaSolarCharger/Shared/Dtos/DtoBackendNotification.cs b/TeslaSolarCharger/Shared/Dtos/DtoBackendNotification.cs new file mode 100644 index 000000000..04a40a1c6 --- /dev/null +++ b/TeslaSolarCharger/Shared/Dtos/DtoBackendNotification.cs @@ -0,0 +1,15 @@ +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Shared.Dtos; + +public class DtoBackendNotification +{ + public int Id { get; set; } + public BackendNotificationType Type { get; set; } + public string Headline { get; set; } + public string DetailText { get; set; } + public DateTime? ValidFromDate { get; set; } + public DateTime? ValidToDate { get; set; } + public string? ValidFromVersion { get; set; } + public string? ValidToVersion { get; set; } +} diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs index 874971028..8392bbfaf 100644 --- a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs +++ b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs @@ -88,5 +88,14 @@ private int? ChargingPower public bool ReducedChargeSpeedWarning { get; set; } public DateTimeOffset LastApiDataRefresh { get; set; } public int ApiRefreshIntervalSeconds { get; set; } + public bool UseBle { get; set; } + public string? BleApiBaseUrl { get; set; } public List PlannedChargingSlots { get; set; } = new List(); + public List WakeUpCalls { get; set; } = new List(); + public List VehicleDataCalls { get; set; } = new List(); + public List VehicleCalls { get; set; } = new List(); + public List ChargeStartCalls { get; set; } = new List(); + public List ChargeStopCalls { get; set; } = new List(); + public List SetChargingAmpsCall { get; set; } = new List(); + public List OtherCommandCalls { get; set; } = new List(); } diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs index da1372bec..deb54408a 100644 --- a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs +++ b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs @@ -15,7 +15,6 @@ public class Settings : ISettings public bool ControlledACarAtLastCycle { get; set; } public DateTimeOffset LastPvValueUpdate { get; set; } public int? AverageHomeGridVoltage { get; set; } - public int TeslaApiRequestCounter { get; set; } = 0; public bool CrashedOnStartup { get; set; } public string? StartupCrashMessage { get; set; } @@ -34,5 +33,7 @@ public class Settings : ISettings public bool IsStartupCompleted { get; set; } + public DateTime StartupTime { get; set; } + public string? ChargePricesUpdateText { get; set; } } diff --git a/TeslaSolarCharger/Shared/Enums/BackendNotificationType.cs b/TeslaSolarCharger/Shared/Enums/BackendNotificationType.cs new file mode 100644 index 000000000..a1b0ff2d6 --- /dev/null +++ b/TeslaSolarCharger/Shared/Enums/BackendNotificationType.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Shared.Enums; + +public enum BackendNotificationType +{ + Information, + Warning, + Error, +} diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index cce13a589..dce37f749 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -14,6 +14,7 @@ public class Constants : IConstants public string NextAllowedTeslaApiRequest => "NextAllowedTeslaApiRequest"; public string BackupZipBaseFileName => "TSC-Backup.zip"; + public string DefaultMargin => "mb-4"; public Margin InputMargin => Margin.Dense; @@ -22,8 +23,10 @@ public class Constants : IConstants public string TokenRefreshUnauthorized => "TokenRefreshUnauthorized"; public string TokenMissingScopes => "TokenMissingScopes"; public string CarConfigurationsConverted => "CarConfigurationsConverted"; + public string BleBaseUrlConverted => "BleBaseUrlConverted"; public string HandledChargesCarIdsConverted => "HandledChargesCarIdsConverted"; public string HandledChargesConverted => "HandledChargesConverted"; + public string ChargingDetailsSolarPowerShareFixed => "ChargingDetailsSolarPowerShareFixed"; public TimeSpan MaxTokenRequestWaitTime => TimeSpan.FromMinutes(5); public TimeSpan MinTokenRestLifetime => TimeSpan.FromMinutes(2); public int MaxTokenUnauthorizedCount => 5; diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index c6ff38e76..bc19d0e6b 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -22,10 +22,12 @@ public interface IConstants TimeSpan MinTokenRestLifetime { get; } int MaxTokenUnauthorizedCount { get; } string CarConfigurationsConverted { get; } + string BleBaseUrlConverted { get; } string DefaultMargin { get; } Margin InputMargin { get; } string HandledChargesCarIdsConverted { get; } string HandledChargesConverted { get; } string GridPoleIcon { get; } int ChargingDetailsAddTriggerEveryXSeconds { get; } + string ChargingDetailsSolarPowerShareFixed { get; } } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index a9a3c137d..8a7d7ad60 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -148,6 +148,13 @@ public bool LogLocationData() return value; } + public bool SendTeslaApiStatsToBackend() + { + var environmentVariableName = "SendTeslaApiStatsToBackend"; + var value = configuration.GetValue(environmentVariableName); + return value; + } + public string BackendApiBaseUrl() { var environmentVariableName = "BackendApiBaseUrl"; @@ -171,11 +178,6 @@ public string FleetApiClientId() public string? BleBaseUrl() { var value = GetBaseConfiguration().BleApiBaseUrl; - if (string.IsNullOrWhiteSpace(value)) - { - var environmentVariableName = "BleBaseUrl"; - value = configuration.GetValue(environmentVariableName); - } if (!string.IsNullOrWhiteSpace(value)) { if (!value.EndsWith("/")) @@ -852,9 +854,4 @@ public async Task UpdateBaseConfigurationAsync(DtoBaseConfiguration dtoBaseConfi await UpdateJsonFile(BaseConfigFileFullName(), baseConfigurationJsonString).ConfigureAwait(false); } - - public bool ShouldDisplayApiRequestCounter() - { - return configuration.GetValue("DisplayApiRequestCounter"); - } }