Version 3.0.381

This commit is contained in:
Anja 2024-04-09 12:53:23 +02:00
parent f963c0a219
commit 3a363acf3a
1525 changed files with 60589 additions and 125098 deletions

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
{
public abstract class BikeInfo : IBikeInfo
{
/// <summary> Default value of demo property. </summary>
public const bool DEFAULTVALUEISDEMO = false;
/// <summary> Holds the info about the bike state. </summary>
private readonly IStateInfo _StateInfo;
/// <summary>
/// Holds the bike object.
/// </summary>
public Bike Bike { get; }
/// <summary>
/// Holds the drive object.
/// </summary>
public DriveMutable Drive { get; }
/// <summary> Gets the information where the data origins from. </summary>
public DataSource DataSource { get; }
/// <summary> Constructs a bike object.</summary>
/// <param name="dataSource">Specified the source of the data.</param>
protected BikeInfo(
IStateInfo stateInfo,
Bike bike,
DriveMutable drive,
DataSource dataSource,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null,
string stationId = null,
Uri operatorUri = null,
RentalDescription tariffDescription = null)
{
Bike = bike ?? throw new ArgumentNullException(nameof(bike));
Drive = drive ?? throw new ArgumentNullException(nameof(drive));
DataSource = dataSource;
_StateInfo = stateInfo;
IsDemo = isDemo ?? DEFAULTVALUEISDEMO;
Group = group ?? new List<string>();
StationId = stationId;
OperatorUri = operatorUri;
TariffDescription = tariffDescription;
}
public BikeInfo(BikeInfo bikeInfo) : this(
bikeInfo != null ? bikeInfo?.State : throw new ArgumentNullException(nameof(bikeInfo)),
bikeInfo.Bike,
bikeInfo.Drive,
bikeInfo.DataSource,
bikeInfo.IsDemo,
bikeInfo.Group,
bikeInfo.StationId,
bikeInfo.OperatorUri,
bikeInfo.TariffDescription)
{ }
/// <summary> True if device is demo device, false otherwise. </summary>
public bool IsDemo { get; }
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
public IEnumerable<string> Group { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
public string StationId { get; }
/// <summary> Holds description about the tariff. </summary>
public RentalDescription TariffDescription { get; }
/// Holds the rent state of the bike.
/// </summary>
public IStateInfo State
{
get { return _StateInfo; }
}
public string Id => Bike.Id;
public WheelType? WheelType => Bike.WheelType;
public TypeOfBike? TypeOfBike => Bike.TypeOfBike;
/// <summary> Gets the model of the lock. </summary>
public LockModel LockModel => Bike.LockModel;
public string Description => Bike.Description;
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
public Uri OperatorUri { get; }
/// <summary>
/// Converts the instance to text.
/// </summary>
public override string ToString()
{
return $"Id={Bike.Id}{(Bike.WheelType != null ? $", wheel(s)={Bike.WheelType}" : string.Empty)}{(Bike.TypeOfBike != null ? $"type={Bike.TypeOfBike}" : "")}, state={State}, location={(!string.IsNullOrEmpty(StationId) ? $"Station {StationId}" : "On the road")}, is demo={IsDemo}.";
}
}
}

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
{
[DataContract]
public abstract class BikeInfoMutable : IBikeInfoMutable, INotifyPropertyChanged
{
/// <summary> Holds the bike. </summary>
private readonly Bike _Bike;
/// <summary> Holds the drive of the bike. </summary>
private readonly DriveMutable _Drive;
/// <summary> Holds the state info of the bike. </summary>
private readonly StateInfoMutable _StateInfo;
/// <summary>
/// Constructs a bike info object.
/// </summary>
/// <param name="isDemo">True if device is demo device, false otherwise.</param>
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
/// <param name="stationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="stateInfo">Bike state info.</param>
protected BikeInfoMutable(
Bike bike,
DriveMutable drive,
DataSource dataSource,
bool isDemo = BikeInfo.DEFAULTVALUEISDEMO,
IEnumerable<string> group = null,
string stationId = null,
string stationName = null,
Uri operatorUri = null,
IRentalDescription tariffDescription = null,
Func<DateTime> dateTimeProvider = null,
IStateInfo stateInfo = null)
{
IsDemo = isDemo;
Group = group;
_Bike = bike;
_Drive = drive;
DataSource = dataSource;
_StateInfo = new StateInfoMutable(dateTimeProvider, stateInfo);
_StateInfo.PropertyChanged += (sender, eventargs) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(eventargs.PropertyName));
StationId = stationId;
StationName = stationName;
OperatorUri = operatorUri;
TariffDescription = tariffDescription;
}
/// <summary> Id of station a which bike is located, null otherwise.</summary>
[DataMember]
public string StationId { get; private set; }
/// <summary> Name of station a which bike is located, null otherwise. </summary>
[DataMember]
public string StationName { get; }
/// <summary> Holds description about the tariff. </summary>
[DataMember]
public IRentalDescription TariffDescription { get; private set; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
[DataMember]
public StateInfoMutable State
{
get { return _StateInfo; }
}
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
public Uri OperatorUri { get; }
/// <summary> Unused member. </summary>
IStateInfoMutable IBikeInfoMutable.State => _StateInfo;
public string Id => _Bike.Id;
public bool IsDemo { get; }
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
public IEnumerable<string> Group { get; }
public WheelType? WheelType => _Bike.WheelType;
public TypeOfBike? TypeOfBike => _Bike.TypeOfBike;
/// <summary>
/// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
/// </summary>
public AaRideType? AaRideType => _Bike.AaRideType;
public LockModel LockModel => _Bike.LockModel;
public string Description => _Bike.Description;
public DriveMutable Drive => _Drive;
/// <summary>
/// Fired whenever property of bike changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private DataSource _DataSource = DataSource.Copri;
/// <summary> Gets or sets the information where the data origins from. </summary>
public DataSource DataSource
{
get => _DataSource;
set
{
if (_DataSource == value)
return;
_DataSource = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DataSource)));
}
}
/// <summary> Loads a bike object from copri server booking_cancel (cancel reservation)/ booking_update (return bike) response.</summary>
/// <param name="bike">Bike object to load response into.</param>
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
/// <param name="stationId">Id of the station if bike station changed, null otherwise.</param>
public void Load(
NotifyPropertyChangedLevel notifyLevel,
string stationId = null)
{
State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel);
if (stationId == null)
{
// Station did not change.
return;
}
StationId = stationId;
}
/// <summary>
/// Converts the instance to text.
/// </summary>
public override string ToString()
{
return $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $", type={TypeOfBike}" : "")}, demo={IsDemo}, state={State.ToString()}, location={(!string.IsNullOrEmpty(StationId) ? $"Station {StationId}" : "On the road")}.";
}
}
}

