Compare commits
38 Commits
947a1313c8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a9e56b131c | |||
| ecb4482a1b | |||
| 86c000f323 | |||
| 84dc22f324 | |||
| 6ac6d05f5f | |||
| f39d900fbb | |||
| 387ef9e70a | |||
| 22df381755 | |||
| c44f0d327d | |||
| 1272ecab46 | |||
| 3c64228f8c | |||
| b00cebbd0a | |||
| 00e79097d6 | |||
| c62dc22245 | |||
| 85cf55f878 | |||
| 6c59133e13 | |||
| d2e976aee1 | |||
| 576b3b91a4 | |||
| 0549406720 | |||
| 1de73e0bca | |||
| 430daa39dc | |||
| 4cfad4ae33 | |||
| df999abf6c | |||
| c0a14e070c | |||
| 317b3eeacd | |||
| a9121cf48e | |||
| 326c46cb27 | |||
| 0d2fdc4de6 | |||
| de4e06401b | |||
| 05c102aee8 | |||
| 5daf0825a0 | |||
| 166ac0b290 | |||
| 1aa42c1cd6 | |||
| ce5852cbe8 | |||
| d291e6ec3e | |||
| f06dd72213 | |||
| 5719bf41b9 | |||
| dee65c9ee4 |
@@ -43,12 +43,15 @@ jobs:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name automatic-parking \
|
--name automatic-parking \
|
||||||
--network traefik \
|
--network traefik \
|
||||||
|
--network-alias mqtt \
|
||||||
--label 'traefik.enable=true' \
|
--label 'traefik.enable=true' \
|
||||||
--label 'traefik.http.routers.automatic-parking.rule=Host(`automatic-parking.app`)' \
|
--label 'traefik.http.routers.automatic-parking.rule=Host(`automatic-parking.app`)' \
|
||||||
--label 'traefik.http.routers.automatic-parking.entrypoints=websecure' \
|
--label 'traefik.http.routers.automatic-parking.entrypoints=websecure' \
|
||||||
--label 'traefik.http.routers.automatic-parking.tls.certresolver=le' \
|
--label 'traefik.http.routers.automatic-parking.tls.certresolver=le' \
|
||||||
--label 'traefik.http.services.automatic-parking.loadbalancer.server.port=8080' \
|
--label 'traefik.http.services.automatic-parking.loadbalancer.server.port=8080' \
|
||||||
|
--label 'traefik.tcp.routers.mqtt.rule=HostSNI(`*`)' \
|
||||||
|
--label 'traefik.tcp.routers.mqtt.entrypoints=mqtt' \
|
||||||
|
--label 'traefik.tcp.services.mqtt.loadbalancer.server.port=1883' \
|
||||||
--label 'traefik.docker.network=traefik' \
|
--label 'traefik.docker.network=traefik' \
|
||||||
-e ASPNETCORE_ENVIRONMENT=Development \
|
-e ASPNETCORE_ENVIRONMENT=Development \
|
||||||
docker-registry.automatic-parking.dev/automatic-parking:poc
|
docker-registry.automatic-parking.dev/automatic-parking:poc
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-preview AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
USER $APP_UID
|
USER $APP_UID
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["ProofOfConcept/ProofOfConcept.csproj", "ProofOfConcept/"]
|
COPY ["ProofOfConcept/ProofOfConcept.csproj", "ProofOfConcept/"]
|
||||||
|
|||||||
25
Source/ProofOfConcept/Models/FleetResponse.cs
Normal file
25
Source/ProofOfConcept/Models/FleetResponse.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
public class FleetRootResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("response")] public FleetResponse FleetResponse { get; set; } = new FleetResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FleetResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("key_paired_vins")] public List<string> KeyPairedVins { get; set; } = new List<string>();
|
||||||
|
[JsonPropertyName("unpaired_vins")] public List<string> UnpairedVins { get; set; } = new List<string>();
|
||||||
|
[JsonPropertyName("vehicle_info")] public Dictionary<string, VehicleInfo> VehicleInfo { get; set; } = new Dictionary<string, VehicleInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VehicleInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("firmware_version")] public string FirmwareVersion { get; set; } = "";
|
||||||
|
[JsonPropertyName("vehicle_command_protocol_required")] public bool VehicleCommandProtocolRequired { get; set; }
|
||||||
|
[JsonPropertyName("discounted_device_data")] public bool DiscountedDeviceData { get; set; }
|
||||||
|
[JsonPropertyName("fleet_telemetry_version")] public string FleetTelemetryVersion { get; set; } = "";
|
||||||
|
[JsonPropertyName("total_number_of_keys")] public int TotalNumberOfKeys { get; set; }
|
||||||
|
}
|
||||||
47
Source/ProofOfConcept/Models/ParkingState.cs
Normal file
47
Source/ProofOfConcept/Models/ParkingState.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
public class ParkingState
|
||||||
|
{
|
||||||
|
public bool CarParked { get; set; }
|
||||||
|
public bool ParkingInProgress { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? CarParkedAt { get; set; }
|
||||||
|
public DateTimeOffset? ParkingStartedAt { get; set; }
|
||||||
|
public DateTimeOffset? ParkingStoppedAt { get; set; }
|
||||||
|
|
||||||
|
public void SetCarParked()
|
||||||
|
{
|
||||||
|
if (!CarParked)
|
||||||
|
{
|
||||||
|
CarParked = true;
|
||||||
|
CarParkedAt = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCarMoved()
|
||||||
|
{
|
||||||
|
if (CarParked)
|
||||||
|
{
|
||||||
|
CarParked = false;
|
||||||
|
CarParkedAt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParkingStarted()
|
||||||
|
{
|
||||||
|
if (!ParkingInProgress)
|
||||||
|
{
|
||||||
|
ParkingInProgress = true;
|
||||||
|
ParkingStartedAt = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParkingStopped()
|
||||||
|
{
|
||||||
|
if (ParkingInProgress)
|
||||||
|
{
|
||||||
|
ParkingInProgress = false;
|
||||||
|
ParkingStoppedAt = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Source/ProofOfConcept/Models/TeslaState.cs
Normal file
73
Source/ProofOfConcept/Models/TeslaState.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
public class TeslaState
|
||||||
|
{
|
||||||
|
private string gear = "P";
|
||||||
|
private bool locked;
|
||||||
|
private bool driverSeatOccupied;
|
||||||
|
private bool gpsState;
|
||||||
|
private double latitude;
|
||||||
|
private double longitude;
|
||||||
|
|
||||||
|
public string Gear
|
||||||
|
{
|
||||||
|
get => this.gear;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.gear = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Locked
|
||||||
|
{
|
||||||
|
get => this.locked;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.locked = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DriverSeatOccupied
|
||||||
|
{
|
||||||
|
get => this.driverSeatOccupied;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.driverSeatOccupied = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GPSState
|
||||||
|
{
|
||||||
|
get => this.gpsState;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.gpsState = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Latitude
|
||||||
|
{
|
||||||
|
get => this.latitude;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.latitude = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Longitude
|
||||||
|
{
|
||||||
|
get => this.longitude;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.longitude = value;
|
||||||
|
LastUpdate = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset LastUpdate { get; private set; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
@@ -11,6 +13,8 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
|||||||
using ProofOfConcept.Models;
|
using ProofOfConcept.Models;
|
||||||
using ProofOfConcept.Services;
|
using ProofOfConcept.Services;
|
||||||
using ProofOfConcept.Utilities;
|
using ProofOfConcept.Utilities;
|
||||||
|
using SzakatsA.Result;
|
||||||
|
using IPNetwork = System.Net.IPNetwork;
|
||||||
|
|
||||||
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
|
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
|
||||||
|
|
||||||
@@ -136,8 +140,9 @@ builder.Services
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add own services
|
// Add own services
|
||||||
builder.Services.AddSingleton<IMessageProcessor, MessageProcessor>();
|
builder.Services.AddSingleton<MessageProcessor>();
|
||||||
builder.Services.AddTransient<ITeslaAuthenticatorService, TeslaAuthenticatorService>();
|
builder.Services.AddTransient<ITeslaAuthenticatorService, TeslaAuthenticatorService>();
|
||||||
|
builder.Services.AddTransient<ZoneDeterminatorService>();
|
||||||
|
|
||||||
// Add hosted services
|
// Add hosted services
|
||||||
builder.Services.AddHostedService<MQTTServer>();
|
builder.Services.AddHostedService<MQTTServer>();
|
||||||
@@ -147,8 +152,9 @@ builder.Services.AddHostedService<MQTTClient>();
|
|||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
ForwardedHeadersOptions forwardedHeadersOptions = new ForwardedHeadersOptions() { ForwardedHeaders = ForwardedHeaders.All };
|
ForwardedHeadersOptions forwardedHeadersOptions = new ForwardedHeadersOptions() { ForwardedHeaders = ForwardedHeaders.All };
|
||||||
forwardedHeadersOptions.KnownNetworks.Clear();
|
forwardedHeadersOptions.KnownIPNetworks.Clear();
|
||||||
forwardedHeadersOptions.KnownProxies.Clear();
|
forwardedHeadersOptions.KnownProxies.Clear();
|
||||||
|
forwardedHeadersOptions.KnownIPNetworks.Add(new IPNetwork(IPAddress.Any, 0));
|
||||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@@ -169,10 +175,10 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
return JsonSerializer.Serialize(token, new JsonSerializerOptions() { WriteIndented = true });
|
return JsonSerializer.Serialize(token, new JsonSerializerOptions() { WriteIndented = true });
|
||||||
});
|
});
|
||||||
app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync());
|
app.MapGet("/CheckRegisteredApplication", ([FromServices] ITeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync());
|
||||||
app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync());
|
app.MapGet("/RegisterApplication", ([FromServices] ITeslaAuthenticatorService service) => service.RegisterApplicationAsync());
|
||||||
app.MapGet("/Authorize", async (IHttpContextAccessor contextAccessor) => await (contextAccessor.HttpContext!).ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/Tesla" }));
|
app.MapGet("/Authorize", async ([FromQuery] string redirect, [FromServices] IHttpContextAccessor contextAccessor) => await (contextAccessor.HttpContext!).ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = redirect }));
|
||||||
app.MapGet("/KeyPairing", () => Results.Redirect("https://tesla.com/_ak/developer-domain.com"));
|
app.MapGet("/KeyPairing", () => Results.Redirect("https://tesla.com/_ak/automatic-parking.app"));
|
||||||
app.MapGet("/Tokens", async (IHttpContextAccessor httpContextAccessor) =>
|
app.MapGet("/Tokens", async (IHttpContextAccessor httpContextAccessor) =>
|
||||||
{
|
{
|
||||||
var ctx = httpContextAccessor.HttpContext;
|
var ctx = httpContextAccessor.HttpContext;
|
||||||
@@ -231,6 +237,45 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.MapGet("/Diagnose", async ([FromServices] ILogger<Configurator> logger, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IHttpClientFactory httpClientFactory) =>
|
||||||
|
{
|
||||||
|
logger.LogTrace("Checking errors for car...");
|
||||||
|
|
||||||
|
HttpContext? context = httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
if (context is null)
|
||||||
|
return Results.BadRequest();
|
||||||
|
|
||||||
|
string? access_token = await context.GetTokenAsync("access_token");
|
||||||
|
string? refresh_token = await context.GetTokenAsync("refresh_token");
|
||||||
|
logger.LogCritical("User has access_token: {access_token} and refresh_token: {refresh_token}", access_token, refresh_token);
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(access_token))
|
||||||
|
return Results.LocalRedirect("/Authorize?redirect=Diagnose");
|
||||||
|
|
||||||
|
HttpClient client = httpClientFactory.CreateClient("InsecureClient");
|
||||||
|
client.BaseAddress = new Uri("https://tesla_command_proxy");
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {access_token}");
|
||||||
|
|
||||||
|
//Get cars
|
||||||
|
VehiclesEnvelope? vehiclesEnvelope = await client.GetFromJsonAsync<VehiclesEnvelope>("/api/1/vehicles");
|
||||||
|
string[] vinNumbers = vehiclesEnvelope?.Response.Select(x => x.Vin ?? "").Where(v => !String.IsNullOrWhiteSpace(v)).ToArray() ?? Array.Empty<string>();
|
||||||
|
logger.LogCritical("User has access to {count} cars: {vins}", vinNumbers.Length, String.Join(", ", vinNumbers));
|
||||||
|
|
||||||
|
if (vinNumbers.Length == 0)
|
||||||
|
return Results.Ok("No cars found");
|
||||||
|
|
||||||
|
foreach (string vinNumber in vinNumbers)
|
||||||
|
{
|
||||||
|
HttpResponseMessage responseMessage = await client.GetAsync($"/api/1/vehicles/{vinNumber}/fleet_telemetry_errors");
|
||||||
|
string response = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
logger.LogInformation("Telemetry errors for {vinNumber}: {response}", vinNumber, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok("Done");
|
||||||
|
});
|
||||||
|
|
||||||
app.MapGet("/Tesla", async ([FromServices] ILogger<Configurator> logger, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IHttpClientFactory httpClientFactory) =>
|
app.MapGet("/Tesla", async ([FromServices] ILogger<Configurator> logger, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IHttpClientFactory httpClientFactory) =>
|
||||||
{
|
{
|
||||||
HttpContext? context = httpContextAccessor.HttpContext;
|
HttpContext? context = httpContextAccessor.HttpContext;
|
||||||
@@ -243,7 +288,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
logger.LogCritical("User has access_token: {access_token} and refresh_token: {refresh_token}", access_token, refresh_token);
|
logger.LogCritical("User has access_token: {access_token} and refresh_token: {refresh_token}", access_token, refresh_token);
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(access_token))
|
if (String.IsNullOrEmpty(access_token))
|
||||||
return Results.LocalRedirect("/Authorize");
|
return Results.LocalRedirect("/Authorize?redirect=Tesla");
|
||||||
|
|
||||||
HttpClient client = httpClientFactory.CreateClient("InsecureClient");
|
HttpClient client = httpClientFactory.CreateClient("InsecureClient");
|
||||||
client.BaseAddress = new Uri("https://tesla_command_proxy");
|
client.BaseAddress = new Uri("https://tesla_command_proxy");
|
||||||
@@ -251,9 +296,22 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
//Get cars
|
//Get cars
|
||||||
VehiclesEnvelope? vehiclesEnvelope = await client.GetFromJsonAsync<VehiclesEnvelope>("/api/1/vehicles");
|
VehiclesEnvelope? vehiclesEnvelope = await client.GetFromJsonAsync<VehiclesEnvelope>("/api/1/vehicles");
|
||||||
string[] vins = vehiclesEnvelope?.Response.Select(x => x.Vin ?? "").Where(v => !String.IsNullOrWhiteSpace(v)).ToArray() ?? Array.Empty<string>();
|
string[] vinNumbers = vehiclesEnvelope?.Response.Select(x => x.Vin ?? "").Where(v => !String.IsNullOrWhiteSpace(v)).ToArray() ?? Array.Empty<string>();
|
||||||
logger.LogCritical("User has access to {count} cars: {vins}", vins.Length, String.Join(", ", vins));
|
logger.LogCritical("User has access to {count} cars: {vins}", vinNumbers.Length, String.Join(", ", vinNumbers));
|
||||||
|
|
||||||
|
if (vinNumbers.Length == 0)
|
||||||
|
return Results.Ok("No cars found");
|
||||||
|
|
||||||
|
//Check if key pairing is required
|
||||||
|
var requestObject = new { vins = vinNumbers };
|
||||||
|
HttpResponseMessage statusResponse = await client.PostAsJsonAsync("/api/1/vehicles/fleet_status", requestObject);
|
||||||
|
string statusResponseContent = await statusResponse.Content.ReadAsStringAsync();
|
||||||
|
logger.LogTrace("Status response: {statusResponseContent}", statusResponseContent);
|
||||||
|
|
||||||
|
FleetResponse? fleetResponse = JsonSerializer.Deserialize<FleetResponse>(statusResponseContent);
|
||||||
|
|
||||||
|
if (!fleetResponse?.KeyPairedVins.Any() ?? false)
|
||||||
|
return Results.Redirect("/KeyPairing");
|
||||||
|
|
||||||
//Get CA from validate server file
|
//Get CA from validate server file
|
||||||
string fileContent = await File.ReadAllTextAsync("Resources/validate_server.json");
|
string fileContent = await File.ReadAllTextAsync("Resources/validate_server.json");
|
||||||
@@ -261,10 +319,10 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
TelemetryConfigRequest configRequest = new TelemetryConfigRequest()
|
TelemetryConfigRequest configRequest = new TelemetryConfigRequest()
|
||||||
{
|
{
|
||||||
Vins = new List<string>(vins),
|
Vins = new List<string>(vinNumbers),
|
||||||
Config = new TelemetryConfig()
|
Config = new TelemetryConfig()
|
||||||
{
|
{
|
||||||
Hostname = "tesla-connector.automatic-parking.app",
|
Hostname = "tesla-telemetry.automatic-parking.app",
|
||||||
Port = 443,
|
Port = 443,
|
||||||
CertificateAuthority = vm?.CA ?? "EMPTY",
|
CertificateAuthority = vm?.CA ?? "EMPTY",
|
||||||
Fields = new Dictionary<string, TelemetryFieldConfig>()
|
Fields = new Dictionary<string, TelemetryFieldConfig>()
|
||||||
@@ -279,18 +337,23 @@ if (app.Environment.IsDevelopment())
|
|||||||
};
|
};
|
||||||
logger.LogInformation("Config request: {configRequest}", JsonSerializer.Serialize(configRequest, new JsonSerializerOptions() { WriteIndented = true }));
|
logger.LogInformation("Config request: {configRequest}", JsonSerializer.Serialize(configRequest, new JsonSerializerOptions() { WriteIndented = true }));
|
||||||
|
|
||||||
if (vins.Length == 0)
|
|
||||||
return Results.Ok("No cars found");
|
|
||||||
|
|
||||||
client.BaseAddress = new Uri("https://fleet-api.prd.eu.vn.cloud.tesla.com");
|
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {access_token}");
|
|
||||||
client.DefaultRequestHeaders.Add("X-Tesla-User-Agent", "Tesla-Connector/1.0.0");
|
|
||||||
|
|
||||||
HttpResponseMessage response = await client.PostAsJsonAsync("/api/1/vehicles/fleet_telemetry_config", configRequest);
|
HttpResponseMessage response = await client.PostAsJsonAsync("/api/1/vehicles/fleet_telemetry_config", configRequest);
|
||||||
return Results.Ok(response.Content.ReadAsStringAsync());
|
return Results.Ok(response.Content.ReadAsStringAsync());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.MapGet("/Zone", async ([FromQuery] double latitude, [FromQuery] double longitude, [FromServices] ILogger<Configurator> logger, [FromServices] ZoneDeterminatorService zoneDeterminator) =>
|
||||||
|
{
|
||||||
|
logger.LogTrace("Getting zone for: {latitude}, {longitude}...", latitude, longitude);
|
||||||
|
|
||||||
|
Result<string> result = await zoneDeterminator.DetermineZoneCodeAsync(latitude, longitude);
|
||||||
|
|
||||||
|
if (result.IsSuccessful)
|
||||||
|
return Results.Ok(result.Value);
|
||||||
|
else
|
||||||
|
return Results.Ok(result.Exception);
|
||||||
|
});
|
||||||
|
|
||||||
//Map static assets
|
//Map static assets
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.7.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.7.0" />
|
||||||
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
|
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
|
||||||
<PackageReference Include="MQTTnet.Server" Version="5.0.1.1416" />
|
<PackageReference Include="MQTTnet.Server" Version="5.0.1.1416" />
|
||||||
|
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||||
|
<PackageReference Include="NetTopologySuite.Features" Version="2.2.0" />
|
||||||
|
<PackageReference Include="NetTopologySuite.IO.GeoJSON" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Pushover" Version="1.0.0" />
|
||||||
<PackageReference Include="SzakatsA.Result" Version="1.1.0" />
|
<PackageReference Include="SzakatsA.Result" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -34,6 +38,9 @@
|
|||||||
<None Update="Resources\Signature\public-key.pem">
|
<None Update="Resources\Signature\public-key.pem">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Resources\parking_zones.geojson">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
1
Source/ProofOfConcept/Resources/nmfr_full.json
Normal file
1
Source/ProofOfConcept/Resources/nmfr_full.json
Normal file
File diff suppressed because one or more lines are too long
137968
Source/ProofOfConcept/Resources/parking_zones.geojson
Normal file
137968
Source/ProofOfConcept/Resources/parking_zones.geojson
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MQTTnet;
|
using MQTTnet;
|
||||||
|
|
||||||
@@ -5,13 +6,13 @@ namespace ProofOfConcept.Services;
|
|||||||
|
|
||||||
public class MQTTClient : IHostedService
|
public class MQTTClient : IHostedService
|
||||||
{
|
{
|
||||||
private ILogger<MQTTClient> logger;
|
private readonly ILogger<MQTTClient> logger;
|
||||||
private MQTTClientConfiguration configuration;
|
private readonly MQTTClientConfiguration configuration;
|
||||||
private MQTTServerConfiguration serverConfiguration;
|
private readonly MQTTServerConfiguration serverConfiguration;
|
||||||
|
|
||||||
private readonly IMqttClient client;
|
private readonly IMqttClient client;
|
||||||
|
|
||||||
public MQTTClient(ILogger<MQTTClient> logger, IOptions<MQTTClientConfiguration> options, IOptions<MQTTServerConfiguration> serverOptions, IMessageProcessor messageProcessor)
|
public MQTTClient(ILogger<MQTTClient> logger, IOptions<MQTTClientConfiguration> options, IOptions<MQTTServerConfiguration> serverOptions, MessageProcessor messageProcessor)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.configuration = options.Value;
|
this.configuration = options.Value;
|
||||||
@@ -21,8 +22,27 @@ public class MQTTClient : IHostedService
|
|||||||
|
|
||||||
this.client.ApplicationMessageReceivedAsync += (e) =>
|
this.client.ApplicationMessageReceivedAsync += (e) =>
|
||||||
{
|
{
|
||||||
logger.LogInformation("Message received: {Message}", e.ApplicationMessage.Payload);
|
string topic = e.ApplicationMessage.Topic;
|
||||||
messageProcessor.ProcessMessage(e.ApplicationMessage.Payload.ToString());
|
|
||||||
|
if (topic.IndexOf("/", StringComparison.Ordinal) > 0)
|
||||||
|
{
|
||||||
|
string[] parts = topic.Split('/'); //telemetry/5YJ3E7EB7KF291652/v/Location
|
||||||
|
|
||||||
|
if (parts is ["telemetry", _, "v", _])
|
||||||
|
{
|
||||||
|
string vin = parts[1];
|
||||||
|
string field = parts[3];
|
||||||
|
|
||||||
|
string? message = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||||
|
logger.LogInformation("Message received: {Message}", message);
|
||||||
|
messageProcessor.ProcessMessage(vin, field.ToLowerInvariant(), message.StripQuotes()).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning("Topic not passed to message processor: {Topic}", topic);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning("Topic not passed to message processor: {Topic}", topic);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -39,7 +59,7 @@ public class MQTTClient : IHostedService
|
|||||||
await this.client.ConnectAsync(options, cancellationToken);
|
await this.client.ConnectAsync(options, cancellationToken);
|
||||||
this.logger.LogTrace("Connected");
|
this.logger.LogTrace("Connected");
|
||||||
|
|
||||||
await this.client.SubscribeAsync("telemetry", cancellationToken: cancellationToken);
|
await this.client.SubscribeAsync("telemetry/#", cancellationToken: cancellationToken);
|
||||||
this.logger.LogTrace("Subscribed");
|
this.logger.LogTrace("Subscribed");
|
||||||
|
|
||||||
this.logger.LogInformation("Started");
|
this.logger.LogInformation("Started");
|
||||||
@@ -63,3 +83,5 @@ public class MQTTClientConfiguration
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file static class StringExtensions { public static string StripQuotes(this string value) => value.Trim('"'); }
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
using MQTTnet.Server;
|
using MQTTnet.Server;
|
||||||
@@ -54,7 +55,7 @@ public class MQTTServer : IHostedService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("Client {ClientID} published message to topic: {Topic}", e.ClientId, e.ApplicationMessage.Topic);
|
logger.LogInformation("Client {ClientID} published message to topic: {Topic}: {Message}", e.ClientId, e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.Payload));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ public class MQTTServer : IHostedService
|
|||||||
{
|
{
|
||||||
if (e.UserName != configuration.Observer.Username || e.Password != configuration.Observer.Password)
|
if (e.UserName != configuration.Observer.Username || e.Password != configuration.Observer.Password)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Observer tried to connect with invalid credentials");
|
logger.LogWarning("Observer tried to connect with invalid credentials: {Username} / {Password} from {RemoteIP}", e.UserName, e.Password, e.RemoteEndPoint is IPEndPoint ipEndPoint ? ipEndPoint.ToString() : e.RemoteEndPoint.GetType().FullName);
|
||||||
e.ReasonCode = MqttConnectReasonCode.NotAuthorized;
|
e.ReasonCode = MqttConnectReasonCode.NotAuthorized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,178 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using ProofOfConcept.Models;
|
||||||
|
using Pushover;
|
||||||
|
using SzakatsA.Result;
|
||||||
|
|
||||||
namespace ProofOfConcept.Services;
|
namespace ProofOfConcept.Services;
|
||||||
|
|
||||||
public interface IMessageProcessor
|
public class MessageProcessor
|
||||||
{
|
|
||||||
Task ProcessMessage(string jsonMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MessageProcessor : IMessageProcessor
|
|
||||||
{
|
{
|
||||||
private readonly ILogger<MessageProcessor> logger;
|
private readonly ILogger<MessageProcessor> logger;
|
||||||
private MessageProcessorConfiguration configuration;
|
private MessageProcessorConfiguration configuration;
|
||||||
|
|
||||||
private readonly IMemoryCache memoryCache;
|
private readonly IMemoryCache memoryCache;
|
||||||
|
private readonly ZoneDeterminatorService zoneDeterminatorService;
|
||||||
|
|
||||||
public MessageProcessor(ILogger<MessageProcessor> logger, IOptions<MessageProcessorConfiguration> options, IMemoryCache memoryCache)
|
private readonly TeslaState teslaState;
|
||||||
|
private readonly ParkingState parkingState;
|
||||||
|
|
||||||
|
private readonly PushoverClient pushApi;
|
||||||
|
|
||||||
|
public MessageProcessor(ILogger<MessageProcessor> logger, IOptions<MessageProcessorConfiguration> options, IMemoryCache memoryCache, ZoneDeterminatorService zoneDeterminatorService)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.configuration = options.Value;
|
this.configuration = options.Value;
|
||||||
|
|
||||||
this.memoryCache = memoryCache;
|
this.memoryCache = memoryCache;
|
||||||
|
this.zoneDeterminatorService = zoneDeterminatorService;
|
||||||
|
|
||||||
|
this.teslaState = new TeslaState();
|
||||||
|
this.parkingState = new ParkingState();
|
||||||
|
|
||||||
|
this.pushApi = new PushoverClient(this.configuration.PushoverAPIKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessMessage(string jsonMessage)
|
public async Task ProcessMessage(string vin, string field, string value)
|
||||||
{
|
{
|
||||||
this.logger.LogTrace("Processing message from Tesla: {Message}", jsonMessage);
|
this.logger.LogTrace("Processing {Field} = {Value} for {VIN}...", field, value, vin);
|
||||||
|
|
||||||
|
string[] validGears = [ "P", "R", "N", "D", "SNA" ];
|
||||||
|
if (field == "gear" && validGears.Contains(value))
|
||||||
|
this.teslaState.Gear = value;
|
||||||
|
else if (field == "gear" && value == "null")
|
||||||
|
this.teslaState.Gear = "P";
|
||||||
|
|
||||||
|
else if (field == "locked" && bool.TryParse(value, out bool locked))
|
||||||
|
this.teslaState.Locked = locked;
|
||||||
|
|
||||||
|
else if (field == "driverseatoccupied" && bool.TryParse(value, out bool driverSeatOccupied))
|
||||||
|
this.teslaState.DriverSeatOccupied = driverSeatOccupied;
|
||||||
|
|
||||||
|
else if (field == "location")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(value);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
this.teslaState.Latitude = root.GetProperty("latitude").GetDouble();
|
||||||
|
this.teslaState.Longitude = root.GetProperty("longitude").GetDouble();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError("Invalid location data: {LocationValue}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.LogTrace("State updated for {VIN}. Current state is Gear: {Gear}, Locked: {locked}, driver seat occupied: {DriverSeatOccupied}, Location: {Latitude},{Longitude}", vin, this.teslaState.Gear, this.teslaState.Locked, this.teslaState.DriverSeatOccupied, this.teslaState.Latitude, this.teslaState.Longitude);
|
||||||
|
|
||||||
|
if (this.teslaState is { Gear: "P", Locked: true, DriverSeatOccupied: false })
|
||||||
|
{
|
||||||
|
this.parkingState.SetCarParked();
|
||||||
|
this.logger.LogInformation("{vin} is in parked state", vin);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.parkingState.SetCarMoved();
|
||||||
|
this.logger.LogInformation("{vin} is NOT in a parked state", vin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.parkingState is { ParkingInProgress: false, CarParked: true })
|
||||||
|
await StartParkingAsync(vin);
|
||||||
|
|
||||||
|
else if (this.parkingState.ParkingInProgress && (this.teslaState.Gear != "P" || this.teslaState.DriverSeatOccupied || !this.teslaState.Locked))
|
||||||
|
await StopParkingAsync(vin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartParkingAsync(string vin)
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Start parking for {vin}...", vin);
|
||||||
|
|
||||||
|
//Get parking zone
|
||||||
|
Result<string> zoneLookupResult = await this.zoneDeterminatorService.DetermineZoneCodeAsync(this.teslaState.Latitude, this.teslaState.Longitude);
|
||||||
|
bool sendNotification = this.configuration.VinNotifications.TryGetValue(vin, out string? pushoverToken);
|
||||||
|
|
||||||
|
if (zoneLookupResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
//Set parking started
|
||||||
|
this.parkingState.SetParkingStarted();
|
||||||
|
|
||||||
|
if (String.IsNullOrWhiteSpace(zoneLookupResult.Value))
|
||||||
|
{
|
||||||
|
// Push not a parking zone
|
||||||
|
if (sendNotification)
|
||||||
|
this.pushApi.Send(pushoverToken, new PushoverMessage
|
||||||
|
{
|
||||||
|
Title = "Nem parkolózóna",
|
||||||
|
Message = $"Megálltál nem parkoló zónában, a GPS szerint: {this.teslaState.Latitude},{this.teslaState.Longitude}",
|
||||||
|
Priority = Priority.Normal,
|
||||||
|
Timestamp = DateTimeOffset.Now.ToLocalTime().ToString(),
|
||||||
|
});
|
||||||
|
this.logger.LogInformation("Parking started in non-parking zone for {VIN}", vin);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Push parking started in zone
|
||||||
|
if (sendNotification)
|
||||||
|
this.pushApi.Send(pushoverToken, new PushoverMessage
|
||||||
|
{
|
||||||
|
Title = $"Parkolás elindult: {zoneLookupResult.Value}",
|
||||||
|
Message = $"Megálltál egy parkolási zónában, a GPS szerint: {this.teslaState.Latitude},{this.teslaState.Longitude}" + Environment.NewLine +
|
||||||
|
$"A zónatérkép szerint ez a {zoneLookupResult.Value} jelű zóna",
|
||||||
|
Priority = Priority.Normal,
|
||||||
|
Timestamp = DateTimeOffset.Now.ToLocalTime().ToString(),
|
||||||
|
});
|
||||||
|
this.logger.LogInformation("Parking started for {VIN} at {ZoneCode}", vin, zoneLookupResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.logger.LogError(zoneLookupResult.Exception, "Can't start parking: error while determining parking zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopParkingAsync(string vin)
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Stopping parking for {vin}...", vin);
|
||||||
|
|
||||||
|
// Push parking stopped
|
||||||
|
this.parkingState.SetParkingStopped();
|
||||||
|
if (this.configuration.VinNotifications.TryGetValue(vin, out string? pushoverToken))
|
||||||
|
this.pushApi.Send(pushoverToken, new PushoverMessage
|
||||||
|
{
|
||||||
|
Title = $"Parkolás leállt ({DateTimeOffset.Now.Subtract(this.parkingState.ParkingStartedAt!.Value).ToElapsed()})",
|
||||||
|
Message = $"A {this.parkingState.ParkingStartedAt?.ToString("yyyy-MM-dd HH:mm")} -kor indult parkolásod leállt",
|
||||||
|
Priority = Priority.Normal,
|
||||||
|
Timestamp = DateTimeOffset.Now.ToLocalTime().ToString(),
|
||||||
|
});
|
||||||
|
this.logger.LogInformation("Parking stopped for {VIN}", vin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MessageProcessorConfiguration
|
public class MessageProcessorConfiguration
|
||||||
{
|
{
|
||||||
|
public string PushoverAPIKey { get; set; } = "acr9fqxafqeqjpr4apryh17m4ak24b";
|
||||||
|
public Dictionary<string, string> VinNotifications { get; set; } = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "5YJ3E7EB7KF291652", "u2ouaqqu5gd9f1bq3rmrtwriumaffu"}, /*Zoli*/
|
||||||
|
{ "LRW3E7EK4NC482668", "udbz5g2hi24m4wcanx44qqkwf7r1c7" /*Nagy Balázs*/ }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
file static class DateTimeOffsetExtensions
|
||||||
|
{
|
||||||
|
public static string ToElapsed(this TimeSpan ts)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
if (ts.Days > 0)
|
||||||
|
parts.Add($"{ts.Days} nap");
|
||||||
|
if (ts.Hours > 0)
|
||||||
|
parts.Add($"{ts.Hours} óra");
|
||||||
|
if (ts.Minutes > 0)
|
||||||
|
parts.Add($"{ts.Minutes} perc");
|
||||||
|
|
||||||
|
return string.Join(", ", parts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ public class TeslaAuthenticatorService : ITeslaAuthenticatorService
|
|||||||
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{euBaseURL}/api/1/partner_accounts", postBody, cancellationToken);
|
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{euBaseURL}/api/1/partner_accounts", postBody, cancellationToken);
|
||||||
bool success = response.IsSuccessStatusCode;
|
bool success = response.IsSuccessStatusCode;
|
||||||
|
|
||||||
logger.LogInformation("Application registration result: {Success}", success ? "Success" : "Failed");
|
logger.LogInformation("Application response code: {Success}", success ? "Success" : "Failed");
|
||||||
|
|
||||||
//Load response from server
|
//Load response from server
|
||||||
string json = await response.Content.ReadAsStringAsync(cancellationToken);
|
string json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
@@ -320,5 +320,5 @@ public class TeslaAuthenticatorServiceConfiguration
|
|||||||
public string ClientID { get; set; } = "b2240ee4-332a-4252-91aa-bbcc24f78fdb";
|
public string ClientID { get; set; } = "b2240ee4-332a-4252-91aa-bbcc24f78fdb";
|
||||||
public string ClientSecret { get; set; } = "ta-secret.YG+XSdlvr6Lv8U-x";
|
public string ClientSecret { get; set; } = "ta-secret.YG+XSdlvr6Lv8U-x";
|
||||||
public TimeSpan MemoryCacheDelta { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan MemoryCacheDelta { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
public string Domain { get; set; } = "tesla-connector.automatic-parking.app";
|
public string Domain { get; set; } = "automatic-parking.app";
|
||||||
}
|
}
|
||||||
71
Source/ProofOfConcept/Services/ZoneDeterminatorService.cs
Normal file
71
Source/ProofOfConcept/Services/ZoneDeterminatorService.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NetTopologySuite.Features;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
using NetTopologySuite.IO;
|
||||||
|
using SzakatsA.Result;
|
||||||
|
|
||||||
|
namespace ProofOfConcept.Services;
|
||||||
|
|
||||||
|
public class ZoneDeterminatorService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ZoneDeterminatorService> logger;
|
||||||
|
private ZoneDeterminatorServiceConfiguration configuration;
|
||||||
|
|
||||||
|
private FeatureCollection parkingZones;
|
||||||
|
private bool initialized;
|
||||||
|
|
||||||
|
public ZoneDeterminatorService(ILogger<ZoneDeterminatorService> logger, IOptions<ZoneDeterminatorServiceConfiguration> options)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.configuration = options.Value;
|
||||||
|
|
||||||
|
this.parkingZones = new FeatureCollection();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<string>> DetermineZoneCodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Determinating parking zone code for coordinates: {Latitude}, {Longitude}...", latitude, longitude);
|
||||||
|
|
||||||
|
if (!this.initialized)
|
||||||
|
await InitializeAsync(cancellationToken);
|
||||||
|
|
||||||
|
Point point = new Point(longitude, latitude);
|
||||||
|
IFeature? zone = this.parkingZones.FirstOrDefault(f => f.Geometry.Contains(point));
|
||||||
|
|
||||||
|
if (zone is null)
|
||||||
|
return Result.Success(String.Empty);
|
||||||
|
else if (!zone.Attributes.Exists("zoneid"))
|
||||||
|
return Result.Fail(new MissingFieldException("Zone ID not found for parking zone"));
|
||||||
|
else if (zone.Attributes["zoneid"] is null)
|
||||||
|
return Result.Fail(new NullReferenceException("Zone ID null for parking zone"));
|
||||||
|
else if (zone.Attributes["zoneid"].ToString() is null)
|
||||||
|
return Result.Fail(new InvalidCastException($"Zone ID is of type {zone.Attributes["zoneID"].GetType().FullName}"));
|
||||||
|
else
|
||||||
|
return Result.Success(zone.Attributes["zoneid"].ToString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Initializing...");
|
||||||
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
this.logger.LogTrace("Reading file...");
|
||||||
|
string geojson = await File.ReadAllTextAsync(this.configuration.ZoneFilePath, cancellationToken);
|
||||||
|
this.logger.LogTrace("File read in {Elapsed} ms", sw.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
this.logger.LogTrace("Parsing geojson...");
|
||||||
|
GeoJsonReader reader = new GeoJsonReader();
|
||||||
|
this.parkingZones = reader.Read<FeatureCollection>(geojson);
|
||||||
|
this.logger.LogTrace("Geojson parsed in {Elapsed} ms: {FeatureCount} features", sw.ElapsedMilliseconds, this.parkingZones.Count);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this.logger.LogInformation("Initialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ZoneDeterminatorServiceConfiguration
|
||||||
|
{
|
||||||
|
public string ZoneFilePath { get; set; } = "Resources/parking_zones.geojson";
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Trace",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"System.Net.Http.HttpClient": "Warning",
|
||||||
|
"Microsoft.Extensions.Http": "Information"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user