using System.Text.Json; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using ProofOfConcept.Models; using Pushover; using SzakatsA.Result; namespace ProofOfConcept.Services; public class MessageProcessor { private readonly ILogger logger; private MessageProcessorConfiguration configuration; private readonly IMemoryCache memoryCache; private readonly ZoneDeterminatorService zoneDeterminatorService; private readonly TeslaState teslaState; private readonly ParkingState parkingState; private readonly PushoverClient pushApi; public MessageProcessor(ILogger logger, IOptions options, IMemoryCache memoryCache, ZoneDeterminatorService zoneDeterminatorService) { this.logger = logger; this.configuration = options.Value; 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 vin, string field, string value) { 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 == "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 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 string PushoverAPIKey { get; set; } = "acr9fqxafqeqjpr4apryh17m4ak24b"; public Dictionary VinNotifications { get; set; } = new Dictionary() { { "5YJ3E7EB7KF291652", "u2ouaqqu5gd9f1bq3rmrtwriumaffu"} /*Zoli*/ }; } file static class DateTimeOffsetExtensions { public static string ToElapsed(this TimeSpan ts) { var parts = new List(); 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); } }