View file

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
{
/// <summary>
/// Allows to access bike info.
/// </summary>
public interface IBikeInfo
{
/// <summary>
/// Holds the bike object.
/// </summary>
BikeNS.Bike Bike { get; }
/// <summary>
/// Holds the drive.
/// </summary>
DriveMutable Drive { get; }
/// <summary> Gets or sets the information where the data origins from. </summary>
DataSource DataSource { get; }
/// <summary> True if bike is a demo bike. </summary>
bool IsDemo { get; }
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
IEnumerable<string> Group { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
string StationId { get; }
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
Uri OperatorUri { get; }
/// <summary> Holds description about the tariff. </summary>
RentalDescription TariffDescription { get; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
IStateInfo State { get; }
}
/// <summary>
/// Origin of the data.
/// </summary>
public enum DataSource
{
/// <summary>
/// Data source copri.
/// </summary>
Copri,
/// <summary>
/// Data source cache.
/// </summary>
Cache
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
{
public interface IBikeInfoMutable
{
/// <summary>
/// Holds the unique id of the bike;
/// </summary>
string Id { get; }
/// <summary> True if bike is a demo bike. </summary>
bool IsDemo { get; }
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
IEnumerable<string> Group { get; }
/// <summary>
/// Holds the count of wheels.
/// </summary>
WheelType? WheelType { get; }
/// <summary>
/// Holds the type of bike.
/// </summary>
TypeOfBike? TypeOfBike { get; }
/// <summary>
/// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
/// </summary>
AaRideType? AaRideType { get; }
/// <summary> Gets the model of the lock. </summary>
LockModel LockModel { get; }
/// <summary> Holds the description of the bike. </summary>
string Description { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
string StationId { get; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
IStateInfoMutable State { get; }
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
Uri OperatorUri { get; }
/// <summary>
/// Hold the drive object.
/// </summary>
DriveMutable Drive { get; }
/// <summary> Gets or sets the information where the data origins from. </summary>
DataSource DataSource { get; set; }
/// <summary>
/// Gets the rental description.
/// </summary>
IRentalDescription TariffDescription { get; }
/// <summary> Loads a bike object from copri server booking_cancel (cancel reservation)/ booking_update (return bike) response.</summary>
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
/// <param name="stationId">Id of the station if bike station changed, null otherwise.</param>
void Load(
NotifyPropertyChangedLevel notifyLevel,
string stationId = null);
event PropertyChangedEventHandler PropertyChanged;
}
public enum NotifyPropertyChangedLevel
{
/// <summary> Notify about all property changes.</summary>
All,
/// <summary> Notify about no property changes.</summary>
None
}
}

View file

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS
{
/// <summary> Count of wheels. </summary>
/// <remarks> Numeric values of enum must match count of wheels</remarks>
public enum WheelType
{
Mono = 1,
Two = 2,
Trike = 3,
Quad = 4
}
/// <summary> Type of bike. </summary>
public enum TypeOfBike
{
Allround = 0,
Cargo = 1,
City = 2,
}
/// <summary> Holds whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).</summary>
public enum AaRideType
{
NoAaRide = 0,
AaRide = 1,
}
/// <summary> Holds the model of lock. </summary>
public enum LockModel
{
ILockIt, // haveltec GbmH Brandenburg, Germany bluetooth lock
BordComputer, // TeilRad BC
Sigo, // Sigo GmbH Darmstadt, Germany bike lock
}
/// <summary> Holds the type of lock. </summary>
public enum LockType
{
Backend, // Backend, i.e. COPRI controls lock (open, close, ...)
Bluethooth, // Lock is controlled.
}
public class Bike : IEquatable<Bike>
{
/// <summary>
/// Constructs a bike.
/// </summary>
/// <param name="id">Unique id of bike.</param>
public Bike(
string id,
LockModel lockModel,
WheelType? wheelType = null,
TypeOfBike? typeOfBike = null,
AaRideType? aaRideType = null,
string description = null)
{
WheelType = wheelType;
TypeOfBike = typeOfBike;
AaRideType = aaRideType;
LockModel = lockModel;
Id = id;
Description = description;
}
/// <summary>
/// Holds the unique id of the bike;
/// </summary>
public string Id { get; }
/// <summary>
/// Holds the count of wheels.
/// </summary>
public WheelType? WheelType { get; }
/// <summary>
/// Holds the type of bike.
/// </summary>
public TypeOfBike? TypeOfBike { get; }
/// <summary>
/// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
/// </summary>
public AaRideType? AaRideType { get; }
/// <summary> Gets the model of the lock. </summary>
public LockModel LockModel { get; private set; }
/// <summary> Holds the description of the bike. </summary>
public string Description { get; }
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public override bool Equals(object obj)
{
var l_oBike = obj as Bike;
if (l_oBike == null)
{
return false;
}
return Equals(l_oBike);
}
/// <summary> Converts the instance to text.</summary>
public override string ToString()
{
return WheelType == null || TypeOfBike == null
? $"Id={Id}{(!string.IsNullOrEmpty(Description) ? $", {Description}" : "")}"
: $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $"type={TypeOfBike}" : "")}.";
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public bool Equals(Bike other)
{
return other != null &&
Id == other.Id &&
WheelType == other.WheelType &&
TypeOfBike == other.TypeOfBike
&& Description == other.Description;
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public static bool operator ==(Bike bike1, Bike bike2)
{
return EqualityComparer<Bike>.Default.Equals(bike1, bike2);
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public static bool operator !=(Bike bike1, Bike bike2)
{
return !(bike1 == bike2);
}
/// <summary>
/// Generates hash code for bike object.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
var hashCode = -390870100;
hashCode = hashCode * -1521134295 + Id.GetHashCode();
hashCode = hashCode * -1521134295 + WheelType?.GetHashCode() ?? 0;
hashCode = hashCode * -1521134295 + TypeOfBike?.GetHashCode() ?? 0;
hashCode = hashCode * -1521134295 + Description?.GetHashCode() ?? 0;
return hashCode;
}
}
}

View file

@ -0,0 +1,22 @@
using System;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS
{
public static class BikeExtension
{
public static LockType GetLockType(this LockModel model)
{
switch (model)
{
case LockModel.ILockIt:
return LockType.Bluethooth;
case LockModel.Sigo:
return LockType.Backend;
default:
throw new ArgumentException($"Unsupported lock model {model} detected.");
}
}
}
}

View file

@ -0,0 +1,126 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
/// <summary>
/// Provides functionality to get the locked bike location.
/// </summary>
public static class AuthCommand
{
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum Step
{
Authenticate,
}
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum State
{
WebConnectFailed,
GeneralAuthError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface IAuthCommandListener
{
/// <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 InvokeAsync<T>(
IBikeInfoMutable bike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IAuthCommandListener listener)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
//// Step: Authenticate user
InvokeCurrentStep(Step.Authenticate);
try
{
// Repeat reservation to get a new seed/ k_user value.
await connectorFactory(true).Command.CalculateAuthKeys(bike);
Log.ForContext<T>().Information("Calculation of AuthKeys successfully.");
}
catch (Exception exception)
{
if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<T>().Error("Calculation of AuthKeys failed (Copri server not reachable).");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else
{
Log.ForContext<T>().Error("Calculation of AuthKeys failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralAuthError, exception.Message);
}
throw;
}
}
}
}

View file

@ -0,0 +1,133 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
public static class CancelReservationCommand
{
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum Step
{
CancelReservation,
}
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum State
{
WebConnectFailed,
InvalidResponse,
GeneralCancelReservationError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of returning bike process.
/// </summary>
public interface ICancelReservationCommandListener
{
/// <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 InvokeAsync<T>(
IBikeInfoMutable bike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
ICancelReservationCommandListener listener)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
// Cancel Reservation
InvokeCurrentStep(Step.CancelReservation);
try
{
await connectorFactory(true).Command.DoCancelReservation(bike);
Log.ForContext<T>().Information("User canceled reservation of bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<ReservedDisconnected>().Information("User selected reserved bike {bikeId} but cancel reservation failed.", bike.Id);
if (exception is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<T>().Error("Invalid auth. response.");
await InvokeCurrentStateAsync(State.InvalidResponse, exception.Message);
}
else if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<T>().Error("Copri server not reachable.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralCancelReservationError, exception.Message);
}
throw;
}
}
}
}

View file

@ -0,0 +1,257 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Repository.Request;
using System.Threading;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.MultilingualResources;
using Xamarin.Essentials;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
public static class EndRentalCommand
{
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum Step
{
GetLocation,
ReturnBike,
}
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum State
{
GPSNotSupportedException,
GPSNotEnabledException,
NoGPSPermissionsException,
GeneralQueryLocationFailed,
WebConnectFailed,
NotAtStation,
NoGPSData,
ResponseException,
GeneralEndRentalError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of returning bike process.
/// </summary>
public interface IEndRentalCommandListener
{
/// <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>
/// End rental.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<BookingFinishedModel> InvokeAsync<T>(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func<DateTime> dateTimeProvider = null,
Func<bool> isConnectedDelegate = null,
Func<bool, IConnector> connectorFactory = null,
IEndRentalCommandListener listener = null)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
// Get Location
//// Step: Start query geolocation data.
InvokeCurrentStep(Step.GetLocation);
// Get geolocation which was requested when closing lock.
var closingLockLocation = bike.LockInfo.Location;
LocationDto endRentalLocation;
if (closingLockLocation != null)
{
// Location was available when closing bike. No further actions required.
endRentalLocation =
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();
}
else
{
IGeolocation newLockLocation = null;
// Check if bike is around
var deviceState = lockService[bike.LockInfo.Id].GetDeviceState();
// Geolocation can not be queried because bike is not around.
if (deviceState != DeviceState.Connected)
{
Log.ForContext<T>().Information("There is no geolocation information available since lock of bike {bikeId} is not connected", bike.Id);
// no GPS data exception.
//NoGPSDataException.IsNoGPSData("There is no geolocation information available since lock is not connected", out NoGPSDataException exception);
await InvokeCurrentStateAsync(State.NoGPSData, "There is no geolocation information available since lock is not connected");
return null; // return empty BookingFinishedModel
}
else
{
// Bike is around -> Query geolocation.
var ctsLocation = new CancellationTokenSource();
try
{
newLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now);
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Geolocation query failed.");
if (exception is FeatureNotSupportedException)
{
Log.ForContext<T>().Error("Location service are not supported on device.");
await InvokeCurrentStateAsync(State.GPSNotSupportedException, exception.Message);
}
if (exception is FeatureNotEnabledException)
{
Log.ForContext<T>().Error("Location service are off.");
await InvokeCurrentStateAsync(State.GPSNotEnabledException, exception.Message);
}
if (exception is PermissionException)
{
Log.ForContext<T>().Error("No location service permissions granted.");
await InvokeCurrentStateAsync(State.NoGPSPermissionsException, exception.Message);
}
else
{
Log.ForContext<T>().Information("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralQueryLocationFailed, exception.Message);
}
throw;
}
}
// Update last lock state time
// save geolocation data for sending to backend
endRentalLocation = newLockLocation != null
? new LocationDto.Builder
{
Latitude = newLockLocation.Latitude,
Longitude = newLockLocation.Longitude,
Accuracy = newLockLocation.Accuracy ?? double.NaN,
Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(newLockLocation.Timestamp.DateTime),
}.Build()
: null;
}
// Return bike
InvokeCurrentStep(Step.ReturnBike);
BookingFinishedModel bookingFinished;
try
{
bookingFinished = await connectorFactory(true).Command.DoReturn(
bike,
endRentalLocation);
Log.ForContext<T>().Information("Rental of bike {bikeId} was terminated successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Rental of bike {bikeId} can not be terminated.", bike.Id);
if (exception is WebConnectFailureException)
{
// No web.
Log.ForContext<T>().Error("Copri server not reachable. No web.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else if (exception is NotAtStationException notAtStationException)
{
// not at station.
Log.ForContext<T>().Error("COPRI returned out of GEO fencing error. Position send to COPRI {position}.", endRentalLocation);
await InvokeCurrentStateAsync(State.NotAtStation, string.Format(AppResources.ErrorEndRentalNotAtStation, notAtStationException.StationNr, notAtStationException.Distance));
// reset location -> at next try query new location data
bike.LockInfo.Location = null;
}
else if (exception is NoGPSDataException)
{
// no GPS data.
Log.ForContext<T>().Error("COPRI returned a no-GPS-data error.");
await InvokeCurrentStateAsync(State.NoGPSData, exception.Message);
// reset location -> at next try query new location data
bike.LockInfo.Location = null;
}
else if (exception is ResponseException copriException)
{
// COPRI exception.
Log.ForContext<T>().Error("COPRI returned an error. {response}", copriException.Response);
await InvokeCurrentStateAsync(State.ResponseException, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralEndRentalError, exception.Message);
}
throw;
}
return bookingFinished;
}
}
}

View file

@ -0,0 +1,127 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
public static class StartRentalCommand
{
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum Step
{
RentBike,
}
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum State
{
WebConnectFailed,
GeneralStartRentalError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface IStartRentalCommandListener
{
/// <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 InvokeAsync<T>(
IBikeInfoMutable bike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IStartRentalCommandListener listener)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
//// Step: Book bike
InvokeCurrentStep(Step.RentBike);
try
{
if (bike.LockInfo.State != LockingState.Open)
{
await connectorFactory(true).Command.DoBookAsync(bike, LockingAction.Open);
}
else
{
await connectorFactory(true).Command.DoBookAsync(bike);
}
Log.ForContext<T>().Information("User booked bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Booking of bike {bikeId} failed.", bike.Id);
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<T>().Error("Copri server not reachable.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralStartRentalError, exception.Message);
}
throw;
}
}
}
}

View file

@ -0,0 +1,133 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
/// <summary>
/// Provides functionality to get the locked bike location.
/// </summary>
public static class StartReservationCommand
{
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum Step
{
ReserveBike,
}
/// <summary>
/// Possible steps of requesting a bike.
/// </summary>
public enum State
{
TooManyBikesError,
WebConnectFailed,
GeneralStartReservationError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface IStartReservationCommandListener
{
/// <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 InvokeAsync<T>(
IBikeInfoMutable bike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IStartReservationCommandListener listener)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
//// Step: Reserve Bike
InvokeCurrentStep(Step.ReserveBike);
try
{
await connectorFactory(true).Command.DoReserve(bike);
Log.ForContext<T>().Information("User reserved bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Request to reserve bike {bikeId} declined.", bike.Id);
if (exception is BookingDeclinedException)
{
// Too many bikes booked.
Log.ForContext<T>().Error("Maximum count of bikes {exception.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount);
await InvokeCurrentStateAsync(State.TooManyBikesError, (exception as BookingDeclinedException).MaxBikesCount.ToString());
}
else if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<T>().Error("Copri server not reachable.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralStartReservationError, exception.Message);
}
throw;
}
}
}
}

View file

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class BikeInfo : BC.BikeInfo, IBikeInfo
{
/// <summary>
/// Constructs a bike info object for a available bike.
/// </summary>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="currentStationId">Id of station where bike is located.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
string currentStationId,
Uri operatorUri = null,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(),
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid }.Build();
}
/// <summary>
/// Constructs a bike info object for a requested bike.
/// </summary>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="requestedAt">Date time when bike was requested</param>
/// <param name="mailAddress">Mail address of user which requested bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="dateTimeProvider">Date time provider to calculate remaining time.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
DateTime requestedAt,
string mailAddress,
string currentStationId,
Uri operatorUri,
RentalDescription tariffDescription,
Func<DateTime> dateTimeProvider,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
dateTimeProvider,
requestedAt,
tariffDescription?.MaxReservationTimeSpan ?? StateRequestedInfo.UNKNOWNMAXRESERVATIONTIMESPAN,
mailAddress,
"" ), // BC code
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
}
/// <summary>
/// Constructs a bike info object for a booked bike.
/// </summary>
/// <param name="bike">Unique id of bike.</param>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="bookedAt">Date time when bike was booked</param>
/// <param name="mailAddress">Mail address of user which booked bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="wheelType"></param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
DateTime bookedAt,
string mailAddress,
string currentStationId,
Uri operatorUri,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
bookedAt,
mailAddress,
""),
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
}
public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base(
bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null."))
{
LockInfo = lockInfo
?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null.");
}
public LockInfo LockInfo { get; private set; }
}
}

