Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #1809

Merged
merged 14 commits into from
Feb 7, 2025
Merged

Develop #1809

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<MudPaper Class="d-flex justify-end flex-grow-1 gap-4 pr-2 mb-2" Elevation="0">
@if (IsDisabled && !string.IsNullOrEmpty(DisableToolTipText))
@if (IsDisabled && !string.IsNullOrEmpty(DisabledToolTipText))
{
<MudTooltip>
<ChildContent>
<MudButton Color="Color.Primary"
<MudButton Color="@ButtonColor"
Variant="Variant.Filled"
StartIcon="@StartIcon"
Disabled="true"
ButtonType="ButtonType">@ButtonText</MudButton>
</ChildContent>
<TooltipContent>
@DisableToolTipText
@DisabledToolTipText
</TooltipContent>
</MudTooltip>
}
else
{
<MudButton Disabled="IsLoading || IsDisabled"
Color="Color.Primary"
Color="@ButtonColor"
Variant="Variant.Filled"
StartIcon="@StartIcon"
OnClick="AddButtonClicked"
Expand All @@ -44,10 +44,13 @@
[Parameter]
public string StartIcon { get; set; }
[Parameter]
public string? DisableToolTipText { get; set; }
public string? DisabledToolTipText { get; set; }
[Parameter]
public bool IsLoading { get; set; }

[Parameter]
public Color ButtonColor { get; set; } = Color.Primary;

[Parameter]
public ButtonType ButtonType { get; set; } = ButtonType.Button;

Expand Down
2 changes: 1 addition & 1 deletion TeslaSolarCharger/Client/Pages/BaseConfiguration.razor
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ else
<GenericInput For="() => _dtoBaseConfiguration.SendStackTraceToTelegram"></GenericInput>
<RightAlignedButtonComponent ButtonText="Send test message"
IsDisabled="_telegramSettingsChanged"
DisableToolTipText="You need to save the configuration before testing it."
DisabledToolTipText="You need to save the configuration before testing it."
OnButtonClicked="_ => SendTelegramTestMessage()"></RightAlignedButtonComponent>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ErrorMessagingJob> logger, IErrorHandlingService service) : IJob
public class ErrorDetectionJob(ILogger<ErrorMessagingJob> logger, IErrorDetectionService service) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
Expand Down
1 change: 1 addition & 0 deletions TeslaSolarCharger/Server/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi
.AddTransient<IBackendNotificationService, BackendNotificationService>()
.AddTransient<ICarConfigurationService, CarConfigurationService>()
.AddTransient<IErrorHandlingService, ErrorHandlingService>()
.AddTransient<IErrorDetectionService, ErrorDetectionService>()
.AddTransient<ITeslaMateDbContextWrapper, TeslaMateDbContextWrapper>()
.AddTransient<ITeslaService, TeslaFleetApiService>()
.AddTransient<IPasswordGenerationService, PasswordGenerationService>()
Expand Down
1 change: 1 addition & 0 deletions TeslaSolarCharger/Server/Services/BackendApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TeslaSolarCharger.Server.Services.Contracts;

public interface IErrorDetectionService
{
Task DetectErrors();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Task HandleError(string source, string methodName, string headline, string messa
Task HandleErrorResolved(string issueKey, string? vin);
Task SendTelegramMessages();
Task<Fin<List<DtoLoggedError>>> GetActiveLoggedErrors();
Task DetectErrors();
Task<DtoValue<int>> ErrorCount();
Task<DtoValue<int>> WarningCount();
Task<Fin<int>> DismissError(int errorIdValue);
Expand Down
195 changes: 195 additions & 0 deletions TeslaSolarCharger/Server/Services/ErrorDetectionService.cs
Original file line number Diff line number Diff line change
@@ -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<ErrorDetectionService> 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: <code>{settings.StartupCrashMessage}</code>", 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<T> 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<LoggedError> 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 <a href=\"/cloudconnection\">Cloud Connection</a> 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 <a href=\"/cloudconnection\">Cloud Connection</a> 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 <a href=\"/cloudconnection\">Cloud Connection</a> 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 <a href=\"/cloudconnection\">Cloud Connection</a> 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 <a href=\"/cloudconnection\">Cloud Connection</a> 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<LoggedError> 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);
}

}
Loading
Loading