using System; using System.Collections.Generic; using System.Threading.Tasks; using Serilog; using ShareeBike.Model.Connector; using ShareeBike.Repository.Exception; using ShareeBike.Services.BluetoothLock; using ShareeBike.Services.BluetoothLock.Exception; using ShareeBike.Services.BluetoothLock.Tdo; using ShareeBike.Services.Geolocation; using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler; namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command { public static class OpenCommand { /// /// Possible steps of opening a lock. /// public enum Step { OpeningLock, WaitStopPolling, GetLockInfos, UpdateLockingState, } /// /// Possible states of opening a lock. /// public enum State { StopPollingFailed, OutOfReachError, CouldntOpenBoldStatusIsUnknownError, CouldntOpenBoldIsBlockedError, CouldntOpenInconsistentStateError, GeneralOpenError, WebConnectFailed, ResponseIsInvalid, BackendUpdateFailed } /// /// Interface to notify view model about steps/ state changes of opening process. /// public interface IOpenCommandListener { /// /// Reports current step. /// /// Current step to report. void ReportStep(Step currentStep); /// /// Reports current state. /// /// Current state to report. /// Message describing the current state. /// Task ReportStateAsync(State currentState, string message); } /// /// Opens the lock and updates copri. /// /// Interface to notify view model about steps/ state changes of opening process. /// Task which stops polling. public static async Task InvokeAsync( IBikeInfoMutable bike, ILocksService lockService, Func isConnectedDelegate, Func connectorFactory, IOpenCommandListener listener, Task stopPollingTask) { // Invokes member to notify about step being started. void InvokeCurrentStep(Step step) { if (listener == null) return; try { listener.ReportStep(step); } catch (Exception exception) { Log.ForContext().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step); } } // Invokes member to notify about state change. async Task InvokeCurrentStateAsync(State state, string message) { if (listener == null) return; try { await listener.ReportStateAsync(state, message); } catch (Exception exception) { Log.ForContext().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state); } } // Wait polling task to stop (on finished or on canceled). async Task WaitForPendingTasks() { // Step: Wait until getting geolocation has completed. InvokeCurrentStep(Step.WaitStopPolling); Log.ForContext().Information($"Waiting on stop polling to finish..."); try { await Task.WhenAll(new List { stopPollingTask ?? Task.CompletedTask }); } catch (Exception exception) { Log.ForContext().Information("Wait for polling task to finish failed. {@exception}", exception); await InvokeCurrentStateAsync(State.StopPollingFailed, exception.Message); return; } Log.ForContext().Information($"Stop polling finished."); return; } // Get lock infos async Task GetLockInfos() { Log.ForContext().Debug($"Starting step {Step.GetLockInfos}..."); InvokeCurrentStep(Step.GetLockInfos); // get current charging level try { bike.LockInfo.BatteryPercentage = await lockService[bike.LockInfo.Id].GetBatteryPercentageAsync(); Log.ForContext().Information("Lock infos of bike {bikeId} read successfully.", bike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock infos could not be read.", bike.Id); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach"); } else { Log.ForContext().Debug("{@exception}", exception); } } // get version infos. var versionTdo = lockService[bike.LockInfo.Id].VersionInfo; if (versionTdo != null) { bike.LockInfo.VersionInfo = new VersionInfo.Builder { FirmwareVersion = versionTdo.FirmwareVersion, HardwareVersion = versionTdo.HardwareVersion, LockVersion = versionTdo.LockVersion, }.Build(); } } // Updates locking state async Task UpdateLockingState() { // Step: Update backend. InvokeCurrentStep(Step.UpdateLockingState); try { await connectorFactory(true).Command.UpdateLockingStateAsync(bike); Log.ForContext().Information("Backend updated for bike {bikeId} successfully.", bike.Id); } catch (Exception exception) { Log.ForContext().Information("Updating backend for bike {bikeId} failed.", bike.Id); //BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Debug("Copri server not reachable."); await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message); return; } else if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Debug("Message: {Message} Details: {Details}", copriException.Message, copriException.Response); await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message); return; } else { Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception); await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message); return; } } } //// Start Action //// Step: Open lock. Log.ForContext().Information($"Starting step {Step.OpeningLock}..."); InvokeCurrentStep(Step.OpeningLock); LockitLockingState? lockingState; try { lockingState = await lockService[bike.LockInfo.Id].OpenAsync(); Log.ForContext().Information("Lock of bike {bikeId} opened successfully.", bike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} can not be opened.", bike.Id); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach"); await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message); } else if (exception is CouldntOpenBoldStatusIsUnknownException) { Log.ForContext().Debug("Lock status is unknown."); await InvokeCurrentStateAsync(State.CouldntOpenBoldStatusIsUnknownError, exception.Message); } else if (exception is CouldntOpenBoldIsBlockedException) { Log.ForContext().Debug("Bold is blocked.}"); await InvokeCurrentStateAsync(State.CouldntOpenBoldIsBlockedError, exception.Message); } else if (exception is CouldntOpenInconsistentStateExecption inconsistentState && inconsistentState.State == LockingState.Closed) { Log.ForContext().Debug("Lock reports that it is still closed.}"); await InvokeCurrentStateAsync(State.CouldntOpenInconsistentStateError, exception.Message); } else { Log.ForContext().Debug("{@exception}", exception); await InvokeCurrentStateAsync(State.GeneralOpenError, exception.Message); } // Update current state from exception bike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; if (!isConnectedDelegate()) { // Lock state can not be updated because there is no connected. throw; } if (exception is OutOfReachException) { // Locking state can not be updated because lock is not connected. throw; } //// Step: Get Lock Battery and Version info await GetLockInfos(); //// Step: Wait for stop polling finished await WaitForPendingTasks(); //// Step: Update backend. //Do this even if current lock state is closed (lock state must not necessarily be closed before try to close, i.e. something undefined between open and opened). await UpdateLockingState(); throw; } bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected; if (!isConnectedDelegate()) { return; } //// Step: Get Lock Battery and Version info await GetLockInfos(); //// Step: Wait for stop polling finished await WaitForPendingTasks(); //// Step: Update backend. await UpdateLockingState(); } } }