View file

@ -0,0 +1,240 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Request;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.ViewModel;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
namespace ShareeBike.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 geolocation for end rental.</summary>
private LocationDto EndRentalLocation { 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>
/// <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)),
bike.Drive,
bike.DataSource,
bike.IsDemo,
bike.Group,
bike.StationId,
stationName,
bike.OperatorUri,
bike.TariffDescription,
() => DateTime.Now,
bike.State)
{
LockInfo = new LockInfoMutable(
bike.LockInfo.Id,
bike.LockInfo.Guid,
bike.LockInfo.UserKey,
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>
/// Connects the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
public async Task ConnectAsync(
IConnectAndGetStateCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
listener);
}
/// <summary>
/// Opens 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 OpenLockAsync(
IOpenCommandListener listener,
Task stopPollingTask)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
IsConnectedDelegate,
ConnectorFactory,
listener,
stopPollingTask);
}
/// <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>
/// Disconnects the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
public async Task DisconnectAsync(
IDisconnectCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
listener);
}
/// <summary>
/// Reserves the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task ReserveBikeAsync(
IStartReservationCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Cancels the reservation.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task CancelReservationAsync(
ICancelReservationCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Authenticates the user.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task AuthAsync(
IAuthCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Rents the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task RentBikeAsync(
IStartRentalCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Returns the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task<BookingFinishedModel> ReturnBikeAsync(
IEndRentalCommandListener listener)
{
var bookingFinished = await InvokeAsync<BikeInfoMutable>(
this,
GeolocationService,
LockService,
() => DateTime.Now,
IsConnectedDelegate,
ConnectorFactory,
listener);
return bookingFinished;
}
public override string ToString()
{
return $"Id={Id}{(TypeOfBike != null ? $";type={TypeOfBike}" : "")};state={State.ToString()};Lock id={LockInfo.Id}";
}
}
}

View file

@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Exception;
using ShareeBike.Repository.Request;
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 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 states 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> connectorFactory,
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 exception)
{
Log.ForContext<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, 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>().Information($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
try
{
await Task.WhenAll(new List<Task> { locationTask, stopPollingTask ?? Task.CompletedTask });
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Canceling query location/ wait for polling task to finish failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.WaitGeolocationException, exception.Message);
InvokeCurrentStep(Step.QueryLocationTerminated);
return null;
}
Log.ForContext<T>().Information($"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 connectorFactory(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);
Log.ForContext<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().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<T>().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<T>().Debug("Message: {Message} Details: {Details}", 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 Action
//// Step: 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);
Log.ForContext<T>().Information("Starting query location successful.");
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Starting query location failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.StartGeolocationException, exception.Message);
}
//// Step: Close lock.
IGeolocation currentLocation;
Log.ForContext<T>().Information($"Starting step {Step.ClosingLock}...");
InvokeCurrentStep(Step.ClosingLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
Log.ForContext<T>().Information("Lock of bike {bikeId} closed successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be closed.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntCloseMovingException)
{
Log.ForContext<T>().Debug("Lock is moving.");
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
}
else if (exception is CouldntCloseBoltBlockedException)
{
Log.ForContext<T>().Debug("Bold is blocked.}");
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
}
else
{
Log.ForContext<T>().Debug("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
}
// Signal cts to cancel getting geolocation.
ctsLocation.Cancel();
// 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;
}
// 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,183 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.BluetoothLock.Exception;
using ShareeBike.Services.BluetoothLock.Tdo;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
using ShareeBike.ViewModel.Bikes;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class ConnectAndGetStateCommand
{
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum Step
{
ConnectLock,
GetLockingState,
}
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum State
{
BluetoothOff,
NoLocationPermission,
LocationServicesOff,
OutOfReachError,
GeneralConnectLockError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
/// </summary>
public interface IConnectAndGetStateCommandListener
{
/// <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>
/// Connect or disconnect lock.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
IConnectAndGetStateCommandListener listener = null)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Connect lock
InvokeCurrentStep(Step.ConnectLock);
LockInfoTdo result = null;
var continueConnect = true;
var retryCount = 1;
while (continueConnect && result == null)
{
try
{
result = await lockService.ConnectAsync(
new LockInfoAuthTdo.Builder { Id = bike.LockInfo.Id, Guid = bike.LockInfo.Guid, K_seed = bike.LockInfo.Seed, K_u = bike.LockInfo.UserKey }.Build(),
lockService.TimeOut.GetSingleConnect(retryCount));
Log.ForContext<T>().Information("Connected to lock of bike {bikeId} successfully. Value is {lockState}.", bike.Id, bike.LockInfo.State);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Connection to lock of bike {bikeId} failed. ", bike.Id);
if (exception is ConnectBluetoothNotOnException)
{
continueConnect = false;
Log.ForContext<T>().Error("Bluetooth not on.");
await InvokeCurrentStateAsync(State.BluetoothOff, exception.Message);
}
else if (exception is ConnectLocationPermissionMissingException)
{
continueConnect = false;
Log.ForContext<T>().Error("Location permission missing.");
await InvokeCurrentStateAsync(State.NoLocationPermission, exception.Message);
}
else if (exception is ConnectLocationOffException)
{
continueConnect = false;
Log.ForContext<T>().Error("Location services not on.");
await InvokeCurrentStateAsync(State.LocationServicesOff, exception.Message);
}
else if (exception is OutOfReachException)
{
continueConnect = false;
Log.ForContext<T>().Error("Lock is out of reach.");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
string message;
if (retryCount < 2)
{
message = AppResources.ErrorConnectLock;
}
else if (retryCount < 3)
{
message = AppResources.ErrorConnectLockEscalationLevel1;
}
else
{
message = AppResources.ErrorConnectLockEscalationLevel2;
continueConnect = false;
}
await InvokeCurrentStateAsync(State.GeneralConnectLockError, message);
}
if (continueConnect)
{
retryCount++;
continue;
}
throw;
}
}
// Get locking state
InvokeCurrentStep(Step.GetLockingState);
bike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.UnknownDisconnected;
if (bike.LockInfo.State == LockingState.UnknownDisconnected)
{
// Do not display any messages here, because search is implicit.
Log.ForContext<T>().Error("Lock is still not connected.");
}
bike.LockInfo.Guid = result?.Guid ?? new Guid();
}
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Services.BluetoothLock;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class DisconnectCommand
{
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum Step
{
DisconnectLock,
}
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum State
{
GeneralDisconnectError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
/// </summary>
public interface IDisconnectCommandListener
{
/// <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>
/// Connect or disconnect lock.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
IDisconnectCommandListener listener = null)
{
// 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<T>().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<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Disconnect lock
InvokeCurrentStep(Step.DisconnectLock);
if (bike.LockInfo.State != LockingState.UnknownDisconnected)
{
try
{
bike.LockInfo.State = await lockService.DisconnectAsync(bike.LockInfo.Id, bike.LockInfo.Guid);
Log.ForContext<T>().Information("Lock from bike {bikeId} disconnected successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock from bike {bikeId} can not be disconnected. {@exception}", bike.Id, exception);
await InvokeCurrentStateAsync(State.GeneralDisconnectError, exception.Message);
}
}
}
}
}

View file

@ -0,0 +1,291 @@
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
{
/// <summary>
/// Possible steps of opening a lock.
/// </summary>
public enum Step
{
OpeningLock,
WaitStopPolling,
GetLockInfos,
UpdateLockingState,
}
/// <summary>
/// Possible states of opening a lock.
/// </summary>
public enum State
{
StopPollingFailed,
OutOfReachError,
CouldntOpenBoldStatusIsUnknownError,
CouldntOpenBoldIsBlockedError,
CouldntOpenInconsistentStateError,
GeneralOpenError,
WebConnectFailed,
ResponseIsInvalid,
BackendUpdateFailed
}
/// <summary>
/// Interface to notify view model about steps/ state changes of opening process.
/// </summary>
public interface IOpenCommandListener
{
/// <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>
/// Opens the lock and updates copri.
/// </summary>
/// <param name="listener">Interface to notify view model about steps/ state changes of opening process.</param>
/// <param name="stopPolling">Task which stops polling.</param>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> 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<T>().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<T>().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<T>().Information($"Waiting on stop polling to finish...");
try
{
await Task.WhenAll(new List<Task> { stopPollingTask ?? Task.CompletedTask });
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Wait for polling task to finish failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.StopPollingFailed, exception.Message);
return;
}
Log.ForContext<T>().Information($"Stop polling finished.");
return;
}
// Get lock infos
async Task GetLockInfos()
{
Log.ForContext<T>().Debug($"Starting step {Step.GetLockInfos}...");
InvokeCurrentStep(Step.GetLockInfos);
// get current charging level
try
{
bike.LockInfo.BatteryPercentage = await lockService[bike.LockInfo.Id].GetBatteryPercentageAsync();
Log.ForContext<T>().Information("Lock infos of bike {bikeId} read successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock infos could not be read.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
}
else
{
Log.ForContext<T>().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<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().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<T>().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<T>().Debug("Message: {Message} Details: {Details}", 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 Action
//// Step: Open lock.
Log.ForContext<T>().Information($"Starting step {Step.OpeningLock}...");
InvokeCurrentStep(Step.OpeningLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].OpenAsync();
Log.ForContext<T>().Information("Lock of bike {bikeId} opened successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be opened.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntOpenBoldStatusIsUnknownException)
{
Log.ForContext<T>().Debug("Lock status is unknown.");
await InvokeCurrentStateAsync(State.CouldntOpenBoldStatusIsUnknownError, exception.Message);
}
else if (exception is CouldntOpenBoldIsBlockedException)
{
Log.ForContext<T>().Debug("Bold is blocked.}");
await InvokeCurrentStateAsync(State.CouldntOpenBoldIsBlockedError, exception.Message);
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<T>().Debug("Lock reports that it is still closed.}");
await InvokeCurrentStateAsync(State.CouldntOpenInconsistentStateError, exception.Message);
}
else
{
Log.ForContext<T>().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();
}
}
}

View file

@ -0,0 +1,6 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IBikeInfo : BC.IBikeInfo
{
}
}

View file

@ -0,0 +1,76 @@
using System.Threading.Tasks;
using ShareeBike.Repository.Request;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IBikeInfoMutable : BC.IBikeInfoMutable
{
ILockInfoMutable LockInfo { get; }
/// <summary>
/// Connects the lock.
/// </summary>
/// <param name="listener">View model to process connect notifications.</param>
Task ConnectAsync(IConnectAndGetStateCommandListener listener);
/// <summary>
/// Disconnects the lock.
/// </summary>
/// <param name="listener">View model to process disconnect notifications.</param>
Task DisconnectAsync(IDisconnectCommandListener listener);
/// <summary>
/// Opens the lock.
/// </summary>
/// <param name="listener">View model to process opening notifications.</param>
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
Task OpenLockAsync(IOpenCommandListener listener, Task stopPollingTask);
/// <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>
/// Reserves the bike.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task ReserveBikeAsync(IStartReservationCommandListener listener);
/// <summary>
/// Cancels the reservation.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task CancelReservationAsync(ICancelReservationCommandListener listener);
/// <summary>
/// Authenticates the user.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task AuthAsync(IAuthCommandListener listener);
/// <summary>
/// Rents the bike.
/// </summary>
/// <param name="listener">View model to process renting notifications.</param>
Task RentBikeAsync(IStartRentalCommandListener listener);
/// <summary>
/// Returns the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
Task<BookingFinishedModel> ReturnBikeAsync(IEndRentalCommandListener listener);
}
}

View file

@ -0,0 +1,37 @@
using System;
using ShareeBike.Services.Geolocation;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface ILockInfoMutable
{
/// <summary> Identification number of bluetooth lock.</summary>
int Id { get; }
/// <summary> Gets the user key.</summary>
byte[] UserKey { get; }
LockingState State { get; set; }
/// <summary> Holds the percentage of lock battery.</summary>
double BatteryPercentage { get; set; }
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
Guid Guid { get; set; }
byte[] Seed { get; }
/// <summary> Timestamp of the last locking state change.</summary>
DateTime? LastLockingStateChange { get; }
/// <summary>
/// Gets or sets the current location of the bike, null if location is unknown.
/// </summary>
IGeolocation Location { get; set; }
/// <summary>
/// Gets the version info of the locks.
/// </summary>
IVersionInfo VersionInfo { get; set; }
}
}

View file

@ -0,0 +1,20 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IVersionInfo
{
/// <summary>
/// Holds the firmware version of the lock.
/// </summary>
int FirmwareVersion { get; }
/// <summary>
/// Holds the hardware version (revision) of the lock.
/// </summary>
int HardwareVersion { get; }
/// <summary>
/// Holds lock version (2 classic, 3 plus, 4 GPS).
/// </summary>
int LockVersion { get; }
}
}

View file

@ -0,0 +1,89 @@
using System;
using ShareeBike.Services.Geolocation;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class LockInfoMutable : ILockInfoMutable
{
/// <summary> Lock info object. </summary>
private LockInfo LockInfo { get; set; }
/// <summary>
/// Delegate to create time stamp.
/// </summary>
private Func<DateTime> _nowDelegate;
/// <summary> Constructs a bluetooth lock info object. </summary>
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
/// <param name="nowDelegate">Delegate to create time stamp if null DateTime.Now is used.</param>
public LockInfoMutable(
int id,
Guid guid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
LockingState state,
Func<DateTime> nowDelegate = null)
{
_nowDelegate = nowDelegate ?? (() => DateTime.Now);
LockInfo = new LockInfo.Builder() { Id = id, Guid = guid, UserKey = userKey, AdminKey = adminKey, Seed = seed, State = state }.Build();
}
public int Id => LockInfo.Id;
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
public Guid Guid
{
get => LockInfo.Guid;
set => LockInfo = new LockInfo.Builder(LockInfo) { Guid = value }.Build();
}
public byte[] Seed => LockInfo.Seed;
public byte[] UserKey => LockInfo.UserKey;
public byte[] AdminKey => LockInfo.AdminKey;
/// <summary>
/// Gets or sets the locking state.
/// </summary>
public LockingState State
{
get => LockInfo.State;
set
{
if (LockInfo.State == value)
{
// State does not change, nothing to do.
return;
}
Location = null; // Invalidate location.
LastLockingStateChange = _nowDelegate(); // Get time stamp when state change happened.
LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
}
}
/// <summary> Gets the time stamp of the last locking state change.</summary>
public DateTime? LastLockingStateChange { get; private set; }
/// <summary>
/// Gets or sets the current location of the bike, null if location is unknown.
/// </summary>
public IGeolocation Location { get; set; }
/// <summary> Holds the percentage of lock battery.</summary>
public double BatteryPercentage { get; set; } = double.NaN;
/// <summary>
/// Gets the version info of the lock.
/// </summary>
public IVersionInfo VersionInfo { get; set; } = new VersionInfo.Builder().Build();
/// <summary> Loads lock info object from values. </summary>
public void Load(int id, Guid guid, byte[] seed, byte[] userKey, byte[] adminKey)
{
LockInfo = new LockInfo.Builder(LockInfo) { Id = id, Guid = guid, Seed = seed, UserKey = userKey, AdminKey = adminKey }.Build();
}
}
}

View file

@ -0,0 +1,69 @@
using Newtonsoft.Json;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class VersionInfo : IVersionInfo
{
/// <summary>
/// Holds info about firmware- and hardware version of a lock and the type of lock (lock version).
/// </summary>
private VersionInfo() { }
/// <summary>
/// Holds the firmware version of the lock.
/// </summary>
public int FirmwareVersion { get; private set; } = 0;
/// <summary>
/// Holds the hardware version (revision) of the lock.
/// </summary>
public int HardwareVersion { get; private set; } = 0;
/// <summary>
/// Holds lock version (2 classic, 3 plus, 4 GPS).
/// </summary>
public int LockVersion { get; private set; } = 0;
public override bool Equals(object obj)
=> Equals(obj as VersionInfo);
public bool Equals(VersionInfo other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
return ToString() == other.ToString();
}
public override int GetHashCode() => ToString().GetHashCode();
public override string ToString() => JsonConvert.SerializeObject(this);
public static bool operator ==(VersionInfo lhs, VersionInfo rhs)
{
if (ReferenceEquals(lhs, null))
return ReferenceEquals(rhs, null) ? true /*null == null = true*/: false;
return lhs.Equals(rhs);
}
public static bool operator !=(VersionInfo lhs, VersionInfo rhs)
=> !(lhs == rhs);
public class Builder
{
private VersionInfo versionInfo = new VersionInfo();
public int FirmwareVersion { get => versionInfo.FirmwareVersion; set => versionInfo.FirmwareVersion = value; }
public int HardwareVersion { get => versionInfo.HardwareVersion; set => versionInfo.HardwareVersion = value; }
public int LockVersion { get => versionInfo.LockVersion; set => versionInfo.LockVersion = value; }
public VersionInfo Build()
{
return versionInfo;
}
}
}
}

View file

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.MiniSurvey;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
{
public class BikeInfo : BC.BikeInfo
{
/// <summary>
/// Constructs a bike info object for a available bike or bike for which feed back is pending.
/// </summary>
/// <param name="bike">Bike object.</param>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="currentStationId">Id of station where bike is located.</param>
/// <param name="lockInfo">Lock info.</param>
/// <param name="isFeedbackPending">If true user has not yet given feedback after returning bike.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
string currentStationId,
LockInfo lockInfo,
bool isFeedbackPending = false,
Uri operatorUri = null,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null,
IMiniSurveyModel miniSurvey = null,
string co2Saving = null) : base(
new StateInfo(isFeedbackPending),
bike != null
? new Bike(
bike.Id,
LockModel.Sigo,
bike.WheelType /* Ensure consistent lock model value */,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = lockInfo;
MiniSurvey = miniSurvey;
Co2Saving = co2Saving;
}
/// <summary>
/// Constructs a bike info object for a requested bike.
/// </summary>
/// <param name="bike">Bike object.</param>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="requestedAt">Date time when bike was requested</param>
/// <param name="mailAddress">Mail address of user which requested bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="lockInfo">Lock info.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
DateTime requestedAt,
string mailAddress,
string currentStationId,
LockInfo lockInfo,
Uri operatorUri,
RentalDescription tariffDescription,
Func<DateTime> dateTimeProvider,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
dateTimeProvider,
requestedAt,
tariffDescription?.MaxReservationTimeSpan ?? StateRequestedInfo.UNKNOWNMAXRESERVATIONTIMESPAN,
mailAddress,
""), // BC code
bike != null
? new Bike(
bike.Id,
LockModel.Sigo /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = lockInfo;
MiniSurvey = new MiniSurveyModel();
Co2Saving = string.Empty;
}
/// <summary>
/// Constructs a bike info object for a booked bike.
/// </summary>
/// <param name="bike">Bike object.</param>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="bookedAt">Date time when bike was booked</param>
/// <param name="mailAddress">Mail address of user which booked bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="lockInfo">Lock info.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
DateTime bookedAt,
string mailAddress,
string currentStationId,
LockInfo lockInfo,
Uri operatorUri,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
bookedAt,
mailAddress,
""),
bike != null
? new Bike(
bike.Id,
LockModel.Sigo /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = lockInfo;
MiniSurvey = new MiniSurveyModel();
Co2Saving = string.Empty;
}
public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base(
bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null."))
{
LockInfo = lockInfo
?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null.");
}
/// <summary> Holds the lock info.</summary>
public LockInfo LockInfo { get; private set; }
public IMiniSurveyModel MiniSurvey { get; private set; }
public string Co2Saving { get; private set; }
}
}

