diff --git a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj
index 1bf763b70..857a0fa27 100644
--- a/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj
+++ b/Plugins.SmaEnergymeter/Plugins.SmaEnergymeter.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Plugins.Solax/Plugins.Solax.csproj b/Plugins.Solax/Plugins.Solax.csproj
index 746969dbf..ffa71da2c 100644
--- a/Plugins.Solax/Plugins.Solax.csproj
+++ b/Plugins.Solax/Plugins.Solax.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/README.md b/README.md
index 024054c08..dbc7526b1 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,7 @@
[](https://hub.docker.com/r/pkuehnel/teslasolarcharger)
[](https://hub.docker.com/r/pkuehnel/teslasolarcharger)
-[](https://hub.docker.com/r/pkuehnel/teslasolarcharger)
-[](https://hub.docker.com/r/pkuehnel/smartteslaampsetter)
+[](https://hub.docker.com/r/pkuehnel/teslasolarcharger)
[](https://www.paypal.com/donate/?hosted_button_id=S3CK8Q9KV3JUL)
[](https://github.com/pkuehnel/TeslaSolarCharger/actions/workflows/edgeRelease.yml)
@@ -175,8 +174,7 @@ volumes:
[](https://hub.docker.com/r/pkuehnel/teslasolarchargersmaplugin)
[](https://hub.docker.com/r/pkuehnel/teslasolarchargersmaplugin)
-[](https://hub.docker.com/r/pkuehnel/teslasolarchargersmaplugin)
-[](https://hub.docker.com/r/pkuehnel/smartteslaampsettersmaplugin)
+[](https://hub.docker.com/r/pkuehnel/teslasolarchargersmaplugin)
The SMA plugin is used to access your EnergyMeter (or Sunny Home Manager 2.0) values.
To use the plugin, add these lines to the bottom of your `docker-compose.yml`.
@@ -327,8 +325,7 @@ volumes:
[](https://hub.docker.com/r/pkuehnel/teslasolarchargersolaredgeplugin)
[](https://hub.docker.com/r/pkuehnel/teslasolarchargersolaredgeplugin)
-[](https://hub.docker.com/r/pkuehnel/teslasolarchargersolaredgeplugin)
-[](https://hub.docker.com/r/pkuehnel/smartteslaampsettersolaredgeplugin)
+[](https://hub.docker.com/r/pkuehnel/teslasolarchargersolaredgeplugin)
The SolarEdge Plugin uses the cloud API, which is limited to 300 which is reset after 15 minutes. When the limit is reached the solaredge API does not gather any new values. This results in TSC displaying 0 grid and home battery power until 15 minutes are over.
diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs
index 23356bf02..4a61b6153 100644
--- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs
+++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs
@@ -9,6 +9,7 @@ public class TeslaToken
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string IdToken { get; set; }
+ public int UnauthorizedCounter { get; set; }
public DateTime ExpiresAtUtc { get; set; }
public TeslaFleetApiRegion Region { get; set; }
}
diff --git a/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.Designer.cs
new file mode 100644
index 000000000..997474ca6
--- /dev/null
+++ b/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.Designer.cs
@@ -0,0 +1,252 @@
+//
+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("20240221111628_AddTokenUnauthorizedCounter")]
+ partial class AddTokenUnauthorizedCounter
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+
+ 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("TeslaFleetApiState")
+ .HasColumnType("INTEGER");
+
+ b.Property("TeslaMateCarId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TeslaMateCarId")
+ .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.HandledCharge", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AverageSpotPrice")
+ .HasColumnType("TEXT");
+
+ b.Property("CalculatedPrice")
+ .HasColumnType("TEXT");
+
+ b.Property("CarId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChargingProcessId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UsedGridEnergy")
+ .HasColumnType("TEXT");
+
+ b.Property("UsedSolarEnergy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("HandledCharges");
+ });
+
+ modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChargingPower")
+ .HasColumnType("INTEGER");
+
+ b.Property("GridProportion")
+ .HasColumnType("REAL");
+
+ b.Property("HandledChargeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PowerFromGrid")
+ .HasColumnType("INTEGER");
+
+ b.Property("TimeStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UsedWattHours")
+ .HasColumnType("REAL");
+
+ b.HasKey("Id");
+
+ b.HasIndex("HandledChargeId");
+
+ b.ToTable("PowerDistributions");
+ });
+
+ modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Price")
+ .HasColumnType("TEXT");
+
+ b.Property("StartDate")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SpotPrices");
+ });
+
+ modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresAtUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("IdToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("RefreshToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Region")
+ .HasColumnType("INTEGER");
+
+ b.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.PowerDistribution", b =>
+ {
+ b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge")
+ .WithMany("PowerDistributions")
+ .HasForeignKey("HandledChargeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("HandledCharge");
+ });
+
+ modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b =>
+ {
+ b.Navigation("PowerDistributions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.cs b/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.cs
new file mode 100644
index 000000000..e0a983ed6
--- /dev/null
+++ b/TeslaSolarCharger.Model/Migrations/20240221111628_AddTokenUnauthorizedCounter.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace TeslaSolarCharger.Model.Migrations
+{
+ ///
+ public partial class AddTokenUnauthorizedCounter : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "UnauthorizedCounter",
+ table: "TeslaTokens",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "UnauthorizedCounter",
+ table: "TeslaTokens");
+ }
+ }
+}
diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs
index f6684975f..ada501b40 100644
--- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs
+++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs
@@ -199,6 +199,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Region")
.HasColumnType("INTEGER");
+ b.Property("UnauthorizedCounter")
+ .HasColumnType("INTEGER");
+
b.HasKey("Id");
b.ToTable("TeslaTokens");
diff --git a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs
index d1ae1829d..4ca6ca02e 100644
--- a/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs
+++ b/TeslaSolarCharger.SharedBackend/Contracts/IConstants.cs
@@ -13,8 +13,10 @@ public interface IConstants
string InstallationIdKey { get; }
string FleetApiTokenRequested { get; }
- string TokenRefreshUnauthorized { get; }
string TokenMissingScopes { get; }
string BackupZipBaseFileName { get; }
string FleetApiProxyNeeded { get; }
+ TimeSpan MaxTokenRequestWaitTime { get; }
+ TimeSpan MinTokenRestLifetime { get; }
+ int MaxTokenUnauthorizedCount { get; }
}
diff --git a/TeslaSolarCharger.SharedBackend/Values/Constants.cs b/TeslaSolarCharger.SharedBackend/Values/Constants.cs
index 9e7ed4890..4c4bbf805 100644
--- a/TeslaSolarCharger.SharedBackend/Values/Constants.cs
+++ b/TeslaSolarCharger.SharedBackend/Values/Constants.cs
@@ -16,4 +16,7 @@ public class Constants : IConstants
public string TokenRefreshUnauthorized => "TokenRefreshUnauthorized";
public string TokenMissingScopes => "TokenMissingScopes";
public string FleetApiProxyNeeded => "FleetApiProxyNeeded";
+ public TimeSpan MaxTokenRequestWaitTime => TimeSpan.FromMinutes(5);
+ public TimeSpan MinTokenRestLifetime => TimeSpan.FromMinutes(2);
+ public int MaxTokenUnauthorizedCount => 5;
}
diff --git a/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj b/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj
index 3a22e5778..dff97e717 100644
--- a/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj
+++ b/TeslaSolarCharger.Tests/TeslaSolarCharger.Tests.csproj
@@ -9,7 +9,7 @@
-
+
@@ -18,8 +18,8 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/TeslaSolarCharger/Client/Components/TextShortenComponent.razor b/TeslaSolarCharger/Client/Components/TextShortenComponent.razor
new file mode 100644
index 000000000..47f1754cc
--- /dev/null
+++ b/TeslaSolarCharger/Client/Components/TextShortenComponent.razor
@@ -0,0 +1,89 @@
+@inject IJSRuntime JavaScriptRuntime
+@inject ISnackbar Snackbar
+
+
+@if (string.IsNullOrWhiteSpace(InputString))
+{
+
+}
+else if (!_hasShortenedText)
+{
+ @InputString
+}
+else
+{
+
+
+ @_textToDisplay
+
+
+
+ @TooltipText
+
+
+
+}
+
+@code {
+ [Parameter]
+ public string? InputString { get; set; }
+
+ [Parameter]
+ public int MaxLength { get; set; } = 10;
+
+ [Parameter]
+ public bool ShouldDisplayTruncatedCharCount { get; set; } = true;
+
+ [Parameter]
+ public string? TooltipText { get; set; }
+
+ [Parameter]
+ public EventCallback OnCopyClicked { get; set; }
+
+
+ private bool _hasShortenedText;
+
+ private string? _textToDisplay;
+
+ protected override void OnInitialized()
+ {
+ _textToDisplay = TruncateAndAppend(InputString, MaxLength, ShouldDisplayTruncatedCharCount);
+ if (!string.Equals(_textToDisplay, InputString))
+ {
+ _hasShortenedText = true;
+ }
+ }
+
+
+ private string? TruncateAndAppend(string? input, int maxLength, bool shouldDisplayTruncatedCharCount)
+ {
+ if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
+ {
+ return input;
+ }
+ const int charsPerPlaceholder = 3;
+ var numberOfPlaceHolders = shouldDisplayTruncatedCharCount ? 2 : 1;
+ var baseTemplate = "{0}...";
+ var suffixTemplate = shouldDisplayTruncatedCharCount ? baseTemplate + "(+{1})" : baseTemplate;
+ var suffixLength = shouldDisplayTruncatedCharCount ? suffixTemplate.Length - (numberOfPlaceHolders * charsPerPlaceholder) + (input.Length - (MaxLength - suffixTemplate.Length + numberOfPlaceHolders * charsPerPlaceholder)).ToString().Length : 3;
+ var truncatedLength = maxLength - suffixLength;
+ var truncatedString = input.Substring(0, truncatedLength);
+ return string.Format(suffixTemplate, truncatedString, input.Length - truncatedLength);
+ }
+
+ private async Task CopyToClipBoard()
+ {
+ try
+ {
+ await JavaScriptRuntime.InvokeVoidAsync("copyToClipboard", InputString);
+ Snackbar.Add("Copied to clipboard.", Severity.Success);
+ await OnCopyClicked.InvokeAsync();
+ }
+ catch(Exception e)
+ {
+ Snackbar.Add("Failed to copy to clipboard.", Severity.Error);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor
index c1b5ad51c..4efe76dd0 100644
--- a/TeslaSolarCharger/Client/Pages/Index.razor
+++ b/TeslaSolarCharger/Client/Pages/Index.razor
@@ -414,7 +414,13 @@ else
}
- Installation ID: @_installationId
+ Installation ID:
+
Language settings: @CultureInfo.CurrentCulture
@@ -695,4 +701,9 @@ else
_testingFleetApiCarIds.Remove(carId);
}
+ private void ShowInstallationIdWarning()
+ {
+ Snackbar.Add("Do not share the ID with anyone", Severity.Warning);
+ }
+
}
\ No newline at end of file
diff --git a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
index 2320f4e6c..cb666676e 100644
--- a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
+++ b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
@@ -16,9 +16,9 @@
-
-
-
+
+
+
diff --git a/TeslaSolarCharger/Client/wwwroot/index.html b/TeslaSolarCharger/Client/wwwroot/index.html
index 24ce11797..aa3e2c95c 100644
--- a/TeslaSolarCharger/Client/wwwroot/index.html
+++ b/TeslaSolarCharger/Client/wwwroot/index.html
@@ -32,6 +32,7 @@
🗙
+
diff --git a/TeslaSolarCharger/Client/wwwroot/js/copyToClipboard.js b/TeslaSolarCharger/Client/wwwroot/js/copyToClipboard.js
new file mode 100644
index 000000000..2e989d08f
--- /dev/null
+++ b/TeslaSolarCharger/Client/wwwroot/js/copyToClipboard.js
@@ -0,0 +1,25 @@
+window.copyToClipboard = async (textToCopy) => {
+ // Navigator clipboard api needs a secure context (https)
+ if (navigator.clipboard && window.isSecureContext) {
+ await navigator.clipboard.writeText(textToCopy);
+ } else {
+ // Use the 'out of viewport hidden text area' trick
+ const textArea = document.createElement("textarea");
+ textArea.value = textToCopy;
+
+ // Move textarea out of the viewport so it's not visible
+ textArea.style.position = "absolute";
+ textArea.style.left = "-999999px";
+
+ document.body.prepend(textArea);
+ textArea.select();
+
+ try {
+ document.execCommand('copy');
+ } catch (error) {
+ console.error(error);
+ } finally {
+ textArea.remove();
+ }
+ }
+}
diff --git a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs
index d95af29e5..408cfdc02 100644
--- a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs
+++ b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs
@@ -22,7 +22,7 @@ public class FleetApiController(
public Task> GetOauthUrl(string locale, string baseUrl) => backendApiService.StartTeslaOAuth(locale, baseUrl);
[HttpGet]
- public Task RefreshFleetApiToken() => fleetApiService.RefreshTokenAsync();
+ public Task RefreshFleetApiToken() => fleetApiService.GetNewTokenFromBackend();
///
/// Note: This endpoint is only available in development environment
diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs
index 42f94acef..9787b89d1 100644
--- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs
+++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs
@@ -164,7 +164,7 @@ public PossibleIssues(IssueKeys issueKeys)
issueKeys.FleetApiTokenRequestExpired, CreateIssue("The Tesla token could not be received.",
IssueType.Error,
"Open the Base Configuration and request a new token.",
- "If this issue keeps occuring, feel free to open an issue on Github including the last 5 chars of your installation ID (bottom of the page). Do NOT include the whole ID."
+ "If this issue keeps occuring, feel free to open an issue on Github including the first 10 chars of your installation ID (bottom of the page). Do NOT include the whole ID."
)
},
{
diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs
index cf5a21377..d1bc0a18e 100644
--- a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs
+++ b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs
@@ -4,20 +4,15 @@
namespace TeslaSolarCharger.Server.Scheduling.Jobs;
[DisallowConcurrentExecution]
-public class FleetApiTokenRefreshJob : IJob
+public class FleetApiTokenRefreshJob(ILogger logger,
+ ITeslaFleetApiService service)
+ : IJob
{
- private readonly ILogger _logger;
- private readonly ITeslaFleetApiService _service;
-
- public FleetApiTokenRefreshJob(ILogger logger, ITeslaFleetApiService service)
- {
- _logger = logger;
- _service = service;
- }
-
public async Task Execute(IJobExecutionContext context)
{
- _logger.LogTrace("{method}({context})", nameof(Execute), context);
- await _service.RefreshTokenAsync().ConfigureAwait(false);
+ logger.LogTrace("{method}({context})", nameof(Execute), context);
+ await service.RefreshFleetApiRequestsAreAllowed().ConfigureAwait(false);
+ await service.GetNewTokenFromBackend().ConfigureAwait(false);
+ await service.RefreshTokensIfAllowedAndNeeded().ConfigureAwait(false);
}
}
diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs
index defab83a2..b733d76fd 100644
--- a/TeslaSolarCharger/Server/Services/BackendApiService.cs
+++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs
@@ -39,8 +39,7 @@ public async Task> StartTeslaOAuth(string locale, string baseUr
var currentTokens = await _teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false);
_teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens);
var cconfigEntriesToRemove = await _teslaSolarChargerContext.TscConfigurations
- .Where(c => c.Key == _constants.TokenRefreshUnauthorized
- || c.Key == _constants.TokenMissingScopes)
+ .Where(c => c.Key == _constants.TokenMissingScopes)
.ToListAsync().ConfigureAwait(false);
_teslaSolarChargerContext.TscConfigurations.RemoveRange(cconfigEntriesToRemove);
await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs
index fc6807280..3097349c9 100644
--- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs
+++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs
@@ -8,11 +8,14 @@ public interface ITeslaFleetApiService
{
Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token);
Task> GetFleetApiTokenState();
- Task RefreshTokenAsync();
+ Task GetNewTokenFromBackend();
Task OpenChargePortDoor(int carId);
Task> TestFleetApiAccess(int carId);
DtoValue IsFleetApiEnabled();
DtoValue IsFleetApiProxyEnabled();
Task IsFleetApiProxyNeededInDatabase();
Task RefreshCarData();
+ Task RefreshTokensIfAllowedAndNeeded();
+ Task RefreshFleetApiRequestsAreAllowed();
+
}
diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
index 50bbdca34..99a0374db 100644
--- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
+++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
@@ -14,6 +14,7 @@
using TeslaSolarCharger.Shared.Contracts;
using TeslaSolarCharger.Shared.Dtos;
using TeslaSolarCharger.Shared.Dtos.Contracts;
+using TeslaSolarCharger.Shared.Dtos.Settings;
using TeslaSolarCharger.Shared.Enums;
using TeslaSolarCharger.SharedBackend.Contracts;
using TeslaSolarCharger.SharedBackend.Dtos;
@@ -468,7 +469,7 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState)
private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}") where T : class
{
logger.LogTrace("{method}({vin}, {@fleetApiRequest}, {contentData})", nameof(SendCommandToTeslaApi), vin, fleetApiRequest, contentData);
- var accessToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false);
+ var accessToken = await GetAccessToken().ConfigureAwait(false);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json");
@@ -491,10 +492,14 @@ await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameo
}
var teslaCommandResultResponse = JsonConvert.DeserializeObject>(responseString);
- if (!response.IsSuccessStatusCode)
+ if (response.IsSuccessStatusCode)
+ {
+ }
+ else
{
await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi),
$"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Content data:{contentData}. Response string: {responseString}").ConfigureAwait(false);
+ logger.LogError("Sending command to Tesla API resulted in non succes status code: {statusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}", response.StatusCode, fleetApiRequest.RequestUrl, contentData, responseString);
await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, accessToken, responseString, vin).ConfigureAwait(false);
}
@@ -505,6 +510,7 @@ await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameo
await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi),
$"Result of command request is false {fleetApiRequest.RequestUrl}, {contentData}. Response string: {responseString}")
.ConfigureAwait(false);
+ logger.LogError("Result of command request is false {fleetApiRequest.RequestUrl}, {contentData}. Response string: {responseString}", fleetApiRequest.RequestUrl, contentData, responseString);
await HandleUnsignedCommands(vehicleCommandResult).ConfigureAwait(false);
}
}
@@ -560,63 +566,115 @@ private string GetFleetApiBaseUrl(TeslaFleetApiRegion region, bool useProxyBaseU
return $"https://fleet-api.prd.{regionCode}.vn.cloud.tesla.com/";
}
- public async Task RefreshTokenAsync()
+ public async Task GetNewTokenFromBackend()
{
- logger.LogTrace("{method}()", nameof(RefreshTokenAsync));
- settings.AllowUnlimitedFleetApiRequests = await CheckIfFleetApiRequestsAreAllowed().ConfigureAwait(false);
- var tokenState = (await GetFleetApiTokenState().ConfigureAwait(false)).Value;
- switch (tokenState)
- {
- case FleetApiTokenState.NotNeeded:
- logger.LogDebug("Refreshing token not needed.");
- return;
- case FleetApiTokenState.NotRequested:
- logger.LogDebug("No token has been requested, yet.");
- return;
- case FleetApiTokenState.TokenRequestExpired:
- logger.LogError("Your token request has expired, create a new one.");
- return;
- case FleetApiTokenState.TokenUnauthorized:
- logger.LogError("Your refresh token is unauthorized, create a new token.");
- return;
- case FleetApiTokenState.NotReceived:
- break;
- case FleetApiTokenState.Expired:
- break;
- case FleetApiTokenState.UpToDate:
- logger.LogDebug("Token is up to date.");
- break;
- case FleetApiTokenState.NoApiRequestsAllowed:
- logger.LogError("No API requests allowed.");
- return;
- default:
- throw new ArgumentOutOfRangeException();
- }
+ logger.LogTrace("{method}()", nameof(GetNewTokenFromBackend));
+ //As all tokens get deleted when requesting a new one, we can assume that there is no token in the database.
var token = await teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false);
if (token == null)
{
+ var tokenRequestedDate = await GetTokenRequestedDate().ConfigureAwait(false);
+ if (tokenRequestedDate == null)
+ {
+ logger.LogError("Token has not been requested. Fleet API currently not working");
+ return;
+ }
+ if (tokenRequestedDate < dateTimeProvider.UtcNow().Subtract(constants.MaxTokenRequestWaitTime))
+ {
+ logger.LogError("Last token request is too old. Request a new token.");
+ return;
+ }
using var httpClient = new HttpClient();
var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false);
var url = configurationWrapper.BackendApiBaseUrl() + $"Tsc/DeliverAuthToken?installationId={installationId}";
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
+ {
+ logger.LogError("Error getting token from TSC Backend. Response status code: {statusCode}, Response string: {responseString}",
+ response.StatusCode, responseString);
+ }
+ else
+ {
+ var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string.");
+ await AddNewTokenAsync(newToken).ConfigureAwait(false);
+ }
+
+ }
+ }
+
+ public async Task RefreshTokensIfAllowedAndNeeded()
+ {
+ logger.LogTrace("{method}()", nameof(RefreshTokensIfAllowedAndNeeded));
+ var tokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false);
+ if (tokens.Count < 1)
+ {
+ logger.LogError("No token found. Cannot refresh token.");
+ return;
+ }
+ var tokensToRefresh = tokens.Where(t => t.ExpiresAtUtc < (dateTimeProvider.UtcNow() + TimeSpan.FromMinutes(2))).ToList();
+ if (tokensToRefresh.Count < 1)
+ {
+ logger.LogTrace("No token needs to be refreshed.");
+ return;
+ }
+ //ToDo: needs to handle manual generated tokens. For now as soon as rate limits are introduced nobody gets refresh tokens even if they have a token not from www.teslasolarcharger.de
+ if (settings.AllowUnlimitedFleetApiRequests == false)
+ {
+ logger.LogError("Due to rate limitations fleet api requests are not allowed. As this version can not handle rate limits try updating to the latest version.");
+ return;
+ }
+
+ foreach (var tokenToRefresh in tokensToRefresh)
+ {
+ logger.LogWarning("Token {tokenId} needs to be refreshed as it expires on {expirationDateTime}", tokenToRefresh.Id, tokenToRefresh.ExpiresAtUtc);
+
+ //DO NOTE REMOVE *2: As normal requests could result in reaching max unauthorized count, the max value is higher here, so even if token is unauthorized, refreshing it is still tried a couple of times.
+ if (tokenToRefresh.UnauthorizedCounter > (constants.MaxTokenUnauthorizedCount * 2))
+ {
+ logger.LogError("Token {tokenId} has been unauthorized too often. Do not refresh token.", tokenToRefresh.Id);
+ continue;
+ }
+ using var httpClient = new HttpClient();
+ var tokenUrl = "https://auth.tesla.com/oauth2/v3/token";
+ var requestData = new Dictionary
+ {
+ { "grant_type", "refresh_token" },
+ { "client_id", configurationWrapper.FleetApiClientId() },
+ { "refresh_token", tokenToRefresh.RefreshToken },
+ };
+ var encodedContent = new FormUrlEncodedContent(requestData);
+ encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+ var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false);
+ var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ if (response.IsSuccessStatusCode)
+ {
+ }
+ else
{
await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi),
- $"Getting token from TscBackend. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false);
+ $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false);
+ logger.LogError("Refreshing token did result in non success status code. Response status code: {statusCode} Response string: {responseString}", response.StatusCode, responseString);
+ await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, tokenToRefresh, responseString).ConfigureAwait(false);
}
response.EnsureSuccessStatusCode();
- var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string.");
- await AddNewTokenAsync(newToken).ConfigureAwait(false);
+ var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string.");
+ tokenToRefresh.AccessToken = newToken.AccessToken;
+ tokenToRefresh.RefreshToken = newToken.RefreshToken;
+ tokenToRefresh.IdToken = newToken.IdToken;
+ tokenToRefresh.ExpiresAtUtc = dateTimeProvider.UtcNow().AddSeconds(newToken.ExpiresIn);
+ tokenToRefresh.UnauthorizedCounter = 0;
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
+ logger.LogInformation("New Token saved to database.");
}
- var dbToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false);
}
- private async Task CheckIfFleetApiRequestsAreAllowed()
+ public async Task RefreshFleetApiRequestsAreAllowed()
{
+ logger.LogTrace("{method}()", nameof(RefreshFleetApiRequestsAreAllowed));
if (settings.AllowUnlimitedFleetApiRequests && (settings.LastFleetApiRequestAllowedCheck > dateTimeProvider.UtcNow().AddHours(-1)))
{
- return true;
+ return;
}
settings.LastFleetApiRequestAllowedCheck = dateTimeProvider.UtcNow();
using var httpClient = new HttpClient();
@@ -629,15 +687,16 @@ private async Task CheckIfFleetApiRequestsAreAllowed()
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
- return true;
+ settings.AllowUnlimitedFleetApiRequests = true;
+ return;
}
var responseValue = JsonConvert.DeserializeObject>(responseString);
- return responseValue?.Value != false;
+ settings.AllowUnlimitedFleetApiRequests = responseValue?.Value != false;
}
catch (Exception)
{
- return true;
+ settings.AllowUnlimitedFleetApiRequests = true;
}
}
@@ -654,6 +713,7 @@ public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token)
IdToken = token.IdToken,
ExpiresAtUtc = dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn),
Region = token.Region,
+ UnauthorizedCounter = 0,
});
await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -669,13 +729,6 @@ public async Task> GetFleetApiTokenState()
{
return new DtoValue(FleetApiTokenState.NoApiRequestsAllowed);
}
- var isCurrentRefreshTokenUnauthorized = await teslaSolarChargerContext.TscConfigurations
- .Where(c => c.Key == constants.TokenRefreshUnauthorized)
- .AnyAsync().ConfigureAwait(false);
- if (isCurrentRefreshTokenUnauthorized)
- {
- return new DtoValue(FleetApiTokenState.TokenUnauthorized);
- }
var hasCurrentTokenMissingScopes = await teslaSolarChargerContext.TscConfigurations
.Where(c => c.Key == constants.TokenMissingScopes)
.AnyAsync().ConfigureAwait(false);
@@ -686,69 +739,49 @@ public async Task> GetFleetApiTokenState()
var token = await teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false);
if (token != null)
{
+ if (token.UnauthorizedCounter > constants.MaxTokenUnauthorizedCount)
+ {
+ return new DtoValue(FleetApiTokenState.TokenUnauthorized);
+ }
return new DtoValue(token.ExpiresAtUtc < dateTimeProvider.UtcNow() ? FleetApiTokenState.Expired : FleetApiTokenState.UpToDate);
}
- var tokenRequestedDateString = await teslaSolarChargerContext.TscConfigurations
- .Where(c => c.Key == constants.FleetApiTokenRequested)
- .Select(c => c.Value)
- .FirstOrDefaultAsync().ConfigureAwait(false);
- if (tokenRequestedDateString == null)
+ var tokenRequestedDate = await GetTokenRequestedDate().ConfigureAwait(false);
+ if (tokenRequestedDate == null)
{
return new DtoValue(FleetApiTokenState.NotRequested);
}
- var tokenRequestedDate = DateTime.Parse(tokenRequestedDateString, null, DateTimeStyles.RoundtripKind);
var currentDate = dateTimeProvider.UtcNow();
- if (tokenRequestedDate < currentDate.AddMinutes(-5))
+ if (tokenRequestedDate < (currentDate - constants.MaxTokenRequestWaitTime))
{
return new DtoValue(FleetApiTokenState.TokenRequestExpired);
}
return new DtoValue(FleetApiTokenState.NotReceived);
}
- private async Task GetAccessTokenAndRefreshWhenNeededAsync()
+ private async Task GetTokenRequestedDate()
{
- logger.LogTrace("{method}()", nameof(GetAccessTokenAndRefreshWhenNeededAsync));
+ var tokenRequestedDateString = await teslaSolarChargerContext.TscConfigurations
+ .Where(c => c.Key == constants.FleetApiTokenRequested)
+ .Select(c => c.Value)
+ .FirstOrDefaultAsync().ConfigureAwait(false);
+ if (tokenRequestedDateString == null)
+ {
+ return null;
+ }
+ var tokenRequestedDate = DateTime.Parse(tokenRequestedDateString, null, DateTimeStyles.RoundtripKind);
+ return tokenRequestedDate;
+ }
+
+ private async Task GetAccessToken()
+ {
+ logger.LogTrace("{method}()", nameof(GetAccessToken));
var token = await teslaSolarChargerContext.TeslaTokens
.OrderByDescending(t => t.ExpiresAtUtc)
.FirstAsync().ConfigureAwait(false);
- var minimumTokenLifeTime = TimeSpan.FromMinutes(5);
- var isCurrentRefreshTokenUnauthorized = await teslaSolarChargerContext.TscConfigurations
- .Where(c => c.Key == constants.TokenRefreshUnauthorized)
- .AnyAsync().ConfigureAwait(false);
- if (isCurrentRefreshTokenUnauthorized)
+ if (token.UnauthorizedCounter > constants.MaxTokenUnauthorizedCount)
{
- logger.LogError("Token is unauthorized");
- throw new InvalidDataException("Current Tesla Fleet Api Token is unauthorized");
- }
- if (token.ExpiresAtUtc < (dateTimeProvider.UtcNow() + minimumTokenLifeTime))
- {
- logger.LogInformation("Token is expired. Getting new token.");
- using var httpClient = new HttpClient();
- var tokenUrl = "https://auth.tesla.com/oauth2/v3/token";
- var requestData = new Dictionary
- {
- { "grant_type", "refresh_token" },
- { "client_id", configurationWrapper.FleetApiClientId() },
- { "refresh_token", token.RefreshToken },
- };
- var encodedContent = new FormUrlEncodedContent(requestData);
- encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
- var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false);
- var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi),
- $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}").ConfigureAwait(false);
- await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, token, responseString).ConfigureAwait(false);
- }
- response.EnsureSuccessStatusCode();
- var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string.");
- token.AccessToken = newToken.AccessToken;
- token.RefreshToken = newToken.RefreshToken;
- token.IdToken = newToken.IdToken;
- token.ExpiresAtUtc = dateTimeProvider.UtcNow().AddSeconds(newToken.ExpiresIn);
- await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
- logger.LogInformation("New Token saved to database.");
+ logger.LogError("Token unauthorized counter is too high. Request a new token.");
+ throw new InvalidOperationException("Token unauthorized counter is too high. Request a new token.");
}
return token;
}
@@ -760,17 +793,12 @@ private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode
if (statusCode == HttpStatusCode.Unauthorized)
{
logger.LogError(
- "Your token or refresh token is invalid. Very likely you have changed your Tesla password. Response: {responseString}", responseString);
- teslaSolarChargerContext.TeslaTokens.Remove(token);
- teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration()
- {
- Key = constants.TokenRefreshUnauthorized, Value = responseString,
- });
+ "Your token or refresh token is invalid. Very likely you have changed your Tesla password. Current unauthorized counter {unauthorizedCounter}, Should have been valid until: {expiresAt}, Response: {responseString}",
+ ++token.UnauthorizedCounter, token.ExpiresAtUtc, responseString);
}
else if (statusCode == HttpStatusCode.Forbidden)
{
logger.LogError("You did not select all scopes, so TSC can't send commands to your car. Response: {responseString}", responseString);
- teslaSolarChargerContext.TeslaTokens.Remove(token);
teslaSolarChargerContext.TscConfigurations.Add(new TscConfiguration()
{
Key = constants.TokenMissingScopes, Value = responseString,
diff --git a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
index 1e6ee379a..adaba2d09 100644
--- a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
+++ b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
@@ -33,9 +33,9 @@
-->
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -49,7 +49,7 @@
-
+