diff --git a/NissanConnect/NissanConnectLib.Example/Configuration.cs b/NissanConnect/NissanConnectLib.Example/Configuration.cs new file mode 100644 index 0000000..24170a5 --- /dev/null +++ b/NissanConnect/NissanConnectLib.Example/Configuration.cs @@ -0,0 +1,9 @@ +namespace NissanConnectLib.Example; + +internal class Configuration +{ + public string TokenCacheFile { get; set; } = "token.cache"; + public string? Username { get; set; } + public string? Password { get; set; } + public bool ForceBatteryStatusRefresh { get; set; } = false; +} diff --git a/NissanConnect/NissanConnectLib.Example/CustomConsoleFormatter.cs b/NissanConnect/NissanConnectLib.Example/CustomConsoleFormatter.cs new file mode 100644 index 0000000..77ae096 --- /dev/null +++ b/NissanConnect/NissanConnectLib.Example/CustomConsoleFormatter.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; + +namespace NissanConnectLib.Example; + +internal class CustomConsoleFormatter : ConsoleFormatter +{ + public CustomConsoleFormatter() : base(nameof(CustomConsoleFormatter)) { } + + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) + { + var message = logEntry.Formatter(logEntry.State, logEntry.Exception); + + if (string.IsNullOrEmpty(message)) + { + return; + } + + textWriter.WriteLine($"[{DateTime.Now}] [{logEntry.LogLevel,11}] {message}"); + + if (logEntry.Exception is not null) + { + textWriter.WriteLine($"[{DateTime.Now}] [{logEntry.LogLevel,11}] {logEntry.Exception.Message}"); + } + } +} diff --git a/NissanConnect/NissanConnectLib.Example/NissanConnectHostedService.cs b/NissanConnect/NissanConnectLib.Example/NissanConnectHostedService.cs new file mode 100644 index 0000000..78a3698 --- /dev/null +++ b/NissanConnect/NissanConnectLib.Example/NissanConnectHostedService.cs @@ -0,0 +1,169 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; +using NissanConnectLib.Api; +using NissanConnectLib.Models; +using System.Text.Json; + +namespace NissanConnectLib.Example; + +internal class NissanConnectHostedService : IHostedService +{ + private readonly NissanConnectClient _ncc; + private readonly Configuration _config; + private readonly ILogger _logger; + + public NissanConnectHostedService( + NissanConnectClient ncc, + Configuration config, + ILogger logger) + { + _ncc = ncc; + _config = config; + _logger = logger; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "")] + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + await RunExample(); + } + catch (Exception e) + { + _logger.LogError(e, "Error running example"); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping Nissan Connect Hosted Service"); + return Task.CompletedTask; + } + + private async Task RunExample() + { + var loggedIn = false; + + // Save token to cache file when refreshed + _ncc.AccessTokenRefreshed += (sender, token) => + { + _logger.LogInformation("Access token refreshed!"); + File.WriteAllText(_config.TokenCacheFile, JsonSerializer.Serialize(token)); + }; + + // Try to use token cache file + if (File.Exists(_config.TokenCacheFile)) + { + var cachedToken = JsonSerializer.Deserialize(File.ReadAllText(_config.TokenCacheFile)); + _ncc.AccessToken = cachedToken; + + if (await _ncc.GetUserId() is null) + { + _logger.LogWarning("Could not get user ID using cached token, deleting cache file..."); + File.Delete(_config.TokenCacheFile); + } + else + { + _logger.LogInformation("Cached token is valid!"); + loggedIn = true; + } + } + + // Log in using username and password + if (!loggedIn) + { + // Are we missing arguments? + if (string.IsNullOrEmpty(_config.Username) || string.IsNullOrEmpty(_config.Password)) + { + _logger.LogError("Configuration is missing. Specify username and password"); + return; + } + + // Log in using a username and password + loggedIn = await _ncc.LogIn(_config.Username, _config.Password); + if (loggedIn) + { + _logger.LogInformation("Logged in using username and password. Writing token to cache file..."); + File.WriteAllText(_config.TokenCacheFile, JsonSerializer.Serialize(_ncc.AccessToken)); + } + else + { + _logger.LogError("Login failed!"); + return; + } + } + + // Get the user id + var userId = await _ncc.GetUserId(); + if (userId == null) + { + _logger.LogError("Couldn't get user!"); + return; + } + _logger.LogInformation($"Logged in as: {userId[..5]}**********"); + _logger.LogInformation($"Access Token: {_ncc.AccessToken?.AccessToken?[..5] ?? "null"}**********"); + + // Get all cars + var cars = await _ncc.GetCars(userId); + if (cars == null) + { + _logger.LogError("Couldn't get cars!"); + return; + } + _logger.LogInformation($"Found {cars.Count} car(s)!"); + + // List all cars and their battery status + foreach (var car in cars) + { + if (car.Vin is null) continue; + + _logger.LogInformation("Cars:"); + _logger.LogInformation($" Nickname: {car.NickName}"); + _logger.LogInformation($" ModelName: {car.ModelName}"); + _logger.LogInformation($" ModelCode: {car.ModelCode}"); + _logger.LogInformation($" ModelYear: {car.ModelYear}"); + _logger.LogInformation($" VIN: {car.Vin[..3]}**********"); + + // Get battery status for car + var bs = await _ncc.GetBatteryStatus(car.Vin, _config.ForceBatteryStatusRefresh); + if (bs == null) + { + _logger.LogWarning(" Couldn't get battery status!"); + continue; + } + _logger.LogInformation($" BatteryStatus"); + _logger.LogInformation($" BatteryLevel: {bs.BatteryLevel}%"); + _logger.LogInformation($" RangeHvacOff: {bs.RangeHvacOff} km"); + _logger.LogInformation($" RangeHvacOn: {bs.RangeHvacOn} km"); + _logger.LogInformation($" LastUpdateTime: {bs.LastUpdateTime}"); + _logger.LogInformation($" BatteryStatusAge: {bs.BatteryStatusAge}"); + _logger.LogInformation($" PlugStatus: {bs.PlugStatus}"); + _logger.LogInformation($" PlugStatusDetail: {bs.PlugStatusDetail}"); + _logger.LogInformation($" ChargeStatus: {bs.ChargeStatus}"); + _logger.LogInformation($" ChargePower: {bs.ChargePower}"); + + // Get HVAC status for car + var hvacs = await _ncc.GetHvacStatus(car.Vin); + if (hvacs == null) + { + _logger.LogWarning(" Couldn't get HVAC status!"); + continue; + } + _logger.LogInformation($" HvacStatus"); + _logger.LogInformation($" SocThreshold: {hvacs.SocThreshold}%"); + _logger.LogInformation($" LastUpdateTime: {hvacs.LastUpdateTime}"); + _logger.LogInformation($" HvacStatus: {hvacs.HvacStatus}"); + + // Get cockpit status for car + var cs = await _ncc.GetCockpitStatus(car.Vin); + if (cs == null) + { + _logger.LogWarning(" Couldn't get cockpit status!"); + continue; + } + _logger.LogInformation($" Cockpit"); + _logger.LogInformation($" TotalMileage: {cs.TotalMileage} km"); + } + } +} diff --git a/NissanConnect/NissanConnectLib.Example/NissanConnectLib.Example.csproj b/NissanConnect/NissanConnectLib.Example/NissanConnectLib.Example.csproj index 9bf52be..ae40b8c 100644 --- a/NissanConnect/NissanConnectLib.Example/NissanConnectLib.Example.csproj +++ b/NissanConnect/NissanConnectLib.Example/NissanConnectLib.Example.csproj @@ -2,11 +2,16 @@ Exe - net6.0 + net8.0 enable enable + d454171b-3384-4901-807c-335902959850 + + + + diff --git a/NissanConnect/NissanConnectLib.Example/Program.cs b/NissanConnect/NissanConnectLib.Example/Program.cs index d2e71a7..1c71b5b 100644 --- a/NissanConnect/NissanConnectLib.Example/Program.cs +++ b/NissanConnect/NissanConnectLib.Example/Program.cs @@ -1,145 +1,43 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Hosting; using NissanConnectLib.Api; -using NissanConnectLib.Models; -using System.Text.Json; -namespace NissanConnectLib.Example +namespace NissanConnectLib.Example; + +internal class Program { - internal class Program + static async Task Main(string[] args) { - private const string TOKEN_CACHE_FILE = "token.cache"; + var host = Host.CreateDefaultBuilder(args) - static async Task Main(string[] args) + .ConfigureAppConfiguration((hostingContext, config) => { - // Instantiate client - var ncc = new NissanConnectClient(Configuration.Region.EU); - var loggedIn = false; - - // Save token to cache file when refreshed - ncc.AccessTokenRefreshed += (sender, token) => - { - Console.WriteLine("Access token refreshed!"); - File.WriteAllText(TOKEN_CACHE_FILE, JsonSerializer.Serialize(token)); - }; - - // Try to use token cache file - if (File.Exists(TOKEN_CACHE_FILE)) - { - var cachedToken = JsonSerializer.Deserialize(File.ReadAllText(TOKEN_CACHE_FILE)); - ncc.AccessToken = cachedToken; - - if (await ncc.GetUserId() is null) - { - Console.WriteLine("Could not get user ID using cached token, deleting cache file..."); - File.Delete(TOKEN_CACHE_FILE); - } - else - { - Console.WriteLine("Cached token is valid!"); - loggedIn = true; - } - } - - // Log in using username and password - if (!loggedIn) - { - // Are we missing arguments? - if (args.Length == 0) - { - Console.WriteLine("Command line arguments are missing. Specify username and password"); - return; - } - - // Log in using a username and password - if (args.Length == 2) - { - loggedIn = await ncc.LogIn(args[0], args[1]); - if (loggedIn) - { - Console.WriteLine("Logged in using username and password. Writing token to cache file..."); - File.WriteAllText(TOKEN_CACHE_FILE, JsonSerializer.Serialize(ncc.AccessToken)); - } - else - { - Console.WriteLine("Login failed!"); - Console.ReadLine(); - return; - } - } - } - - // Get the user id - var userId = await ncc.GetUserId(); - if (userId == null) - { - Console.WriteLine("Couldn't get user!"); - Console.ReadLine(); - return; - } - Console.WriteLine($"Logged in as: {userId}"); - Console.WriteLine($"Access Token: {ncc.AccessToken?.AccessToken ?? "null"}"); - - // Get all cars - var cars = await ncc.GetCars(userId); - if (cars == null) - { - Console.WriteLine("Couldn't get cars!"); - Console.ReadLine(); - return; - } - Console.WriteLine($"Found {cars.Count} car(s)!"); - - // List all cars and their battery status - foreach (var car in cars) + config.AddEnvironmentVariables(); + config.AddCommandLine(args); + config.AddUserSecrets(); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(options => { - if (car.Vin is null) continue; - - Console.WriteLine("\nCars:"); - Console.WriteLine($" Nickname: {car.NickName}"); - Console.WriteLine($" ModelName: {car.ModelName}"); - Console.WriteLine($" ModelCode: {car.ModelCode}"); - Console.WriteLine($" ModelYear: {car.ModelYear}"); - Console.WriteLine($" VIN: {car.Vin}"); - - // Get battery status for car - var bs = await ncc.GetBatteryStatus(car.Vin); - if (bs == null) - { - Console.WriteLine(" Couldn't get battery status!"); - continue; - } - Console.WriteLine($" BatteryStatus"); - Console.WriteLine($" BatteryLevel: {bs.BatteryLevel}%"); - Console.WriteLine($" RangeHvacOff: {bs.RangeHvacOff} km"); - Console.WriteLine($" RangeHvacOn: {bs.RangeHvacOn} km"); - Console.WriteLine($" LastUpdateTime: {bs.LastUpdateTime}"); - Console.WriteLine($" BatteryStatusAge: {bs.BatteryStatusAge}"); - Console.WriteLine($" PlugStatus: {bs.PlugStatus}"); - Console.WriteLine($" PlugStatusDetail: {bs.PlugStatusDetail}"); - Console.WriteLine($" ChargeStatus: {bs.ChargeStatus}"); - Console.WriteLine($" ChargePower: {bs.ChargePower}"); + options.FormatterName = nameof(CustomConsoleFormatter); + }); + logging.AddConsoleFormatter(); + }) + .ConfigureServices((context, services) => + { + var config = context.Configuration.Get() ?? + new Configuration(); - // Get HVAC status for car - var hvacs = await ncc.GetHvacStatus(car.Vin); - if (hvacs == null) - { - Console.WriteLine(" Couldn't get HVAC status!"); - continue; - } - Console.WriteLine($" HvacStatus"); - Console.WriteLine($" SocThreshold: {hvacs.SocThreshold}%"); - Console.WriteLine($" LastUpdateTime: {hvacs.LastUpdateTime}"); - Console.WriteLine($" HvacStatus: {hvacs.HvacStatus}"); + services.AddHostedService(); + services.AddSingleton(config); + services.AddSingleton(); + }); - // Get cockpit status for car - var cs = await ncc.GetCockpitStatus(car.Vin); - if (cs == null) - { - Console.WriteLine(" Couldn't get cockpit status!"); - continue; - } - Console.WriteLine($" Cockpit"); - Console.WriteLine($" TotalMileage: {cs.TotalMileage} km"); - } - } + await host.Build().RunAsync(); } } diff --git a/NissanConnect/NissanConnectLib/Api/AutoRefreshTokenDelegatingHandler.cs b/NissanConnect/NissanConnectLib/Api/AutoRefreshTokenDelegatingHandler.cs index 6df58ec..33ea15b 100644 --- a/NissanConnect/NissanConnectLib/Api/AutoRefreshTokenDelegatingHandler.cs +++ b/NissanConnect/NissanConnectLib/Api/AutoRefreshTokenDelegatingHandler.cs @@ -1,30 +1,35 @@ -using System.Net.Http.Headers; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; using System.Net; -using NissanConnectLib.Exceptions; -using System.Diagnostics; namespace NissanConnectLib.Api; internal class AutoRefreshTokenDelegatingHandler : DelegatingHandler { private readonly NissanConnectClient _nissanConnectClient; + private readonly ILogger _logger; - public AutoRefreshTokenDelegatingHandler(NissanConnectClient nissanConnectClient) + public AutoRefreshTokenDelegatingHandler(NissanConnectClient nissanConnectClient, ILogger logger) { _nissanConnectClient = nissanConnectClient; + _logger = logger; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { try { + _logger.LogTrace($"{nameof(AutoRefreshTokenDelegatingHandler)}: {nameof(SendAsync)}"); + // Pass the request immediately if we don't have an access token if (_nissanConnectClient.AccessToken is null) { + _logger.LogTrace("AccessToken is null, passing request immediately"); return await base.SendAsync(request, cancellationToken); } - // Set the bearer token + // Add the bearer token to the request + _logger.LogDebug("Adding bearer token to request"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _nissanConnectClient.AccessToken.AccessToken); // Try to make the request @@ -32,15 +37,17 @@ protected override async Task SendAsync(HttpRequestMessage if (response.StatusCode == HttpStatusCode.Unauthorized) { + _logger.LogWarning("Request failed with status code Unauthorized"); - // The request failed, let's try to refresh our access token + // Check if we have a refresh token if (_nissanConnectClient.AccessToken.RefreshToken is not null) { // Refresh token + _logger.LogInformation("Trying to refresh access token"); var newToken = await _nissanConnectClient.RefreshAccessToken(_nissanConnectClient.AccessToken.RefreshToken); if (newToken is null) return response; - Debug.WriteLine($"{nameof(AutoRefreshTokenDelegatingHandler)}: Refreshed access token"); + _logger.LogInformation("Refreshed access token"); // Update the access token, the refreshed access token doesn't have a refresh token, // so we can't set AccessToken directly (_nissanConnectClient.AccessToken = newToken) @@ -48,19 +55,22 @@ protected override async Task SendAsync(HttpRequestMessage _nissanConnectClient.AccessToken.IdToken = newToken.IdToken; _nissanConnectClient.AccessToken.ExpiresIn = newToken.ExpiresIn; + // Notify the client that the access token has been refreshed _nissanConnectClient.OnAccessTokenRefreshed(_nissanConnectClient.AccessToken); - } - // Try to make the request again - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _nissanConnectClient.AccessToken.AccessToken); - response = await base.SendAsync(request, cancellationToken); + // Try to make the request again + _logger.LogDebug("Send the request again with the new access token"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _nissanConnectClient.AccessToken.AccessToken); + response = await base.SendAsync(request, cancellationToken); + } } return response; } - catch + catch (Exception e) { - throw new NotLoggedInException(); + _logger.LogError(e, "Error while sending request"); + throw; } } } diff --git a/NissanConnect/NissanConnectLib/Api/NissanConnectClient.cs b/NissanConnect/NissanConnectLib/Api/NissanConnectClient.cs index a071865..cdd80e2 100644 --- a/NissanConnect/NissanConnectLib/Api/NissanConnectClient.cs +++ b/NissanConnect/NissanConnectLib/Api/NissanConnectClient.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using NissanConnectLib.Exceptions; using NissanConnectLib.Models; using System.Net; @@ -10,7 +12,7 @@ namespace NissanConnectLib.Api; public class NissanConnectClient { - public event EventHandler AccessTokenRefreshed; + public event EventHandler? AccessTokenRefreshed; private readonly string _authBaseUrl; private readonly string _realm; @@ -22,6 +24,7 @@ public class NissanConnectClient private readonly string _userBaseUrl; private readonly string _carAdapterBaseUrl; + private readonly ILogger _logger; private readonly HttpClient _httpClient; /// @@ -34,7 +37,7 @@ public class NissanConnectClient /// is the default region. /// /// - public NissanConnectClient(Region region = Region.EU) + public NissanConnectClient(Region region = Region.EU, ILogger? logger = default) { _authBaseUrl = Settings[region][ConfigurationKey.AuthBaseUrl]; _realm = Settings[region][ConfigurationKey.Realm]; @@ -46,7 +49,10 @@ public NissanConnectClient(Region region = Region.EU) _userBaseUrl = Settings[region][ConfigurationKey.UserBaseUrl]; _carAdapterBaseUrl = Settings[region][ConfigurationKey.CarAdapterBaseUrl]; - _httpClient = new HttpClient(new AutoRefreshTokenDelegatingHandler(this) + // Set a null logger if none is provided to avoid null checks + _logger = logger ?? NullLogger.Instance; + + _httpClient = new HttpClient(new AutoRefreshTokenDelegatingHandler(this, _logger) { InnerHandler = new HttpClientHandler() }); @@ -62,21 +68,12 @@ internal virtual void OnAccessTokenRefreshed(OAuthAccessTokenResult e) /// /// /// - /// public async Task LogIn(string user, string pass) { - var ia = await InitAuthentication(); - if (ia == null) throw new LogInException(); - - var ali = await Authenticate(ia, user, pass); - if (ali == null) throw new LogInException(); - - var code = await Authorize(); - if (code == null) throw new LogInException(); - - var token = await GetAccessToken(code); - if (token == null) throw new LogInException(); - + var ia = await InitAuthentication() ?? throw new LogInException(); + var ali = await Authenticate(ia, user, pass) ?? throw new LogInException(); + var code = await Authorize() ?? throw new LogInException(); + var token = await GetAccessToken(code) ?? throw new LogInException(); AccessToken = token; return true; } @@ -93,8 +90,6 @@ public void LogOut() /// /// Gets the unique user id for the logged in user. /// - /// - /// public async Task GetUserId() { var r = await _httpClient.GetFromJsonAsync($"{_userAdapterBaseUrl}/v1/users/current"); @@ -105,8 +100,6 @@ public void LogOut() /// Gets a list of all the cars owned by the specified user. /// /// - /// - /// public async Task?> GetCars(string userId) { var r = await _httpClient.GetFromJsonAsync($"{_userBaseUrl}/v5/users/{userId}/cars"); @@ -117,8 +110,6 @@ public void LogOut() /// Gets the battery status of the specified car. /// /// - /// - /// public async Task GetBatteryStatus(string vin, bool forceRefresh = false, TimeSpan? waitTime = null) { if (forceRefresh) @@ -136,8 +127,6 @@ public void LogOut() /// Refresh the battery status of the specified car. /// /// - /// - /// public async Task RefreshBatteryStatus(string vin) { var data = new @@ -158,8 +147,6 @@ public async Task RefreshBatteryStatus(string vin) /// Gets the HVAC status of the specified car. /// /// - /// - /// public async Task GetHvacStatus(string vin, bool forceRefresh = false, TimeSpan? waitTime = null) { if (forceRefresh) @@ -177,8 +164,6 @@ public async Task RefreshBatteryStatus(string vin) /// Refresh the HVAC status of the specified car. (Untested!) /// /// - /// - /// public async Task RefreshHvacStatus(string vin) { var data = new @@ -199,8 +184,6 @@ public async Task RefreshHvacStatus(string vin) /// Refresh the location of the specified car. (Untested!) /// /// - /// - /// public async Task RefreshLocation(string vin) { var data = new @@ -221,8 +204,6 @@ public async Task RefreshLocation(string vin) /// Gets the cockpit status of the specified car. /// /// - /// - /// public async Task GetCockpitStatus(string vin) { var r = await _httpClient.GetFromJsonAsync>($"{_carAdapterBaseUrl}/v1/cars/{vin}/cockpit"); @@ -233,8 +214,6 @@ public async Task RefreshLocation(string vin) /// Wake up the specified car (vehicle gateway?). /// /// - /// - /// public async Task WakeUpVehicle(string vin) { var data = new diff --git a/NissanConnect/NissanConnectLib/Configuration.cs b/NissanConnect/NissanConnectLib/Configuration.cs index 4f7479e..efaa192 100644 --- a/NissanConnect/NissanConnectLib/Configuration.cs +++ b/NissanConnect/NissanConnectLib/Configuration.cs @@ -1,41 +1,40 @@ -namespace NissanConnectLib +namespace NissanConnectLib; + +public static class Configuration { - public static class Configuration + public enum Region { - public enum Region - { - EU = 0, - } + EU = 0, + } - public enum ConfigurationKey - { - ClientId, - ClientSecret, - Scope, - AuthBaseUrl, - Realm, - RedirectUri, - CarAdapterBaseUrl, - UserAdapterBaseUrl, - UserBaseUrl - } + public enum ConfigurationKey + { + ClientId, + ClientSecret, + Scope, + AuthBaseUrl, + Realm, + RedirectUri, + CarAdapterBaseUrl, + UserAdapterBaseUrl, + UserBaseUrl + } - public static Dictionary> Settings = new() + public static Dictionary> Settings = new() + { { + Region.EU, new Dictionary { - Region.EU, new Dictionary - { - { ConfigurationKey.ClientId, "a-ncb-nc-android-prod" }, - { ConfigurationKey.ClientSecret, "6GKIax7fGT5yPHuNmWNVOc4q5POBw1WRSW39ubRA8WPBmQ7MOxhm75EsmKMKENem" }, - { ConfigurationKey.Scope, "openid profile vehicles" }, - { ConfigurationKey.AuthBaseUrl, "https://prod.eu2.auth.kamereon.org/kauth" }, - { ConfigurationKey.Realm, "a-ncb-prod" }, - { ConfigurationKey.RedirectUri, "org.kamereon.service.nci:/oauth2redirect" }, - { ConfigurationKey.CarAdapterBaseUrl, "https://alliance-platform-caradapter-prod.apps.eu2.kamereon.io/car-adapter" }, - { ConfigurationKey.UserAdapterBaseUrl, "https://alliance-platform-usersadapter-prod.apps.eu2.kamereon.io/user-adapter" }, - { ConfigurationKey.UserBaseUrl, "https://nci-bff-web-prod.apps.eu2.kamereon.io/bff-web" } - } + { ConfigurationKey.ClientId, "a-ncb-nc-android-prod" }, + { ConfigurationKey.ClientSecret, "6GKIax7fGT5yPHuNmWNVOc4q5POBw1WRSW39ubRA8WPBmQ7MOxhm75EsmKMKENem" }, + { ConfigurationKey.Scope, "openid profile vehicles" }, + { ConfigurationKey.AuthBaseUrl, "https://prod.eu2.auth.kamereon.org/kauth" }, + { ConfigurationKey.Realm, "a-ncb-prod" }, + { ConfigurationKey.RedirectUri, "org.kamereon.service.nci:/oauth2redirect" }, + { ConfigurationKey.CarAdapterBaseUrl, "https://alliance-platform-caradapter-prod.apps.eu2.kamereon.io/car-adapter" }, + { ConfigurationKey.UserAdapterBaseUrl, "https://alliance-platform-usersadapter-prod.apps.eu2.kamereon.io/user-adapter" }, + { ConfigurationKey.UserBaseUrl, "https://nci-bff-web-prod.apps.eu2.kamereon.io/bff-web" } } - }; - } + } + }; } diff --git a/NissanConnect/NissanConnectLib/Enums/ChargePower.cs b/NissanConnect/NissanConnectLib/Enums/ChargePower.cs index fc1ff3d..5b1eec7 100644 --- a/NissanConnect/NissanConnectLib/Enums/ChargePower.cs +++ b/NissanConnect/NissanConnectLib/Enums/ChargePower.cs @@ -1,11 +1,10 @@ -namespace NissanConnectLib.Enums -{ - // https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L106 +namespace NissanConnectLib.Enums; + +// https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L106 - public enum ChargePower - { - Slow = 1, - Normal = 2, - Fast = 3, - } +public enum ChargePower +{ + Slow = 1, + Normal = 2, + Fast = 3, } diff --git a/NissanConnect/NissanConnectLib/Enums/ChargeStatus.cs b/NissanConnect/NissanConnectLib/Enums/ChargeStatus.cs index 0fea03a..c1c44cf 100644 --- a/NissanConnect/NissanConnectLib/Enums/ChargeStatus.cs +++ b/NissanConnect/NissanConnectLib/Enums/ChargeStatus.cs @@ -1,11 +1,10 @@ -namespace NissanConnectLib.Enums -{ - // https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L113 +namespace NissanConnectLib.Enums; + +// https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L113 - public enum ChargeStatus - { - Error = -1, - NotCharging = 0, - Charging = 1, - } +public enum ChargeStatus +{ + Error = -1, + NotCharging = 0, + Charging = 1, } diff --git a/NissanConnect/NissanConnectLib/Enums/PlugStatus.cs b/NissanConnect/NissanConnectLib/Enums/PlugStatus.cs index 8aecbc5..da9e365 100644 --- a/NissanConnect/NissanConnectLib/Enums/PlugStatus.cs +++ b/NissanConnect/NissanConnectLib/Enums/PlugStatus.cs @@ -1,11 +1,10 @@ -namespace NissanConnectLib.Enums -{ - // https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L119 +namespace NissanConnectLib.Enums; + +// https://github.com/mitchellrj/kamereon-python/blob/146904802301aa0b0008e2bdb3a88ed10ff50acf/kamereon/kamereon.py#L119 - public enum PlugStatus - { - Error = -1, - NotPluggedIn = 0, - PluggedIn = 1, - } +public enum PlugStatus +{ + Error = -1, + NotPluggedIn = 0, + PluggedIn = 1, } diff --git a/NissanConnect/NissanConnectLib/Exceptions/LogInException.cs b/NissanConnect/NissanConnectLib/Exceptions/LogInException.cs index 556da60..d765033 100644 --- a/NissanConnect/NissanConnectLib/Exceptions/LogInException.cs +++ b/NissanConnect/NissanConnectLib/Exceptions/LogInException.cs @@ -1,10 +1,6 @@ -namespace NissanConnectLib.Exceptions -{ - internal class LogInException : Exception - { - public LogInException() : base() - { +namespace NissanConnectLib.Exceptions; - } - } +internal class LogInException : Exception +{ + public LogInException() : base() { } } diff --git a/NissanConnect/NissanConnectLib/Exceptions/NotLoggedInException.cs b/NissanConnect/NissanConnectLib/Exceptions/NotLoggedInException.cs index 83c59da..9f6a74e 100644 --- a/NissanConnect/NissanConnectLib/Exceptions/NotLoggedInException.cs +++ b/NissanConnect/NissanConnectLib/Exceptions/NotLoggedInException.cs @@ -1,10 +1,6 @@ -namespace NissanConnectLib.Exceptions -{ - internal class NotLoggedInException : Exception - { - public NotLoggedInException() : base("Not logged in.") - { +namespace NissanConnectLib.Exceptions; - } - } +internal class NotLoggedInException : Exception +{ + public NotLoggedInException() : base("Not logged in.") { } } diff --git a/NissanConnect/NissanConnectLib/Models/ApiResult.cs b/NissanConnect/NissanConnectLib/Models/ApiResult.cs index 912422f..76d9b3c 100644 --- a/NissanConnect/NissanConnectLib/Models/ApiResult.cs +++ b/NissanConnect/NissanConnectLib/Models/ApiResult.cs @@ -1,10 +1,9 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class ApiResult { - public class ApiResult - { - [JsonPropertyName("data")] - public ApiResultData? Data { get; set; } - } + [JsonPropertyName("data")] + public ApiResultData? Data { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/ApiResultData.cs b/NissanConnect/NissanConnectLib/Models/ApiResultData.cs index 3fec8c8..b40a614 100644 --- a/NissanConnect/NissanConnectLib/Models/ApiResultData.cs +++ b/NissanConnect/NissanConnectLib/Models/ApiResultData.cs @@ -1,16 +1,15 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class ApiResultData { - public class ApiResultData - { - [JsonPropertyName("type")] - public string? Type { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } - [JsonPropertyName("id")] - public string? Id { get; set; } + [JsonPropertyName("id")] + public string? Id { get; set; } - [JsonPropertyName("attributes")] - public T? Attributes { get; set; } - } + [JsonPropertyName("attributes")] + public T? Attributes { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/AttributesBatteryStatus.cs b/NissanConnect/NissanConnectLib/Models/AttributesBatteryStatus.cs index 3434833..54045a7 100644 --- a/NissanConnect/NissanConnectLib/Models/AttributesBatteryStatus.cs +++ b/NissanConnect/NissanConnectLib/Models/AttributesBatteryStatus.cs @@ -1,53 +1,52 @@ using NissanConnectLib.Enums; using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class AttributesBatteryStatus { - public class AttributesBatteryStatus - { - [JsonPropertyName("id")] - public string? Id { get; set; } + [JsonPropertyName("id")] + public string? Id { get; set; } - [JsonPropertyName("plugStatusDetail")] - public int PlugStatusDetail { get; set; } + [JsonPropertyName("plugStatusDetail")] + public int PlugStatusDetail { get; set; } - [JsonPropertyName("timeRequiredToFullSlow")] - public int TimeRequiredToFullSlow { get; set; } + [JsonPropertyName("timeRequiredToFullSlow")] + public int TimeRequiredToFullSlow { get; set; } - [JsonPropertyName("plugStatus")] - public PlugStatus PlugStatus { get; set; } + [JsonPropertyName("plugStatus")] + public PlugStatus PlugStatus { get; set; } - [JsonPropertyName("chargeStatus")] - public ChargeStatus ChargeStatus { get; set; } + [JsonPropertyName("chargeStatus")] + public ChargeStatus ChargeStatus { get; set; } - [JsonPropertyName("batteryCapacity")] - public int BatteryCapacity { get; set; } + [JsonPropertyName("batteryCapacity")] + public int BatteryCapacity { get; set; } - [JsonPropertyName("timeRequiredToFullFast")] - public int TimeRequiredToFullFast { get; set; } + [JsonPropertyName("timeRequiredToFullFast")] + public int TimeRequiredToFullFast { get; set; } - [JsonPropertyName("batteryLevel")] - public int BatteryLevel { get; set; } + [JsonPropertyName("batteryLevel")] + public int BatteryLevel { get; set; } - [JsonPropertyName("timeRequiredToFullNormal")] - public int TimeRequiredToFullNormal { get; set; } + [JsonPropertyName("timeRequiredToFullNormal")] + public int TimeRequiredToFullNormal { get; set; } - [JsonPropertyName("rangeHvacOn")] - public int RangeHvacOn { get; set; } + [JsonPropertyName("rangeHvacOn")] + public int RangeHvacOn { get; set; } - [JsonPropertyName("rangeHvacOff")] - public int RangeHvacOff { get; set; } + [JsonPropertyName("rangeHvacOff")] + public int RangeHvacOff { get; set; } - [JsonPropertyName("batteryBarLevel")] - public int BatteryBarLevel { get; set; } + [JsonPropertyName("batteryBarLevel")] + public int BatteryBarLevel { get; set; } - [JsonPropertyName("lastUpdateTime")] - public DateTimeOffset? LastUpdateTime { get; set; } + [JsonPropertyName("lastUpdateTime")] + public DateTimeOffset? LastUpdateTime { get; set; } - [JsonPropertyName("chargePower")] - public ChargePower ChargePower { get; set; } + [JsonPropertyName("chargePower")] + public ChargePower ChargePower { get; set; } - public TimeSpan? BatteryStatusAge => DateTimeOffset.UtcNow - LastUpdateTime?.UtcDateTime; - } + public TimeSpan? BatteryStatusAge => DateTimeOffset.UtcNow - LastUpdateTime?.UtcDateTime; } diff --git a/NissanConnect/NissanConnectLib/Models/AttributesCockpitStatus.cs b/NissanConnect/NissanConnectLib/Models/AttributesCockpitStatus.cs index 2af37ec..53658a1 100644 --- a/NissanConnect/NissanConnectLib/Models/AttributesCockpitStatus.cs +++ b/NissanConnect/NissanConnectLib/Models/AttributesCockpitStatus.cs @@ -1,10 +1,9 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class AttributesCockpitStatus { - public class AttributesCockpitStatus - { - [JsonPropertyName("totalMileage")] - public double? TotalMileage { get; set; } - } + [JsonPropertyName("totalMileage")] + public double? TotalMileage { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/AttributesHvacStatus.cs b/NissanConnect/NissanConnectLib/Models/AttributesHvacStatus.cs index a0d0717..961c1ff 100644 --- a/NissanConnect/NissanConnectLib/Models/AttributesHvacStatus.cs +++ b/NissanConnect/NissanConnectLib/Models/AttributesHvacStatus.cs @@ -1,16 +1,15 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class AttributesHvacStatus { - public class AttributesHvacStatus - { - [JsonPropertyName("socThreshold")] - public double? SocThreshold { get; set; } + [JsonPropertyName("socThreshold")] + public double? SocThreshold { get; set; } - [JsonPropertyName("lastUpdateTime")] - public DateTimeOffset? LastUpdateTime { get; set; } + [JsonPropertyName("lastUpdateTime")] + public DateTimeOffset? LastUpdateTime { get; set; } - [JsonPropertyName("hvacStatus")] - public string? HvacStatus { get; set; } - } + [JsonPropertyName("hvacStatus")] + public string? HvacStatus { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/AuthenticateAuthIdResponse.cs b/NissanConnect/NissanConnectLib/Models/AuthenticateAuthIdResponse.cs index ca698da..2e2cdda 100644 --- a/NissanConnect/NissanConnectLib/Models/AuthenticateAuthIdResponse.cs +++ b/NissanConnect/NissanConnectLib/Models/AuthenticateAuthIdResponse.cs @@ -1,22 +1,21 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class AuthenticateAuthIdResponse { - public class AuthenticateAuthIdResponse - { - [JsonPropertyName("authId")] - public string? AuthId { get; set; } + [JsonPropertyName("authId")] + public string? AuthId { get; set; } - [JsonPropertyName("template")] - public string? Template { get; set; } + [JsonPropertyName("template")] + public string? Template { get; set; } - [JsonPropertyName("stage")] - public string? Stage { get; set; } + [JsonPropertyName("stage")] + public string? Stage { get; set; } - [JsonPropertyName("header")] - public string? Header { get; set; } + [JsonPropertyName("header")] + public string? Header { get; set; } - [JsonPropertyName("callbacks")] - public List? Callbacks { get; set; } - } + [JsonPropertyName("callbacks")] + public List? Callbacks { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/AuthenticateTokenIdResponse.cs b/NissanConnect/NissanConnectLib/Models/AuthenticateTokenIdResponse.cs index b625cd1..d18c798 100644 --- a/NissanConnect/NissanConnectLib/Models/AuthenticateTokenIdResponse.cs +++ b/NissanConnect/NissanConnectLib/Models/AuthenticateTokenIdResponse.cs @@ -1,16 +1,15 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class AuthenticateTokenIdResponse { - public class AuthenticateTokenIdResponse - { - [JsonPropertyName("tokenId")] - public string? TokenId { get; set; } + [JsonPropertyName("tokenId")] + public string? TokenId { get; set; } - [JsonPropertyName("successUrl")] - public string? SuccessUrl { get; set; } + [JsonPropertyName("successUrl")] + public string? SuccessUrl { get; set; } - [JsonPropertyName("realm")] - public string? Realm { get; set; } - } + [JsonPropertyName("realm")] + public string? Realm { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/Callback.cs b/NissanConnect/NissanConnectLib/Models/Callback.cs index 86af36a..1f080d6 100644 --- a/NissanConnect/NissanConnectLib/Models/Callback.cs +++ b/NissanConnect/NissanConnectLib/Models/Callback.cs @@ -1,27 +1,26 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models -{ - public class Callback - { - [JsonPropertyName("type")] - public string? Type { get; set; } +namespace NissanConnectLib.Models; - [JsonPropertyName("input")] - public List? Input { get; set; } +public class Callback +{ + [JsonPropertyName("type")] + public string? Type { get; set; } - [JsonPropertyName("output")] - public List? Output { get; set; } - } + [JsonPropertyName("input")] + public List? Input { get; set; } - public class Input - { - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonPropertyName("output")] + public List? Output { get; set; } +} - [JsonPropertyName("value")] - public string? Value { get; set; } - } +public class Input +{ + [JsonPropertyName("name")] + public string? Name { get; set; } - public class Output : Input { } + [JsonPropertyName("value")] + public string? Value { get; set; } } + +public class Output : Input { } diff --git a/NissanConnect/NissanConnectLib/Models/Car.cs b/NissanConnect/NissanConnectLib/Models/Car.cs index 685553d..612abec 100644 --- a/NissanConnect/NissanConnectLib/Models/Car.cs +++ b/NissanConnect/NissanConnectLib/Models/Car.cs @@ -1,82 +1,81 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class Car { - public class Car - { - [JsonPropertyName("vin")] - public string? Vin { get; set; } + [JsonPropertyName("vin")] + public string? Vin { get; set; } - [JsonPropertyName("color")] - public string? Color { get; set; } + [JsonPropertyName("color")] + public string? Color { get; set; } - [JsonPropertyName("modelName")] - public string? ModelName { get; set; } + [JsonPropertyName("modelName")] + public string? ModelName { get; set; } - [JsonPropertyName("nickname")] - public string? NickName { get; set; } + [JsonPropertyName("nickname")] + public string? NickName { get; set; } - [JsonPropertyName("energy")] - public string? Energy { get; set; } + [JsonPropertyName("energy")] + public string? Energy { get; set; } - [JsonPropertyName("pictureURL")] - public string? PictureUrl { get; set; } + [JsonPropertyName("pictureURL")] + public string? PictureUrl { get; set; } - [JsonPropertyName("registrationNumber")] - public string? RegistrationNumber { get; set; } + [JsonPropertyName("registrationNumber")] + public string? RegistrationNumber { get; set; } - [JsonPropertyName("firstRegistrationDate")] - public DateTimeOffset? FirstRegistrationDate { get; set; } + [JsonPropertyName("firstRegistrationDate")] + public DateTimeOffset? FirstRegistrationDate { get; set; } - [JsonPropertyName("batteryCode")] - public string? BatteryCode { get; set; } + [JsonPropertyName("batteryCode")] + public string? BatteryCode { get; set; } - [JsonPropertyName("engineType")] - public string? EngineType { get; set; } + [JsonPropertyName("engineType")] + public string? EngineType { get; set; } - [JsonPropertyName("syncStatus")] - public string? SyncStatus { get; set; } + [JsonPropertyName("syncStatus")] + public string? SyncStatus { get; set; } - [JsonPropertyName("carGateway")] - public string? CarGateway { get; set; } + [JsonPropertyName("carGateway")] + public string? CarGateway { get; set; } - [JsonPropertyName("phase")] - public int Phase { get; set; } + [JsonPropertyName("phase")] + public int Phase { get; set; } - [JsonPropertyName("privacyMode")] - public string? PrivacyMode { get; set; } + [JsonPropertyName("privacyMode")] + public string? PrivacyMode { get; set; } - [JsonPropertyName("iceEvFlag")] - public string? IceEvFlag { get; set; } + [JsonPropertyName("iceEvFlag")] + public string? IceEvFlag { get; set; } - [JsonPropertyName("canGeneration")] - public string? CanGeneration { get; set; } + [JsonPropertyName("canGeneration")] + public string? CanGeneration { get; set; } - [JsonPropertyName("stolenVehicleFlag")] - public bool StolenVehicleFlag { get; set; } + [JsonPropertyName("stolenVehicleFlag")] + public bool StolenVehicleFlag { get; set; } - [JsonPropertyName("modelCode")] - public string? ModelCode { get; set; } + [JsonPropertyName("modelCode")] + public string? ModelCode { get; set; } - [JsonPropertyName("vinHash")] - public string? VinHash { get; set; } + [JsonPropertyName("vinHash")] + public string? VinHash { get; set; } - [JsonPropertyName("vinCrypt")] - public string? VinCrypt { get; set; } + [JsonPropertyName("vinCrypt")] + public string? VinCrypt { get; set; } - [JsonPropertyName("uuid")] - public string? Uuid { get; set; } + [JsonPropertyName("uuid")] + public string? Uuid { get; set; } - [JsonPropertyName("vidInt")] - public int VidInt { get; set; } + [JsonPropertyName("vidInt")] + public int VidInt { get; set; } - [JsonPropertyName("vehicleOwnedSince")] - public DateTimeOffset? VehicleOwnedSince { get; set; } + [JsonPropertyName("vehicleOwnedSince")] + public DateTimeOffset? VehicleOwnedSince { get; set; } - [JsonPropertyName("modelYear")] - public string? ModelYear { get; set; } + [JsonPropertyName("modelYear")] + public string? ModelYear { get; set; } - [JsonPropertyName("vehicleNickName")] - public string? VehicleNickName { get; set; } - } + [JsonPropertyName("vehicleNickName")] + public string? VehicleNickName { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/CarsResult.cs b/NissanConnect/NissanConnectLib/Models/CarsResult.cs index 7f68cd2..b52d56a 100644 --- a/NissanConnect/NissanConnectLib/Models/CarsResult.cs +++ b/NissanConnect/NissanConnectLib/Models/CarsResult.cs @@ -1,10 +1,9 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class CarsResult { - public class CarsResult - { - [JsonPropertyName("data")] - public List? Data { get; set; } - } + [JsonPropertyName("data")] + public List? Data { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/OAuthAccessTokenResult.cs b/NissanConnect/NissanConnectLib/Models/OAuthAccessTokenResult.cs index 947f9cf..574cf08 100644 --- a/NissanConnect/NissanConnectLib/Models/OAuthAccessTokenResult.cs +++ b/NissanConnect/NissanConnectLib/Models/OAuthAccessTokenResult.cs @@ -1,28 +1,27 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class OAuthAccessTokenResult { - public class OAuthAccessTokenResult - { - [JsonPropertyName("access_token")] - public string? AccessToken { get; set; } + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } - [JsonPropertyName("refresh_token")] - public string? RefreshToken { get; set; } + [JsonPropertyName("refresh_token")] + public string? RefreshToken { get; set; } - [JsonPropertyName("scope")] - public string? Scope { get; set; } + [JsonPropertyName("scope")] + public string? Scope { get; set; } - [JsonPropertyName("id_token")] - public string? IdToken { get; set; } + [JsonPropertyName("id_token")] + public string? IdToken { get; set; } - [JsonPropertyName("token_type")] - public string? TokenType { get; set; } + [JsonPropertyName("token_type")] + public string? TokenType { get; set; } - [JsonPropertyName("expires_in")] - public int ExpiresIn { get; set; } + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } - [JsonPropertyName("nonce")] - public string? Nonce { get; set; } - } + [JsonPropertyName("nonce")] + public string? Nonce { get; set; } } diff --git a/NissanConnect/NissanConnectLib/Models/UserIdResult.cs b/NissanConnect/NissanConnectLib/Models/UserIdResult.cs index 7b422ee..bf979af 100644 --- a/NissanConnect/NissanConnectLib/Models/UserIdResult.cs +++ b/NissanConnect/NissanConnectLib/Models/UserIdResult.cs @@ -1,10 +1,9 @@ using System.Text.Json.Serialization; -namespace NissanConnectLib.Models +namespace NissanConnectLib.Models; + +public class UserIdResult { - public class UserIdResult - { - [JsonPropertyName("userId")] - public string? UserId { get; set; } - } + [JsonPropertyName("userId")] + public string? UserId { get; set; } } diff --git a/NissanConnect/NissanConnectLib/NissanConnectLib.csproj b/NissanConnect/NissanConnectLib/NissanConnectLib.csproj index 06128e2..2c43504 100644 --- a/NissanConnect/NissanConnectLib/NissanConnectLib.csproj +++ b/NissanConnect/NissanConnectLib/NissanConnectLib.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable True @@ -27,6 +27,7 @@ +