View file

@ -0,0 +1,48 @@
using System;
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
{
public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
{
/// <summary> Constructs a bike object from source. </summary>
public BikeInfoMutable(BikeInfo bike, string stationName) : base(
bike != null
? bike.Bike
: throw new ArgumentNullException(nameof(bike)),
bike.Drive,
bike.DataSource,
bike.IsDemo,
bike.Group,
bike.StationId,
stationName,
bike.OperatorUri,
bike.TariffDescription,
() => DateTime.Now,
bike.State)
{
LockInfo = new LockInfoMutable(bike.LockInfo.State);
BookingFinishedModel = new BookingFinishedModel
{
Co2Saving = bike.Co2Saving,
MiniSurvey = new MiniSurvey.MiniSurveyModel()
};
if ((bike?.MiniSurvey?.Questions) == null
|| bike.MiniSurvey.Questions.Count <= 0)
{
// No querries to add.
return;
}
// Add a dummy query. Querries are not yet read from COPRI but compiled into the app.
BookingFinishedModel.MiniSurvey.Questions.Add("q1", new MiniSurvey.QuestionModel());
}
public LockInfoMutable LockInfo { get; }
ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
public IBookingFinishedModel BookingFinishedModel { get; }
}
}

View file

@ -0,0 +1,9 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
{
public interface IBikeInfoMutable : BikeInfoNS.BC.IBikeInfoMutable
{
ILockInfoMutable LockInfo { get; }
IBookingFinishedModel BookingFinishedModel { get; }
}
}

