Adds Tesla API integration
All checks were successful
Build, Push and Run Container / build (push) Successful in 25s
All checks were successful
Build, Push and Run Container / build (push) Successful in 25s
Adds an endpoint to interact with the Tesla API via a command proxy. This includes fetching vehicle information and configuring telemetry settings. It introduces new models to represent the Tesla API responses and request structures.
This commit is contained in:
33
Source/ProofOfConcept/Models/TelemetryConfigRequest.cs
Normal file
33
Source/ProofOfConcept/Models/TelemetryConfigRequest.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
public sealed class TelemetryConfigRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("vins")]
|
||||||
|
public List<string> Vins { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("config")]
|
||||||
|
public TelemetryConfig Config { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TelemetryConfig
|
||||||
|
{
|
||||||
|
[JsonPropertyName("hostname")]
|
||||||
|
public string Hostname { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("port")]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ca")]
|
||||||
|
public string CertificateAuthority { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("fields")]
|
||||||
|
public Dictionary<string, TelemetryFieldConfig> Fields { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TelemetryFieldConfig
|
||||||
|
{
|
||||||
|
[JsonPropertyName("interval_seconds")]
|
||||||
|
public int IntervalSeconds { get; set; }
|
||||||
|
}
|
||||||
29
Source/ProofOfConcept/Models/VehiclesResponse.cs
Normal file
29
Source/ProofOfConcept/Models/VehiclesResponse.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
public sealed class VehiclesEnvelope
|
||||||
|
{
|
||||||
|
[JsonPropertyName("response")]
|
||||||
|
public List<VehicleSummary> Response { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("count")]
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VehicleSummary
|
||||||
|
{
|
||||||
|
// Tesla fields commonly present on /api/1/vehicles
|
||||||
|
[JsonPropertyName("id")] public long Id { get; set; }
|
||||||
|
[JsonPropertyName("vehicle_id")] public long VehicleId { get; set; }
|
||||||
|
[JsonPropertyName("vin")] public string? Vin { get; set; }
|
||||||
|
[JsonPropertyName("display_name")] public string? DisplayName { get; set; }
|
||||||
|
[JsonPropertyName("state")] public string? State { get; set; }
|
||||||
|
|
||||||
|
// Extra fields sometimes included; safe to keep nullable
|
||||||
|
[JsonPropertyName("in_service")] public bool? InService { get; set; }
|
||||||
|
[JsonPropertyName("calendar_enabled")] public bool? CalendarEnabled { get; set; }
|
||||||
|
[JsonPropertyName("id_s")] public string? IdS { get; set; }
|
||||||
|
[JsonPropertyName("option_codes")] public string? OptionCodes { get; set; }
|
||||||
|
[JsonPropertyName("color")] public string? Color { get; set; }
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
87
Source/ProofOfConcept/Utilities/CertificateAuthority.cs
Normal file
87
Source/ProofOfConcept/Utilities/CertificateAuthority.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
namespace ProofOfConcept.Utilities;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
public static class CertificateAuthority
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a self-signed Root CA certificate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subjectName">Distinguished Name (e.g., "CN=My Root CA, O=MyOrg, C=HU").</param>
|
||||||
|
/// <param name="validYears">Validity in years.</param>
|
||||||
|
/// <returns>X509Certificate2 with private key.</returns>
|
||||||
|
public static X509Certificate2 CreateRootCA(string subjectName, int validYears = 10)
|
||||||
|
{
|
||||||
|
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
|
|
||||||
|
var req = new CertificateRequest(
|
||||||
|
new X500DistinguishedName(subjectName),
|
||||||
|
key,
|
||||||
|
HashAlgorithmName.SHA256);
|
||||||
|
|
||||||
|
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, true, 1, true));
|
||||||
|
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
|
||||||
|
req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
|
||||||
|
|
||||||
|
var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||||
|
var notAfter = notBefore.AddYears(validYears);
|
||||||
|
|
||||||
|
var cert = req.CreateSelfSigned(notBefore, notAfter);
|
||||||
|
|
||||||
|
// Attach private key so we can export
|
||||||
|
return cert.CopyWithPrivateKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a TLS/HTTPS certificate for a domain, signed by the given Root CA.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rootCA">Root CA certificate (must include private key).</param>
|
||||||
|
/// <param name="subjectName">Common Name (e.g., "CN=myapp.local").</param>
|
||||||
|
/// <param name="dnsNames">DNS SAN entries.</param>
|
||||||
|
/// <param name="ipAddresses">Optional IP SAN entries.</param>
|
||||||
|
/// <param name="validDays">Validity in days (max ~397 for Apple clients).</param>
|
||||||
|
/// <returns>X509Certificate2 with private key.</returns>
|
||||||
|
public static X509Certificate2 CreateHttpsCertificate(X509Certificate2 rootCA, string subjectName, IEnumerable<string> dnsNames, IEnumerable<IPAddress>? ipAddresses = null, int validDays = 397)
|
||||||
|
{
|
||||||
|
if (!rootCA.HasPrivateKey)
|
||||||
|
throw new ArgumentException("Root CA must have a private key", nameof(rootCA));
|
||||||
|
|
||||||
|
using var leafKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
|
|
||||||
|
var req = new CertificateRequest(
|
||||||
|
new X500DistinguishedName(subjectName),
|
||||||
|
leafKey,
|
||||||
|
HashAlgorithmName.SHA256);
|
||||||
|
|
||||||
|
// Basic constraints: not a CA
|
||||||
|
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||||
|
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
|
||||||
|
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, true)); // serverAuth
|
||||||
|
req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
|
||||||
|
|
||||||
|
// Subject Alternative Names
|
||||||
|
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||||
|
foreach (var dns in dnsNames)
|
||||||
|
sanBuilder.AddDnsName(dns);
|
||||||
|
if (ipAddresses != null)
|
||||||
|
{
|
||||||
|
foreach (var ip in ipAddresses)
|
||||||
|
sanBuilder.AddIpAddress(ip);
|
||||||
|
}
|
||||||
|
req.CertificateExtensions.Add(sanBuilder.Build());
|
||||||
|
|
||||||
|
var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||||
|
var notAfter = notBefore.AddDays(validDays);
|
||||||
|
|
||||||
|
// Generate random serial
|
||||||
|
var serial = new byte[16];
|
||||||
|
RandomNumberGenerator.Fill(serial);
|
||||||
|
serial[0] &= 0x7F; // positive
|
||||||
|
|
||||||
|
var issued = req.Create(rootCA, notBefore, notAfter, serial);
|
||||||
|
return issued.CopyWithPrivateKey(leafKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user