Version 3.0.370

This commit is contained in:
Anja 2023-08-31 12:20:06 +02:00
parent f5cf9bb22f
commit bdb2dec1c1
233 changed files with 10252 additions and 6779 deletions

View file

@ -1,11 +1,42 @@
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Repository.Request;
using TINK.Services.BluetoothLock;
using TINK.Services.Geolocation;
using TINK.ViewModel;
using static TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command.GetLockedLocationCommand;
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
{
/// <summary> Provides a connector object.</summary>
private Func<bool, IConnector> ConnectorFactory { get; }
/// <summary> Provides geolocation information.</summary>
private IGeolocationService GeolocationService { get; }
/// <summary> Provides a connector object.</summary>
private ILocksService LockService { get; }
/// <summary> Delegate to retrieve connected state. </summary>
private Func<bool> IsConnectedDelegate { get; }
/// <summary>Object to manage update of view model objects from Copri.</summary>
protected Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Constructs a bike object from source. </summary>
public BikeInfoMutable(BikeInfo bike, string stationName) : base(
/// <param name="viewUpdateManager">Manages update of view model objects from Copri.</param>
public BikeInfoMutable(
IGeolocationService geolocation,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
BikeInfo bike,
string stationName) : base(
bike != null
? bike.Bike
: throw new ArgumentNullException(nameof(bike)),
@ -27,12 +58,58 @@ namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock
bike.LockInfo.AdminKey,
bike.LockInfo.Seed,
bike.LockInfo.State);
GeolocationService = geolocation
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No geolocation object available.");
LockService = lockService
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No lock service object available.");
IsConnectedDelegate = isConnectedDelegate
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No is connected delegate available.");
ConnectorFactory = connectorFactory
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No connector available.");
ViewUpdateManager = viewUpdateManager
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No update manger available.");
}
public LockInfoMutable LockInfo { get; }
ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
/// <summary>
/// Closes the lock and updates copri.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
/// <param name="stopPollingTask">Task which stops polling.</param>
public async Task CloseLockAsync(
ICloseCommandListener listener,
Task stopPollingTask)
{
await InvokeAsync<BikeInfoMutable>(
this,
GeolocationService,
LockService,
IsConnectedDelegate,
ConnectorFactory,
listener,
stopPollingTask);
}
/// <summary>
/// Gets the location of the locked bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task<LocationDto> GetLockedBikeLocationAsync(IGetLockedLocationCommandListener listener) =>
await InvokeAsync<BikeInfoMutable>(
this,
GeolocationService,
LockService,
listener: listener);
public new string ToString()
{
return $"Id={Id}{(TypeOfBike != null ? $";type={TypeOfBike}" : "")};state={State.ToString()}";

View file

@ -0,0 +1,283 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using TINK.Model.Connector;
using TINK.Repository.Exception;
using TINK.Repository.Request;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Services.Geolocation;
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class CloseCommand
{
/// <summary>
/// Possible steps of closing a lock.
/// </summary>
public enum Step
{
StartStopingPolling,
StartingQueryingLocation,
ClosingLock,
WaitStopPollingQueryLocation,
/// <summary>
/// Notifies back end about modified lock state.
/// </summary>
UpdateLockingState,
QueryLocationTerminated
}
/// <summary>
/// Possible steps of closing a lock.
/// </summary>
public enum State
{
OutOfReachError,
CouldntCloseMovingError,
CouldntCloseBoltBlockedError,
GeneralCloseError,
StartGeolocationException,
WaitGeolocationException,
WebConnectFailed,
ResponseIsInvalid,
BackendUpdateFailed
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface ICloseCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Closes the lock and updates copri.
/// </summary>
/// <param name="listener">Interface to notify view model about steps/ state changes of closing process.</param>
/// <param name="stopPolling">Task which stops polling.</param>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactor,
ICloseCommandListener 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 ex)
{
Log.ForContext<T>().Error("An exception {exception} was thrown invoking step- action for set {step} ", ex, 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 ex)
{
Log.ForContext<T>().Error("An exception {exception} was thrown invoking state- action for set {state} ", ex, state);
}
}
// Wait for geolocation and polling task to stop (on finished or on canceled).
async Task<IGeolocation> WaitForPendingTasks(Task<IGeolocation> locationTask)
{
// Step: Wait until getting geolocation has completed.
InvokeCurrentStep(Step.WaitStopPollingQueryLocation);
Log.ForContext<T>().Debug($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
try
{
await Task.WhenAll(new List<Task> { locationTask, stopPollingTask ?? Task.CompletedTask });
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<T>().Information("Canceling query location/ wait for polling task to finish failed. {Exception}", ex);
await InvokeCurrentStateAsync(State.WaitGeolocationException, ex.Message);
InvokeCurrentStep(Step.QueryLocationTerminated);
return null;
}
Log.ForContext<T>().Debug($"Steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} finished.");
InvokeCurrentStep(Step.QueryLocationTerminated);
return locationTask.Result;
}
// Updates locking state
async Task UpdateLockingState(IGeolocation location, DateTime timeStamp)
{
// Step: Update backend.
InvokeCurrentStep(Step.UpdateLockingState);
try
{
await connectorFactor(true).Command.UpdateLockingStateAsync(
bike,
location != null
? new LocationDto.Builder
{
Latitude = location.Latitude,
Longitude = location.Longitude,
Accuracy = location.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(location.Timestamp.DateTime),
}.Build()
: null);
}
catch (Exception exception)
{
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<T>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", bike);
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
return;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<T>().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", bike, copriException.Message, copriException.Response);
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
return;
}
else
{
Log.ForContext<T>().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 query geolocation data.
Log.ForContext<T>().Debug($"Starting step {Step.StartingQueryingLocation}...");
InvokeCurrentStep(Step.StartingQueryingLocation);
var ctsLocation = new CancellationTokenSource();
Task<IGeolocation> currentLocationTask = Task.FromResult<IGeolocation>(null);
var timeStampNow = DateTime.Now;
try
{
currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<T>().Information("Starting query location failed. {Exception}", bike, ex);
await InvokeCurrentStateAsync(State.StartGeolocationException, ex.Message);
}
// Close lock.
IGeolocation currentLocation;
Log.ForContext<T>().Debug($"Starting step {Step.ClosingLock}...");
InvokeCurrentStep(Step.ClosingLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock can not be closed. {Exception}", exception);
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntCloseMovingException)
{
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
}
else if (exception is CouldntCloseBoltBlockedException)
{
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
}
else
{
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
}
Log.ForContext<T>().Error("Lock can not be closed. {Exception}", exception);
// Signal cts to cancel getting geolocation.
ctsLocation.Cancel();
//// Step: Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
// 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: Update backend.
// Do this even if current lock state is open (lock state must not necessarily be open before try to open, i.e. something undefined between open and closed).
await UpdateLockingState(currentLocation, timeStampNow);
throw;
}
//// Step: Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
// Keep geolocation where closing action occurred.
bike.LockInfo.Location = currentLocation;
if (!isConnectedDelegate())
{
return;
}
//// Step: Update backend.
await UpdateLockingState(currentLocation, timeStampNow);
}
}
}

View file

@ -0,0 +1,178 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using TINK.Repository.Request;
using TINK.Services.BluetoothLock;
using TINK.Services.Geolocation;
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
/// <summary>
/// Provides functionality to get the locked bike location.
/// </summary>
public static class GetLockedLocationCommand
{
/// <summary>
/// Possible steps of closing a lock.
/// </summary>
public enum Step
{
StartingQueryLocation,
DisconnectingLockOnDisconnectedNoLocationError,
}
/// <summary>
/// Possible steps of closing a lock.
/// </summary>
public enum State
{
DisconnetedNoLocationError,
DisconnectError,
QueryLocationSucceeded,
QueryLocationFailed,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface IGetLockedLocationCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Get current location.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<LocationDto> InvokeAsync<T>(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func<DateTime> dateTimeProvider = null,
IGetLockedLocationCommandListener listener = null)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
} catch (Exception ex)
{
Log.ForContext<T>().Error("An exception {exception} was thrown invoking step- action for set {step} ", ex, 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 ex)
{
Log.ForContext<T>().Error("An exception {exception} was thrown invoking state- action for set {state} ", ex, state);
}
}
InvokeCurrentStep(Step.StartingQueryLocation);
// Get geolocation which was requested when closing lock.
IGeolocation closingLockLocation = bike.LockInfo.Location;
if (closingLockLocation != null)
{
// Location was available when closing bike. No further actions required.
return new LocationDto.Builder
{
Latitude = closingLockLocation.Latitude,
Longitude = closingLockLocation.Longitude,
Accuracy = closingLockLocation.Accuracy ?? double.NaN,
Age = bike.LockInfo.LastLockingStateChange is DateTime lastLockState1 ? lastLockState1.Subtract(closingLockLocation.Timestamp.DateTime) : TimeSpan.MaxValue,
}.Build();
}
// Check if bike is around => geolocation information can be queried
var deviceState = lockService[bike.LockInfo.Id].GetDeviceState();
if (deviceState != DeviceState.Connected)
{
// Geolocation can not be queried because bike is not around.
Log.ForContext<T>().Information("User selected booked bike {bike} but returning failed. There is no geolocation information available.", bike);
await InvokeCurrentStateAsync(State.DisconnetedNoLocationError, "");
// Disconnect lock.
InvokeCurrentStep(Step.DisconnectingLockOnDisconnectedNoLocationError);
try
{
bike.LockInfo.State = await lockService.DisconnectAsync(bike.LockInfo.Id, bike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("Lock can not be disconnected. {Exception}", exception);
await InvokeCurrentStateAsync(State.DisconnectError, exception.Message);
}
await InvokeCurrentStateAsync(State.QueryLocationSucceeded, "");
throw new Exception();
}
// Query geolocation.
var ctsLocation = new CancellationTokenSource();
try
{
closingLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<T>().Information("Returning closed bike {Bike} is not possible. Geolocation query failed. {Exception}", bike, ex);
await InvokeCurrentStateAsync(State.QueryLocationFailed, ex.Message);
throw;
}
await InvokeCurrentStateAsync(State.QueryLocationSucceeded, string.Empty);
// Update last lock state time
// save geolocation data for sending to backend
var currentLocationDto = closingLockLocation != null
? new LocationDto.Builder
{
Latitude = closingLockLocation.Latitude,
Longitude = closingLockLocation.Longitude,
Accuracy = closingLockLocation.Accuracy ?? double.NaN,
Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(closingLockLocation.Timestamp.DateTime),
}.Build()
: null;
return currentLocationDto;
}
}
}

View file

@ -1,7 +1,27 @@
using System.Threading.Tasks;
using TINK.Repository.Request;
using TINK.ViewModel.Bikes.Bike.BluetoothLock;
using static TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command.GetLockedLocationCommand;
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IBikeInfoMutable : BC.IBikeInfoMutable
{
ILockInfoMutable LockInfo { get; }
/// <summary>
/// Closes the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
Task CloseLockAsync(ICloseCommandListener listener, Task stopPollingTask);
/// <summary>
/// Gets the location of the locked bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
Task<LocationDto> GetLockedBikeLocationAsync(IGetLockedLocationCommandListener listener);
}
}

View file

@ -64,7 +64,7 @@ namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock
}
}
/// <summary> Gets the timestamp of the last locking state change.</summary>
/// <summary> Gets the time stamp of the last locking state change.</summary>
public DateTime? LastLockingStateChange { get; private set; }
/// <summary>