View file

@ -0,0 +1,7 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
{
public interface ILockInfoMutable
{
LockingState State { get; set; }
}
}

View file

@ -0,0 +1,30 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
{
public class LockInfoMutable : ILockInfoMutable
{
/// <summary> Lock info object. </summary>
private LockInfo LockInfo { get; set; }
/// <summary> Constructs a bluetooth lock info object. </summary>
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
public LockInfoMutable(LockingState state)
{
LockInfo = new LockInfo.Builder() { State = state }.Build();
}
public LockingState State
{
get => LockInfo.State;
set => LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
}
/// <summary> Holds the percentage of lock battery.</summary>
public double BatteryPercentage { get; set; } = double.NaN;
/// <summary> Loads lock info object from values. </summary>
public void Load()
{
LockInfo = new LockInfo.Builder(LockInfo) { }.Build();
}
}
}

View file

@ -0,0 +1,113 @@
using Serilog;
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
{
/// <summary>
/// Holds the state of a chargeable battery.
/// </summary>
public class Battery : IBattery
{
private Battery() { }
/// <summary>
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
/// </summary>
public double CurrentChargePercent { get; private set; } = double.NaN;
/// <summary>
/// Gets the current charging level of the battery in bars, null if unknown.
/// </summary>
public int? CurrentChargeBars { get; private set; } = null;
/// <summary>
/// Gets the maximum charging level of the battery in bars, null if unknown.
/// </summary>
public int? MaxChargeBars { get; private set; } = null;
/// <summary>
/// Gets whether backend is aware of battery charging level.
/// </summary>
public bool? IsBackendAccessible { get; private set; } = null;
/// <summary>
/// Gets whether to display battery level or not.
/// </summary>
public bool? IsHidden { get; private set; } = null;
public class Builder
{
/// <summary>
/// Holds the current charging level of the battery in bars.
/// </summary>
public int? CurrentChargeBars { get; set; } = null;
/// <summary>
/// Holds the maximum charging level of the battery in bars.
/// </summary>
public int? MaxChargeBars { get; set; } = null;
/// <summary>
/// Holds the current charging level of the battery in percent.
/// </summary>
public double CurrentChargePercent { get; set; } = double.NaN;
/// <summary>
/// Holds whether backend is aware of battery charging level.
/// </summary>
public bool? IsBackendAccessible { get; set; } = null;
/// <summary>
/// Holds whether to display battery level or not.
/// </summary>
public bool? IsHidden { get; set; } = null;
public Battery Build()
{
if (!double.IsNaN(CurrentChargePercent)
&& (CurrentChargePercent < 0 || 100 < CurrentChargePercent))
{
// Invalid filling level detected
CurrentChargePercent = double.NaN;
}
if (CurrentChargeBars < 0)
{
// Current value of bars must never be smaller zero.
CurrentChargeBars = null;
}
if (MaxChargeBars < 0)
{
// Max value of bars must never be smaller zero.
MaxChargeBars = null;
}
if (CurrentChargeBars != null
&& MaxChargeBars == null)
{
// If current charge bars is set, max charge must be set as well.
Log.ForContext<Battery>().Error($"Current bars value can not be set to {CurrentChargeBars} if max bars is not set.");
CurrentChargeBars = null;
}
if (CurrentChargeBars != null
&& MaxChargeBars != null
&& CurrentChargeBars > MaxChargeBars)
{
// If current charge bars must never be larger than max charge bars.
Log.ForContext<Battery>().Error($"Invalid current bars value {CurrentChargeBars} detected. Value must never be larger than max value bars {MaxChargeBars}.");
CurrentChargeBars = null;
}
return new Battery
{
CurrentChargeBars = CurrentChargeBars,
MaxChargeBars = MaxChargeBars,
CurrentChargePercent = CurrentChargePercent,
IsBackendAccessible = IsBackendAccessible,
IsHidden = IsHidden
};
}
}
}
}

