From 61e5038f36cd952dc823890f81befb0567b5c7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 1 Feb 2025 11:09:04 +0100 Subject: [PATCH 1/8] fix(ErrorHandlingService): fix typo --- TeslaSolarCharger/Server/Services/ErrorHandlingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index 8042bd854..81d8b992d 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -433,7 +433,7 @@ private async Task DetectTokenStateIssues(List activeErrors) logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); var backendTokenState = await tokenHelper.GetBackendTokenState(true); var fleetApiTokenState = await tokenHelper.GetFleetApiTokenState(true); - await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "Backen API Token not up to date", + await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "Backend API Token not up to date", "You are currently not connected to the backend. Open the Cloud Connection and request a new token.", backendTokenState != TokenState.UpToDate).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", From e6e9d3b7ce42bf3e0889cd1bd864cffd5170e2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 2 Feb 2025 10:58:15 +0100 Subject: [PATCH 2/8] fix(TeslaFleetApiService): check if fleet api can be used as fallback was not correct --- TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 293744a0e..1db6d9992 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -120,7 +120,7 @@ public async Task WakeUpCar(int carId, bool isFleetApiTest) { logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); var car = settings.Cars.First(c => c.Id == carId); - var result = await SendCommandToTeslaApi(car.Vin, WakeUpRequest, null, true).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(car.Vin, WakeUpRequest, null, isFleetApiTest).ConfigureAwait(false); if (car.TeslaMateCarId != default) { //ToDo: fix with https://github.com/pkuehnel/TeslaSolarCharger/issues/1511 @@ -949,7 +949,9 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } } - if (!isFleetApiTest && fleetApiRequest.RequestUrl != VehicleRequest.RequestUrl && (!await backendApiService.IsFleetApiLicensed(car.Vin, true))) + if (!isFleetApiTest + && (fleetApiRequest.RequestUrl != VehicleRequest.RequestUrl) + && (!await backendApiService.IsFleetApiLicensed(car.Vin, true))) { await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Fleet API not licensed for car {car.Vin}", "Can not send Fleet API commands to car as Fleet API is not licensed", From 7cff4db89fcc32830514d06f47577362e4281576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 5 Feb 2025 00:23:00 +0100 Subject: [PATCH 3/8] fix(chore): minor ui issues --- .../Components/RightAlignedButtonComponent.razor | 13 ++++++++----- .../Client/Pages/BaseConfiguration.razor | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TeslaSolarCharger/Client/Components/RightAlignedButtonComponent.razor b/TeslaSolarCharger/Client/Components/RightAlignedButtonComponent.razor index 35dc60bbb..88f68343b 100644 --- a/TeslaSolarCharger/Client/Components/RightAlignedButtonComponent.razor +++ b/TeslaSolarCharger/Client/Components/RightAlignedButtonComponent.razor @@ -1,23 +1,23 @@  - @if (IsDisabled && !string.IsNullOrEmpty(DisableToolTipText)) + @if (IsDisabled && !string.IsNullOrEmpty(DisabledToolTipText)) { - @ButtonText - @DisableToolTipText + @DisabledToolTipText } else { From f3f19a3e52e00f5e4124ae69ea75bff1f6455868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 5 Feb 2025 00:23:47 +0100 Subject: [PATCH 4/8] fix(CarBasicConfigurationValidator): allow min and mux current to be the same --- TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs index e881dc33a..afe532666 100644 --- a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs +++ b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs @@ -57,7 +57,7 @@ public CarBasicConfigurationValidator() RuleFor(x => x.MaximumAmpere).GreaterThan(0); RuleFor(x => x.MaximumAmpere).LessThanOrEqualTo(64); RuleFor(x => x) - .Must(config => config.MaximumAmpere > config.MinimumAmpere) + .Must(config => config.MaximumAmpere >= config.MinimumAmpere) .WithMessage("MaximumAmpere must be greater than MinimumAmpere."); RuleFor(x => x.UsableEnergy).GreaterThan(5); RuleFor(x => x.ChargingPriority).GreaterThan(0); From a4680e7b48535d28376660952fc2a5bbc4a2fbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 29 Jan 2025 00:58:38 +0100 Subject: [PATCH 5/8] feat(FleetTelemetryWebSocketService): do not try to connect to fleet telemetry if not licensed --- .../Services/FleetTelemetryWebSocketService.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index 9dd6496e5..efad60530 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -24,7 +24,8 @@ public class FleetTelemetryWebSocketService( IConfigurationWrapper configurationWrapper, IDateTimeProvider dateTimeProvider, IServiceProvider serviceProvider, - ISettings settings) : IFleetTelemetryWebSocketService + ISettings settings, + IBackendApiService backendApiService) : IFleetTelemetryWebSocketService { private readonly TimeSpan _heartbeatsendTimeout = TimeSpan.FromSeconds(5); @@ -52,6 +53,11 @@ public async Task ReconnectWebSocketsForEnabledCars() && (c.IsFleetTelemetryHardwareIncompatible == false)) .Select(c => new { c.Vin, IncludeTrackingRelevantFields = c.IncludeTrackingRelevantFields, }) .ToListAsync(); + if (cars.Any() && (!await backendApiService.IsBaseAppLicensed(true))) + { + logger.LogWarning("Base App is not licensed, do not connect to Fleet Telemetry"); + return; + } var bytesToSend = Encoding.UTF8.GetBytes("Heartbeat"); foreach (var car in cars) { @@ -60,6 +66,11 @@ public async Task ReconnectWebSocketsForEnabledCars() continue; } + if (car.IncludeTrackingRelevantFields && (!await backendApiService.IsFleetApiLicensed(car.Vin, true))) + { + logger.LogWarning("Car {vin} is not licensed for Fleet API, do not connect as IncludeTrackingRelevant fields is enabled", car.Vin); + continue; + } var existingClient = Clients.FirstOrDefault(c => c.Vin == car.Vin); if (existingClient != default) { From 2afbadeee4200c8073bc2b90e98d57f6e5bead9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 5 Feb 2025 21:29:20 +0100 Subject: [PATCH 6/8] refactor(ErrorDetectionService): use separate service for auto error detection --- .../Scheduling/Jobs/ErrorDetectionJob.cs | 3 +- .../Server/ServiceCollectionExtensions.cs | 1 + .../Contracts/IErrorDetectionService.cs | 6 + .../Contracts/IErrorHandlingService.cs | 1 - .../Server/Services/ErrorDetectionService.cs | 195 ++++++++++++++++++ .../Server/Services/ErrorHandlingService.cs | 179 +--------------- 6 files changed, 206 insertions(+), 179 deletions(-) create mode 100644 TeslaSolarCharger/Server/Services/Contracts/IErrorDetectionService.cs create mode 100644 TeslaSolarCharger/Server/Services/ErrorDetectionService.cs diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/ErrorDetectionJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/ErrorDetectionJob.cs index 5e8bc11ee..f0aaac093 100644 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/ErrorDetectionJob.cs +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/ErrorDetectionJob.cs @@ -1,10 +1,11 @@ using Quartz; +using TeslaSolarCharger.Server.Services; using TeslaSolarCharger.Server.Services.Contracts; namespace TeslaSolarCharger.Server.Scheduling.Jobs; [DisallowConcurrentExecution] -public class ErrorDetectionJob(ILogger logger, IErrorHandlingService service) : IJob +public class ErrorDetectionJob(ILogger logger, IErrorDetectionService service) : IJob { public async Task Execute(IJobExecutionContext context) { diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 125932edd..e008e785c 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -111,6 +111,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/Contracts/IErrorDetectionService.cs b/TeslaSolarCharger/Server/Services/Contracts/IErrorDetectionService.cs new file mode 100644 index 000000000..01940379c --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/IErrorDetectionService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface IErrorDetectionService +{ + Task DetectErrors(); +} diff --git a/TeslaSolarCharger/Server/Services/Contracts/IErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/Contracts/IErrorHandlingService.cs index 2f27738cb..f7d1f7e17 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IErrorHandlingService.cs @@ -13,7 +13,6 @@ Task HandleError(string source, string methodName, string headline, string messa Task HandleErrorResolved(string issueKey, string? vin); Task SendTelegramMessages(); Task>> GetActiveLoggedErrors(); - Task DetectErrors(); Task> ErrorCount(); Task> WarningCount(); Task> DismissError(int errorIdValue); diff --git a/TeslaSolarCharger/Server/Services/ErrorDetectionService.cs b/TeslaSolarCharger/Server/Services/ErrorDetectionService.cs new file mode 100644 index 000000000..83527632b --- /dev/null +++ b/TeslaSolarCharger/Server/Services/ErrorDetectionService.cs @@ -0,0 +1,195 @@ +using Microsoft.EntityFrameworkCore; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Resources.PossibleIssues.Contracts; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos.Contracts; +using TeslaSolarCharger.Shared.Enums; +using TeslaSolarCharger.Shared.Resources.Contracts; +using TeslaSolarCharger.SharedModel.Enums; + +namespace TeslaSolarCharger.Server.Services; + +public class ErrorDetectionService(ILogger logger, + IErrorHandlingService errorHandlingService, + IDateTimeProvider dateTimeProvider, + ISettings settings, + ITeslaSolarChargerContext context, + IConfigurationWrapper configurationWrapper, + IIssueKeys issueKeys, + ITokenHelper tokenHelper, + IConstants constants, + IFleetTelemetryWebSocketService fleetTelemetryWebSocketService) : IErrorDetectionService +{ + public async Task DetectErrors() + { + var activeErrors = await context.LoggedErrors + .Where(e => e.EndTimeStamp == default) + .ToListAsync().ConfigureAwait(false); + foreach (var error in activeErrors) + { + if (error.Vin == null || settings.CarsToManage.Any(c => c.Vin == error.Vin)) + { + continue; + } + logger.LogDebug("Remove error with ID {id} as it belongs to a car that should not be managed.", error.Id); + error.EndTimeStamp = dateTimeProvider.UtcNow(); + } + await context.SaveChangesAsync().ConfigureAwait(false); + + await AddOrRemoveErrors(activeErrors, issueKeys.RestartNeeded, "TSC restart needed", + "Due to configuration changes a restart of TSC is needed.", settings.RestartNeeded).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.CrashedOnStartup, "TSC crashed on startup", + $"Exeption Message: {settings.StartupCrashMessage}", settings.CrashedOnStartup).ConfigureAwait(false); + + + var pvValueUpdateAge = dateTimeProvider.DateTimeOffSetUtcNow() - settings.LastPvValueUpdate; + var solarValuesTooOld = (pvValueUpdateAge > (configurationWrapper.PvValueJobUpdateIntervall() * 3)) && ( + await context.ModbusResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false) + || await context.RestValueResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false) + || await context.MqttResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false)); + await AddOrRemoveErrors(activeErrors, issueKeys.SolarValuesNotAvailable, "Solar values are not available", + $"Solar values are {pvValueUpdateAge} old. It looks like there is something wrong when trying to get the solar values.", solarValuesTooOld).ConfigureAwait(false); + + //ToDO: fix next line, currently not working due to cyclic reference + //await AddOrRemoveErrors(activeErrors, issueKeys.BaseAppNotLicensed, "Base App not licensed", + // "Can not send commands to car as app is not licensed", !await backendApiService.IsBaseAppLicensed(true)); + + //ToDo: if last check there was no token related issue, only detect token related issues every x minutes as creates high load in backend + await DetectTokenStateIssues(activeErrors); + foreach (var car in settings.CarsToManage) + { + if ((car.LastNonSuccessBleCall != default) + && (car.LastNonSuccessBleCall.Value > (dateTimeProvider.UtcNow() - configurationWrapper.BleUsageStopAfterError()))) + { + //Issue should already be active as is set on TeslaFleetApiService. + //Note: The same logic for the if is used in TeslaFleetApiService.SendCommandToTeslaApi if ble is enabled. + //So: let it be like that even though the if part is empty. + } + else + { + //ToDo: In a future release this should only be done if no fleet api request was sent the last x minutes (BleUsageStopAfterError) + await errorHandlingService.HandleErrorResolved(issueKeys.UsingFleetApiAsBleFallback, car.Vin); + } + var fleetTelemetryEnabled = await context.Cars + .Where(c => c.Vin == car.Vin) + .Select(c => c.UseFleetTelemetry) + .FirstOrDefaultAsync(); + + if (fleetTelemetryEnabled && (!fleetTelemetryWebSocketService.IsClientConnected(car.Vin))) + { + await errorHandlingService.HandleError(nameof(ErrorHandlingService), nameof(DetectErrors), $"Fleet Telemetry not connected for car {car.Vin}", + "Fleet telemetry is not connected. Please check the connection.", issueKeys.FleetTelemetryNotConnected, car.Vin, null); + } + else + { + await errorHandlingService.HandleErrorResolved(issueKeys.FleetTelemetryNotConnected, car.Vin); + } + + if (car.State is CarStateEnum.Asleep or CarStateEnum.Offline) + { + await errorHandlingService.HandleErrorResolved(issueKeys.GetVehicleData, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.VehicleDataRequestUrl, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.VehicleDataRequestUrl, car.Vin); + } + + if (car.State != CarStateEnum.Asleep && car.State != CarStateEnum.Offline && car.State != CarStateEnum.Unknown) + { + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.WakeUpRequestUrl, car.Vin); + } + if (car.State is CarStateEnum.Charging) + { + await errorHandlingService.HandleErrorResolved(issueKeys.BleCommandNoSuccess + constants.ChargeStartRequestUrl, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.ChargeStartRequestUrl, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.ChargeStartRequestUrl, car.Vin); + } + else + { + await errorHandlingService.HandleErrorResolved(issueKeys.BleCommandNoSuccess + constants.ChargeStopRequestUrl, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.ChargeStopRequestUrl, car.Vin); + await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.ChargeStopRequestUrl, car.Vin); + } + } + } + + private async Task DetectTokenStateIssues(List activeErrors) + { + logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); + var backendTokenState = await tokenHelper.GetBackendTokenState(true); + var fleetApiTokenState = await tokenHelper.GetFleetApiTokenState(true); + await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "Backend API Token not up to date", + "You are currently not connected to the backend. Open the Cloud Connection and request a new token.", + backendTokenState != TokenState.UpToDate).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", + "You recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Cloud Connection and request a new token. Important: You need to allow access to all selectable scopes.", + fleetApiTokenState == TokenState.Unauthorized).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.NoFleetApiToken, "No Fleet API Token available.", + "Open the Cloud Connection and request a new token.", + fleetApiTokenState == TokenState.NotAvailable).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Fleet API token is expired", + "Either you recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Cloud Connection and request a new token. Important: You need to allow access to all selectable scopes.", + fleetApiTokenState == TokenState.Expired).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenMissingScopes, "Your Tesla token has missing scopes.", + "Open the Cloud Connection and request a new token. Note: You need to allow all selectable scopes as otherwise TSC won't work properly.", + fleetApiTokenState == TokenState.MissingScopes).ConfigureAwait(false); + + //Remove all fleet api related issue keys on token error because very likely it is because of the underlaying token issue. + if (fleetApiTokenState != TokenState.UpToDate) + { + foreach (var activeError in activeErrors.Where(activeError => activeError.IssueKey.StartsWith(issueKeys.GetVehicleData) + || activeError.IssueKey.StartsWith(issueKeys.CarStateUnknown) + || activeError.IssueKey.StartsWith(issueKeys.FleetApiNonSuccessStatusCode) + || activeError.IssueKey.StartsWith(issueKeys.FleetApiNonSuccessResult) + || activeError.IssueKey.StartsWith(issueKeys.UnsignedCommand))) + { + activeError.EndTimeStamp = dateTimeProvider.UtcNow(); + } + + await context.SaveChangesAsync(); + } + } + + private async Task AddOrRemoveErrors(List activeErrors, string issueKey, string headline, string message, bool shouldBeActive) + { + var filteredErrors = activeErrors.Where(e => e.IssueKey == issueKey).ToList(); + if (shouldBeActive && filteredErrors.Count < 1) + { + var loggedError = new LoggedError() + { + StartTimeStamp = dateTimeProvider.UtcNow(), + IssueKey = issueKey, + Source = nameof(ErrorHandlingService), + MethodName = nameof(AddOrRemoveErrors), + Headline = headline, + Message = message, + }; + context.LoggedErrors.Add(loggedError); + } + else if (shouldBeActive) + { + for (var i = 0; i < filteredErrors.Count; i++) + { + if (i == 0) + { + filteredErrors[i].FurtherOccurrences.Add(dateTimeProvider.UtcNow()); + } + else + { + logger.LogWarning("More than one error with issue key {issueKey} was active", issueKey); + filteredErrors[i].EndTimeStamp = dateTimeProvider.UtcNow(); + } + } + } + else if (!shouldBeActive && filteredErrors.Count > 0) + { + foreach (var filteredError in filteredErrors) + { + filteredError.EndTimeStamp = dateTimeProvider.UtcNow(); + } + } + + await context.SaveChangesAsync().ConfigureAwait(false); + } + +} diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index 81d8b992d..d5b1f49d7 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -8,11 +8,8 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; -using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Dtos.LoggedError; using TeslaSolarCharger.Shared.Enums; -using TeslaSolarCharger.Shared.Resources.Contracts; -using TeslaSolarCharger.SharedModel.Enums; using Error = LanguageExt.Common.Error; namespace TeslaSolarCharger.Server.Services; @@ -23,11 +20,7 @@ public class ErrorHandlingService(ILogger logger, ITeslaSolarChargerContext context, IDateTimeProvider dateTimeProvider, IConfigurationWrapper configurationWrapper, - ISettings settings, - ITokenHelper tokenHelper, - IPossibleIssues possibleIssues, - IConstants constants, - IFleetTelemetryWebSocketService fleetTelemetryWebSocketService) : IErrorHandlingService + IPossibleIssues possibleIssues) : IErrorHandlingService { public async Task>> GetActiveLoggedErrors() { @@ -61,7 +54,7 @@ public async Task>> GetActiveLoggedErrors() }); } - + public async Task>> GetHiddenErrors() { logger.LogTrace("{method}()", nameof(GetHiddenErrors)); @@ -104,97 +97,6 @@ public async Task>> GetHiddenErrors() }); } - public async Task DetectErrors() - { - var activeErrors = await context.LoggedErrors - .Where(e => e.EndTimeStamp == default) - .ToListAsync().ConfigureAwait(false); - foreach (var error in activeErrors) - { - if (error.Vin == null || settings.CarsToManage.Any(c => c.Vin == error.Vin)) - { - continue; - } - logger.LogDebug("Remove error with ID {id} as it belongs to a car that should not be managed.", error.Id); - error.EndTimeStamp = dateTimeProvider.UtcNow(); - } - await context.SaveChangesAsync().ConfigureAwait(false); - - await AddOrRemoveErrors(activeErrors, issueKeys.RestartNeeded, "TSC restart needed", - "Due to configuration changes a restart of TSC is needed.", settings.RestartNeeded).ConfigureAwait(false); - - await AddOrRemoveErrors(activeErrors, issueKeys.CrashedOnStartup, "TSC crashed on startup", - $"Exeption Message: {settings.StartupCrashMessage}", settings.CrashedOnStartup).ConfigureAwait(false); - - - var pvValueUpdateAge = dateTimeProvider.DateTimeOffSetUtcNow() - settings.LastPvValueUpdate; - var solarValuesTooOld = (pvValueUpdateAge > (configurationWrapper.PvValueJobUpdateIntervall() * 3)) &&( - await context.ModbusResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false) - || await context.RestValueResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false) - || await context.MqttResultConfigurations.Where(r => r.UsedFor <= ValueUsage.HomeBatterySoc).AnyAsync().ConfigureAwait(false)); - await AddOrRemoveErrors(activeErrors, issueKeys.SolarValuesNotAvailable, "Solar values are not available", - $"Solar values are {pvValueUpdateAge} old. It looks like there is something wrong when trying to get the solar values.", solarValuesTooOld).ConfigureAwait(false); - - //ToDO: fix next line, currently not working due to cyclic reference - //await AddOrRemoveErrors(activeErrors, issueKeys.BaseAppNotLicensed, "Base App not licensed", - // "Can not send commands to car as app is not licensed", !await backendApiService.IsBaseAppLicensed(true)); - - //ToDo: if last check there was no token related issue, only detect token related issues every x minutes as creates high load in backend - await DetectTokenStateIssues(activeErrors); - foreach (var car in settings.CarsToManage) - { - if ((car.LastNonSuccessBleCall != default) - && (car.LastNonSuccessBleCall.Value > (dateTimeProvider.UtcNow() - configurationWrapper.BleUsageStopAfterError()))) - { - //Issue should already be active as is set on TeslaFleetApiService. - //Note: The same logic for the if is used in TeslaFleetApiService.SendCommandToTeslaApi if ble is enabled. - //So: let it be like that even though the if part is empty. - } - else - { - //ToDo: In a future release this should only be done if no fleet api request was sent the last x minutes (BleUsageStopAfterError) - await HandleErrorResolved(issueKeys.UsingFleetApiAsBleFallback, car.Vin); - } - var fleetTelemetryEnabled = await context.Cars - .Where(c => c.Vin == car.Vin) - .Select(c => c.UseFleetTelemetry) - .FirstOrDefaultAsync(); - - if (fleetTelemetryEnabled && (!fleetTelemetryWebSocketService.IsClientConnected(car.Vin))) - { - await HandleError(nameof(ErrorHandlingService), nameof(DetectErrors), $"Fleet Telemetry not connected for car {car.Vin}", - "Fleet telemetry is not connected. Please check the connection.", issueKeys.FleetTelemetryNotConnected, car.Vin, null); - } - else - { - await HandleErrorResolved(issueKeys.FleetTelemetryNotConnected, car.Vin); - } - - if (car.State is CarStateEnum.Asleep or CarStateEnum.Offline) - { - await HandleErrorResolved(issueKeys.GetVehicleData, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.VehicleDataRequestUrl, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.VehicleDataRequestUrl, car.Vin); - } - - if (car.State != CarStateEnum.Asleep && car.State != CarStateEnum.Offline && car.State != CarStateEnum.Unknown) - { - await HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.WakeUpRequestUrl, car.Vin); - } - if (car.State is CarStateEnum.Charging) - { - await HandleErrorResolved(issueKeys.BleCommandNoSuccess + constants.ChargeStartRequestUrl, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.ChargeStartRequestUrl, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.ChargeStartRequestUrl, car.Vin); - } - else - { - await HandleErrorResolved(issueKeys.BleCommandNoSuccess + constants.ChargeStopRequestUrl, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + constants.ChargeStopRequestUrl, car.Vin); - await HandleErrorResolved(issueKeys.FleetApiNonSuccessResult + constants.ChargeStopRequestUrl, car.Vin); - } - } - } public async Task> ErrorCount() { @@ -428,43 +330,6 @@ private List GetIssueKeysUsingReflection() return keys!; } - private async Task DetectTokenStateIssues(List activeErrors) - { - logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); - var backendTokenState = await tokenHelper.GetBackendTokenState(true); - var fleetApiTokenState = await tokenHelper.GetFleetApiTokenState(true); - await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "Backend API Token not up to date", - "You are currently not connected to the backend. Open the Cloud Connection and request a new token.", - backendTokenState != TokenState.UpToDate).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", - "You recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Cloud Connection and request a new token. Important: You need to allow access to all selectable scopes.", - fleetApiTokenState == TokenState.Unauthorized).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.NoFleetApiToken, "No Fleet API Token available.", - "Open the Cloud Connection and request a new token.", - fleetApiTokenState == TokenState.NotAvailable).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Fleet API token is expired", - "Either you recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Cloud Connection and request a new token. Important: You need to allow access to all selectable scopes.", - fleetApiTokenState == TokenState.Expired).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenMissingScopes, "Your Tesla token has missing scopes.", - "Open the Cloud Connection and request a new token. Note: You need to allow all selectable scopes as otherwise TSC won't work properly.", - fleetApiTokenState == TokenState.MissingScopes).ConfigureAwait(false); - - //Remove all fleet api related issue keys on token error because very likely it is because of the underlaying token issue. - if (fleetApiTokenState != TokenState.UpToDate) - { - foreach (var activeError in activeErrors.Where(activeError => activeError.IssueKey.StartsWith(issueKeys.GetVehicleData) - || activeError.IssueKey.StartsWith(issueKeys.CarStateUnknown) - || activeError.IssueKey.StartsWith(issueKeys.FleetApiNonSuccessStatusCode) - || activeError.IssueKey.StartsWith(issueKeys.FleetApiNonSuccessResult) - || activeError.IssueKey.StartsWith(issueKeys.UnsignedCommand))) - { - activeError.EndTimeStamp = dateTimeProvider.UtcNow(); - } - - await context.SaveChangesAsync(); - } - } - private async Task GetActiveIssueCountBySeverity(IssueSeverity severity) { var activeIssueKeys = await context.LoggedErrors @@ -476,45 +341,5 @@ private async Task GetActiveIssueCountBySeverity(IssueSeverity severity) return activeIssueKeys.Count(activeIssueKey => possibleIssues.GetIssueByKey(activeIssueKey.IssueKey).IssueSeverity == severity); } - private async Task AddOrRemoveErrors(List activeErrors, string issueKey, string headline, string message, bool shouldBeActive) - { - var filteredErrors = activeErrors.Where(e => e.IssueKey == issueKey).ToList(); - if (shouldBeActive && filteredErrors.Count < 1) - { - var loggedError = new LoggedError() - { - StartTimeStamp = dateTimeProvider.UtcNow(), - IssueKey = issueKey, - Source = nameof(ErrorHandlingService), - MethodName = nameof(DetectErrors), - Headline = headline, - Message = message, - }; - context.LoggedErrors.Add(loggedError); - } - else if (shouldBeActive) - { - for (var i = 0; i < filteredErrors.Count; i++) - { - if (i == 0) - { - filteredErrors[i].FurtherOccurrences.Add(dateTimeProvider.UtcNow()); - } - else - { - logger.LogWarning("More than one error with issue key {issueKey} was active", issueKey); - filteredErrors[i].EndTimeStamp = dateTimeProvider.UtcNow(); - } - } - } - else if (!shouldBeActive && filteredErrors.Count > 0) - { - foreach (var filteredError in filteredErrors) - { - filteredError.EndTimeStamp = dateTimeProvider.UtcNow(); - } - } - await context.SaveChangesAsync().ConfigureAwait(false); - } } From 228c9490b4a493f2c5a1e634e8d10fb52ecd4d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 5 Feb 2025 21:40:16 +0100 Subject: [PATCH 7/8] fix(TeslaBleService): only check ble container on enabled cars --- TeslaSolarCharger/Server/Services/TeslaBleService.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/TeslaBleService.cs b/TeslaSolarCharger/Server/Services/TeslaBleService.cs index fa1fe9e97..a0cd0f16d 100644 --- a/TeslaSolarCharger/Server/Services/TeslaBleService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaBleService.cs @@ -200,8 +200,11 @@ public async Task CheckBleApiVersionCompatibilities() } var url = baseUrl + "Hello/TscVersionCompatibility"; using var client = new HttpClient(); - client.Timeout = TimeSpan.FromSeconds(3); - var vins = settings.Cars.Where(c => c.BleApiBaseUrl == host && c.UseBle).Select(c => c.Vin).ToList(); + client.Timeout = TimeSpan.FromSeconds(5); + var vins = settings.Cars + .Where(c => c.BleApiBaseUrl == host && c.UseBle && (c.ShouldBeManaged == true)) + .Select(c => c.Vin) + .ToList(); try { var response = await client.GetAsync(url).ConfigureAwait(false); From 8fe794ce8d92cebc8a0db7251d78d5ca22eb9f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Thu, 6 Feb 2025 17:51:51 +0100 Subject: [PATCH 8/8] fix scopes in fleet telemetry connection service --- .../Server/Services/BackendApiService.cs | 1 + .../Services/FleetTelemetryWebSocketService.cs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 7998bfa82..77b954d21 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -153,6 +153,7 @@ await errorHandlingService.HandleError(nameof(BackendApiService), nameof(Refresh token.RefreshToken = newToken.RefreshToken; token.ExpiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(newToken.ExpiresAt); await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + logger.LogInformation("Backend token refreshed."); memoryCache.Remove(constants.BackendTokenStateKey); } diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index efad60530..6f314ba29 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -21,11 +21,7 @@ namespace TeslaSolarCharger.Server.Services; public class FleetTelemetryWebSocketService( ILogger logger, - IConfigurationWrapper configurationWrapper, - IDateTimeProvider dateTimeProvider, - IServiceProvider serviceProvider, - ISettings settings, - IBackendApiService backendApiService) : IFleetTelemetryWebSocketService + IServiceProvider serviceProvider) : IFleetTelemetryWebSocketService { private readonly TimeSpan _heartbeatsendTimeout = TimeSpan.FromSeconds(5); @@ -44,6 +40,8 @@ public async Task ReconnectWebSocketsForEnabledCars() logger.LogTrace("{method}", nameof(ReconnectWebSocketsForEnabledCars)); var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); + var backendApiService = scope.ServiceProvider.GetRequiredService(); + var dateTimeProvider = scope.ServiceProvider.GetRequiredService(); var cars = await context.Cars .Where(c => c.UseFleetTelemetry && (c.ShouldBeManaged == true) @@ -131,11 +129,13 @@ await client.WebSocketClient private async Task ConnectToFleetTelemetryApi(string vin, bool includeTrackingRelevantFields) { logger.LogTrace("{method}({carId})", nameof(ConnectToFleetTelemetryApi), vin); - var currentDate = dateTimeProvider.UtcNow(); var scope = serviceProvider.CreateScope(); + var dateTimeProvider = scope.ServiceProvider.GetRequiredService(); + var configurationWrapper = scope.ServiceProvider.GetRequiredService(); var context = scope.ServiceProvider.GetRequiredService(); var tscConfigurationService = scope.ServiceProvider.GetRequiredService(); var constants = scope.ServiceProvider.GetRequiredService(); + var currentDate = dateTimeProvider.UtcNow(); var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); if (decryptionKey == default) { @@ -202,6 +202,9 @@ private async Task ReceiveMessages(DtoFleetTelemetryWebSocketClients client, str try { // Receive message from the WebSocket server + var scope = serviceProvider.CreateScope(); + var dateTimeProvider = scope.ServiceProvider.GetRequiredService(); + var configurationWrapper = scope.ServiceProvider.GetRequiredService(); logger.LogTrace("Waiting for new fleet telemetry message for car {vin}", vin); var result = await client.WebSocketClient.ReceiveAsync(new(buffer), client.CancellationToken); logger.LogTrace("Received new fleet telemetry message for car {vin}", vin); @@ -249,7 +252,7 @@ private async Task ReceiveMessages(DtoFleetTelemetryWebSocketClients client, str logger.LogDebug("Save location message for car {carId}", carId); } - var scope = serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); var carValueLog = new CarValueLog { @@ -268,6 +271,7 @@ private async Task ReceiveMessages(DtoFleetTelemetryWebSocketClients client, str await context.SaveChangesAsync().ConfigureAwait(false); if (configurationWrapper.GetVehicleDataFromTesla()) { + var settings = scope.ServiceProvider.GetRequiredService(); var settingsCar = settings.Cars.First(c => c.Vin == vin); string? propertyName = null; switch (message.Type)