View file

@ -0,0 +1,84 @@
using System.ComponentModel;
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
{
/// <summary>
/// Manages the state of a chargeable battery.
/// </summary>
public class BatteryMutable : IBatteryMutable, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
IBattery _battery;
public BatteryMutable(IBattery battery)
{
_battery = battery;
}
/// <summary>
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
/// </summary>
public double CurrentChargePercent => _battery.CurrentChargePercent;
/// <summary>
/// Gets or sets the current charging level of the battery in bars, null if unknown.
/// </summary>
public int? CurrentChargeBars
{
get => _battery.CurrentChargeBars;
set
{
double GetCurrentChargePercent()
{
if (value == null)
{
// Filling level is unknown.
return double.NaN;
}
if (_battery.MaxChargeBars == null || _battery.MaxChargeBars == 0)
{
// Percentage filling level can not be calculated.
return _battery.CurrentChargePercent;
}
return (int)(100 * value / _battery.MaxChargeBars);
}
if (_battery.CurrentChargeBars == value)
{
// Nothing to do.
return;
}
_battery = new Battery.Builder
{
MaxChargeBars = _battery.MaxChargeBars,
IsBackendAccessible = _battery.IsBackendAccessible,
IsHidden = _battery.IsHidden,
CurrentChargeBars = value,
CurrentChargePercent = GetCurrentChargePercent(),
}.Build();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentChargeBars)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentChargePercent)));
}
}
/// <summary>
/// Gets the maximum charging level of the battery in bars, null if unknown.
/// </summary>
public int? MaxChargeBars => _battery.MaxChargeBars;
/// <summary>
/// Gets whether backend is aware of battery charging level.
/// </summary>
public bool? IsBackendAccessible => _battery.IsBackendAccessible;
/// <summary>
/// Gets whether to display battery level or not.
/// </summary>
public bool? IsHidden => _battery.IsHidden;
}
}

View file

@ -0,0 +1,31 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
{
public interface IBattery
{
/// <summary>
/// Holds the current charging level of the battery in percent, double.NaN if unknown.
/// </summary>
double CurrentChargePercent { get; }
/// <summary>
/// Holds the current charging level of the battery in bars. Must not be larger than MaxChargeBars, null if unknown.
/// </summary>
int? CurrentChargeBars { get; }
/// <summary>
/// Holds the maximum charging level of the battery in bars, null if unknown.
/// </summary>
int? MaxChargeBars { get; }
/// <summary>
/// Holds whether backend is aware of battery charging level.
/// </summary>
bool? IsBackendAccessible { get; }
/// <summary>
/// Holds whether to display battery level or not.
/// </summary>
bool? IsHidden { get; }
}
}

View file

@ -0,0 +1,35 @@
using System.ComponentModel;
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
{
/// <summary>
/// Manages the state of a chargeable battery.
/// </summary>
public interface IBatteryMutable : INotifyPropertyChanged
{
/// <summary>
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
/// </summary>
double CurrentChargePercent { get; }
/// <summary>
/// Gets or sets the current charging level of the battery in bars. Must not be larger than MaxChargeBars, null if unknown.
/// </summary>
int? CurrentChargeBars { get; set; }
/// <summary>
/// Gets the maximum charging level of the battery in bars, null if unknown.
/// </summary>
int? MaxChargeBars { get; }
/// <summary>
/// Gets whether backend is aware of battery charging level.
/// </summary>
bool? IsBackendAccessible { get; }
/// <summary>
/// Gets whether to display battery level or not.
/// </summary>
bool? IsHidden { get; }
}
}

View file

@ -0,0 +1,53 @@
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS;
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS
{
public enum DriveType
{
/// <summary>
/// Bike without pedaling aid.
/// </summary>
SoleHumanPowered,
/// <summary>
/// pedal electric cycle: Pedaling is assisted by an electric engine.
/// </summary>
Pedelec
}
public class DriveMutable
{
public DriveMutable(
IEngine engine = null,
IBattery battery = null)
{
if (engine == null)
{
Engine = new Engine();
Battery = new BatteryMutable(new Battery.Builder().Build());
Type = DriveType.SoleHumanPowered;
return;
}
Engine = engine;
Battery = new BatteryMutable(battery ?? new Battery.Builder().Build());
Type = DriveType.Pedelec;
}
/// <summary>
/// Gets the type of the drive.
/// </summary>
public DriveType Type { get; private set; }
/// <summary>
/// Engine driving the bike.
/// </summary>
public IEngine Engine { get; private set; }
/// <summary>
/// Battery powering the engine.
/// </summary>
public IBatteryMutable Battery { get; private set; }
}
}

View file

@ -0,0 +1,13 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS
{
public class Engine : IEngine
{
public Engine(string manufacturer = null)
=> Manufacturer = !string.IsNullOrEmpty(manufacturer) ? manufacturer : null;
/// <summary>
/// Manufacturer of the engine.
/// </summary>
public string Manufacturer { get; private set; } = null;
}
}

View file

@ -0,0 +1,10 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS
{
public interface IEngine
{
/// <summary>
/// Manufacturer of the engine.
/// </summary>
string Manufacturer { get; }
}
}

View file

@ -0,0 +1,23 @@
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS;
namespace ShareeBike.Model.Bikes.BikeInfoNS
{
public interface IDrive
{
/// <summary>
/// Gets the type of the drive.
/// </summary>
DriveNS.DriveType Type { get; }
/// <summary>
/// Engine driving the bike.
/// </summary>
IEngine Engine { get; }
/// <summary>
/// Battery powering the engine.
/// </summary>
IBattery Battery { get; }
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using static ShareeBike.Model.Bikes.BikeInfoNS.RentalDescription;
namespace ShareeBike.Model.Bikes.BikeInfoNS
{
public interface IRentalDescription
{
/// <summary>
/// Name of the tariff.
/// </summary>
string Name { get; set; }
/// <summary>
/// Holds the time span for which a bike can be reserved.
/// </summary>
TimeSpan MaxReservationTimeSpan { get; set; }
/// <summary>
/// Dynamic language aware tariff elements to be displayed to user.
/// </summary>
Dictionary<string, TariffElement> TariffEntries { get; set; }
/// <summary>
/// Well known language aware elements (AGB, tracking info, ...) to be displayed to user.
/// </summary>
Dictionary<string, InfoElement> InfoEntries { get; set; }
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
namespace ShareeBike.Model.Bikes.BikeInfoNS
{
/// <summary>
/// Successor of TarifDescription- object.
/// Manages tariff- and rental info.
/// </summary>
public class RentalDescription : IRentalDescription
{
/// <summary>
/// The different elements of a tariff (example: "Max Gebühr", ) to be displayed by sharee.bike without processing
/// </summary>
public class TariffElement
{
/// <summary>
/// Describes the tariff element (language aware). To be displayed to user (example of elements: "Gratis Mietzeit", "Mietgebühr", "Max Gebühr").
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Holds the tariff element value (language aware, i.e. value from backend might be english, german, ... depending on smart phone value). To be displayed to user (example: "9.00 € / Tag").
/// </summary>
public string Value { get; set; } = string.Empty;
}
/// <summary>
/// Info element of general purpose (AGB, tracking info, ...)
/// </summary>
public class InfoElement
{
/// <summary>
/// Key which identifies the value (required for special processing)
/// </summary>
public string Key { get; set; }
/// <summary>
/// Text (language aware) to be displayed to user.
/// </summary>
public string Value { get; set; }
}
/// <summary>
/// Name of the tariff.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Number of the tariff.
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Holds the time span for which a bike can be reserved.
/// </summary>
public TimeSpan MaxReservationTimeSpan { get; set; }
/// <summary>
/// Dynamic language aware tariff elements to be displayed to user.
/// </summary>
public Dictionary<string, TariffElement> TariffEntries { get; set; } = new Dictionary<string, TariffElement>();
/// <summary>
/// Well known language aware elements (AGB, tracking info, ...) to be displayed to user.
/// </summary>
public Dictionary<string, InfoElement> InfoEntries { get; set; } = new Dictionary<string, InfoElement>();
}
}

View file

@ -0,0 +1,82 @@
using System;
namespace ShareeBike.Model.Bikes.BikeInfoNS
{
/// <summary>
/// Holds tariff info for a single bike.
/// </summary>
#if USCSHARP9
public record TariffDescription
{
/// <summary>
/// Name of the tariff.
/// </summary>
public string Name { get; init; }
/// <summary>
/// Number of the tariff.
/// </summary>
public int? Number { get; init; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public double FeeEuroPerHour { get; init; }
/// <summary>
/// Costs of the abo per month.
/// </summary>
public double AboEuroPerMonth { get; init; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public TimeSpan FreeTimePerSession { get; init; }
/// <summary>
/// Max. costs per day in euro.
/// </summary>
public double MaxFeeEuroPerDay { get; init; }
}
#else
public class TariffDescription
{
/// <summary>
/// Name of the tariff.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Number of the tariff.
/// </summary>
public int? Number { get; set; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public double FeeEuroPerHour { get; set; }
/// <summary>
/// Costs of the abo per month.
/// </summary>
public double AboEuroPerMonth { get; set; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public TimeSpan FreeTimePerSession { get; set; }
/// <summary>
/// Max. costs per day in euro.
/// </summary>
public double MaxFeeEuroPerDay { get; set; }
/// <summary> Info about operator agb as HTML (i.g. text and hyperlink). </summary>
public string OperatorAgb { get; set; }
/// <summary> Text which informs users about GPS tracking if tracking is on. </summary>
public string TrackingInfo { get; set; }
}
#endif
}