diff --git a/TINKLib/CSharp9.cs b/TINKLib/CSharp9.cs
new file mode 100644
index 0000000..604ad5d
--- /dev/null
+++ b/TINKLib/CSharp9.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+// ReSharper disable once CheckNamespace
+namespace System.Runtime.CompilerServices
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static class IsExternalInit
+ {
+ }
+}
\ No newline at end of file
diff --git a/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs b/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs
new file mode 100644
index 0000000..c552060
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Collections.Generic;
+using TINK.Model.Bikes.Bike;
+using TINK.Model.State;
+
+namespace TINK.Model.Bike.BC
+{
+ public class BikeInfo : IBikeInfo
+ {
+ /// Default value of demo property.
+ public const bool DEFAULTVALUEISDEMO = false;
+
+ /// Holds the info about the bike state.
+ private readonly IStateInfo m_oStateInfo;
+
+ ///
+ /// Holds the bike object.
+ ///
+ private Bike Bike { get; }
+
+ /// Constructs a bike object.
+ protected BikeInfo(
+ IStateInfo stateInfo,
+ int id,
+ bool? isDemo = DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null,
+ int? currentStationId = null,
+ Uri operatorUri = null,
+ TariffDescription tariffDescription = null)
+ {
+ Bike = new Bike(id, wheelType, typeOfBike, description);
+
+ m_oStateInfo = stateInfo;
+
+ IsDemo = isDemo ?? DEFAULTVALUEISDEMO;
+ Group = group ?? new List();
+ CurrentStation = currentStationId;
+ OperatorUri = operatorUri;
+ TariffDescription = tariffDescription;
+ }
+
+ public BikeInfo(BikeInfo bikeInfo) : this(
+ bikeInfo?.State,
+ bikeInfo?.Id ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source must not be null."),
+ bikeInfo.IsDemo,
+ bikeInfo.Group,
+ bikeInfo.WheelType,
+ bikeInfo.TypeOfBike,
+ bikeInfo.Description,
+ bikeInfo.CurrentStation,
+ bikeInfo.OperatorUri,
+ bikeInfo.TariffDescription) { }
+
+ ///
+ /// Constructs a bike info object for a available bike.
+ ///
+ /// Unique id of bike.
+ /// Id of station where bike is located.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ ///
+ public BikeInfo(
+ int id,
+ int? currentStationId,
+ Uri operatorUri = null,
+ TariffDescription tariffDescription = null,
+ bool? isDemo = DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null) : this(
+ new StateInfo(),
+ id,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ currentStationId,
+ operatorUri,
+ tariffDescription)
+ {
+ }
+
+ ///
+ /// Constructs a bike info object for a requested bike.
+ ///
+ /// Provider for current date time to calculate remainig time on demand for state of type reserved.
+ ///
+ /// Unique id of bike.
+ /// Name of station where bike is located, null if bike is on the road.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ /// Date time when bike was requested
+ /// Mail address of user which requested bike.
+ /// Booking code.
+ /// Date time provider to calculate reaining time.
+ public BikeInfo(
+ int id,
+ bool? isDemo,
+ IEnumerable group,
+ WheelType? wheelType,
+ TypeOfBike? typeOfBike,
+ string description,
+ int? stationId,
+ Uri operatorUri,
+ TariffDescription tariffDescription,
+ DateTime requestedAt,
+ string mailAddress,
+ string code,
+ Func dateTimeProvider = null) : this(
+ new StateInfo(
+ dateTimeProvider,
+ requestedAt,
+ mailAddress,
+ code),
+ id,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ stationId,
+ operatorUri,
+ tariffDescription)
+ {
+ }
+
+ ///
+ /// Constructs a bike info object for a booked bike.
+ ///
+ /// Provider for current date time to calculate remainig time on demand for state of type reserved.
+ ///
+ /// Unique id of bike.
+ /// Name of station where bike is located, null if bike is on the road.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ /// Date time when bike was booked
+ /// Mail address of user which booked bike.
+ /// Booking code.
+ public BikeInfo(
+ int id,
+ bool? isDemo,
+ IEnumerable group,
+ WheelType? wheelType,
+ TypeOfBike? typeOfBike,
+ string description,
+ int? currentStationId,
+ Uri operatorUri,
+ TariffDescription tariffDescription,
+ DateTime bookedAt,
+ string mailAddress,
+ string code) : this(
+ new StateInfo(
+ bookedAt,
+ mailAddress,
+ code),
+ id,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ currentStationId,
+ operatorUri,
+ tariffDescription)
+ {
+ }
+
+ /// True if device is demo device, false otherwise.
+ public bool IsDemo { get; }
+
+ /// Returns the group (TINK, Konrad, ...).
+ public IEnumerable Group { get; }
+
+ ///
+ /// Station a which bike is located, null otherwise.
+ ///
+ public int? CurrentStation { get; }
+
+ /// Holds description about the tarif.
+ public TariffDescription TariffDescription { get; }
+
+
+ /// Holds the rent state of the bike.
+ ///
+ public IStateInfo State
+ {
+ get { return m_oStateInfo; }
+ }
+
+ public int Id => Bike.Id;
+
+ public WheelType? WheelType => Bike.WheelType;
+
+ public TypeOfBike? TypeOfBike => Bike.TypeOfBike;
+
+ public string Description => Bike.Description;
+
+ ///
+ /// Uri of the operator or null, in case of single operator setup.
+ ///
+ public Uri OperatorUri { get; }
+
+ ///
+ /// Converts the instance to text.
+ ///
+ public new string ToString()
+ {
+ return $"Id={Bike.Id}{(Bike.WheelType != null ? $", wheel(s)={Bike.WheelType}" : string.Empty)}{(Bike.TypeOfBike != null ? $"type={Bike.TypeOfBike}" : "")}, state={State}, location={(CurrentStation.HasValue ? $"Station {CurrentStation}" : "On the road")}, is demo={IsDemo}.";
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs
new file mode 100644
index 0000000..80b9447
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using TINK.Model.Bikes.Bike;
+using TINK.Model.Bikes.Bike.BC;
+using TINK.Model.State;
+
+namespace TINK.Model.Bike.BC
+{
+ [DataContract]
+ public class BikeInfoMutable : IBikeInfoMutable, INotifyPropertyChanged
+ {
+ /// Holds the bike.
+ private readonly Bike m_oBike;
+
+ /// Holds the state info of the bike.
+ private readonly StateInfoMutable m_oStateInfo;
+
+ ///
+ /// Constructs a bike.
+ ///
+ /// Unique id of bike.
+ /// True if device is demo device, false otherwise.
+ /// Provider for current date time to calculate remainig time on demand for state of type reserved.
+ ///
+ /// Name of station where bike is located, null if bike is on the road.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ /// Bike state info.
+ protected BikeInfoMutable(
+ int id,
+ bool isDemo = BikeInfo.DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null,
+ int? currentStationId = null,
+ Uri operatorUri = null,
+ TariffDescription tariffDescription = null,
+ Func dateTimeProvider = null,
+ IStateInfo stateInfo = null)
+ {
+ IsDemo = isDemo;
+ Group = group;
+ m_oBike = new Bike(id, wheelType, typeOfBike, description);
+ m_oStateInfo = new StateInfoMutable(dateTimeProvider, stateInfo);
+ m_oStateInfo.PropertyChanged += (sender, eventargs) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(eventargs.PropertyName));
+ CurrentStation = currentStationId;
+ OperatorUri = operatorUri;
+ TariffDescription = tariffDescription;
+ }
+
+ /// Constructs a bike object from source.
+ public BikeInfoMutable(IBikeInfo p_oBike) : this(
+ p_oBike.Id,
+ p_oBike.IsDemo,
+ p_oBike.Group,
+ p_oBike.WheelType,
+ p_oBike.TypeOfBike,
+ p_oBike.Description,
+ p_oBike.CurrentStation,
+ p_oBike.OperatorUri,
+ p_oBike.TariffDescription,
+ null,
+ p_oBike.State)
+ {
+ }
+
+ ///
+ /// Station a which bike is located, null otherwise.
+ ///
+ [DataMember]
+ public int? CurrentStation { get; }
+
+ /// Holds description about the tarif.
+ [DataMember]
+ public TariffDescription TariffDescription { get; private set; }
+
+ ///
+ /// Holds the rent state of the bike.
+ ///
+ [DataMember]
+ public StateInfoMutable State
+ {
+ get { return m_oStateInfo; }
+ }
+
+ ///
+ /// Uri of the operator or null, in case of single operator setup.
+ ///
+ public Uri OperatorUri { get; }
+
+ /// Unused member.
+ IStateInfoMutable IBikeInfoMutable.State => m_oStateInfo;
+
+ public int Id => m_oBike.Id;
+
+ public bool IsDemo { get; }
+
+ /// Returns the group (TINK, Konrad, ...).
+ public IEnumerable Group { get; }
+
+ public WheelType? WheelType => m_oBike.WheelType;
+
+ public TypeOfBike? TypeOfBike => m_oBike.TypeOfBike;
+
+ public string Description => m_oBike.Description;
+
+ ///
+ /// Fired whenever property of bike changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Converts the instance to text.
+ ///
+ ///
+ public new string ToString()
+ {
+ return $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $", type={TypeOfBike}" : "")}, demo={IsDemo}, state={State.ToString()}, location={(CurrentStation.HasValue ? $"Station {CurrentStation}" : "On the road")}.";
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs b/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs
new file mode 100644
index 0000000..bcb6d13
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using TINK.Model.Bikes.Bike;
+using TINK.Model.State;
+
+namespace TINK.Model.Bike.BC
+{
+ ///
+ /// Allows to access bike info.
+ ///
+ public interface IBikeInfo
+ {
+ ///
+ /// Holds the unique id of the bike;
+ ///
+ int Id { get; }
+
+ /// True if bike is a demo bike.
+ bool IsDemo { get; }
+
+ /// Returns the group (TINK, Konrad, ...).
+ IEnumerable Group { get; }
+
+ ///
+ /// Holds the count of wheels.
+ ///
+ WheelType? WheelType { get; }
+
+ ///
+ /// Holds the type of bike.
+ ///
+ TypeOfBike? TypeOfBike { get; }
+
+ /// Holds the description of the bike.
+ string Description { get; }
+
+ ///
+ /// Station a which bike is located, null otherwise.
+ ///
+ int? CurrentStation { get; }
+
+ ///
+ /// Uri of the operator or null, in case of single operator setup.
+ ///
+ Uri OperatorUri { get; }
+
+ /// Holds description about the tarif.
+ TariffDescription TariffDescription { get; }
+
+ ///
+ /// Holds the rent state of the bike.
+ ///
+ IStateInfo State { get; }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs
new file mode 100644
index 0000000..ede2168
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using TINK.Model.Bike;
+using TINK.Model.State;
+
+namespace TINK.Model.Bikes.Bike.BC
+{
+ public interface IBikeInfoMutable
+ {
+ ///
+ /// Holds the unique id of the bike;
+ ///
+ int Id { get; }
+
+ /// True if bike is a demo bike.
+ bool IsDemo { get; }
+
+ /// Returns the group (TINK, Konrad, ...).
+ IEnumerable Group { get; }
+
+ ///
+ /// Holds the count of wheels.
+ ///
+ WheelType? WheelType { get; }
+
+ ///
+ /// Holds the type of bike.
+ ///
+ TypeOfBike? TypeOfBike { get; }
+
+ /// Holds the description of the bike.
+ string Description { get; }
+
+ ///
+ /// Station a which bike is located, null otherwise.
+ ///
+ int? CurrentStation { get; }
+
+ ///
+ /// Holds the rent state of the bike.
+ ///
+ IStateInfoMutable State { get; }
+
+ ///
+ /// Uri of the operator or null, in case of single operator setup.
+ ///
+ Uri OperatorUri { get; }
+
+ event PropertyChangedEventHandler PropertyChanged;
+ }
+
+ public enum NotifyPropertyChangedLevel
+ {
+ /// Notify about all property changes.
+ All,
+
+ /// Notify about no property changes.
+ None
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/Bike.cs b/TINKLib/Model/Bikes/Bike/Bike.cs
new file mode 100644
index 0000000..51fcb70
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/Bike.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+
+namespace TINK.Model.Bike
+{
+ /// Count of wheels.
+ public enum WheelType
+ {
+ Mono = 0,
+ Two = 1,
+ Trike = 2,
+ }
+
+ /// Type of bike.
+ public enum TypeOfBike
+ {
+ Allround = 0,
+ Cargo = 1,
+ Citybike = 2,
+ }
+
+ public class Bike : IEquatable
+ {
+ ///
+ /// Constructs a bike.
+ ///
+ /// Provider for current date time to calculate remainig time on demand for state of type reserved.
+ ///
+ /// Unique id of bike.
+ /// Name of station where bike is located, null if bike is on the road.
+ public Bike(
+ int p_iId,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null)
+ {
+ WheelType = wheelType;
+ TypeOfBike = typeOfBike;
+ Id = p_iId;
+ Description = description;
+ }
+
+ ///
+ /// Holds the unique id of the bike;
+ ///
+ public int Id { get; }
+
+ ///
+ /// Holds the count of wheels.
+ ///
+ public WheelType? WheelType { get; }
+
+ ///
+ /// Holds the type of bike.
+ ///
+ public TypeOfBike? TypeOfBike { get; }
+
+ /// Holds the description of the bike.
+ public string Description { get; }
+
+
+ /// Compares two bike object.
+ /// Object to compare with.
+ /// True if bikes are equal.
+ public override bool Equals(object obj)
+ {
+ var l_oBike = obj as Bike;
+ if (l_oBike == null)
+ {
+ return false;
+ }
+
+ return Equals(l_oBike);
+ }
+
+ /// Converts the instance to text.
+ public new 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}" : "")}.";
+ }
+
+ /// Compares two bike object.
+ /// Object to compare with.
+ /// True if bikes are equal.
+ public bool Equals(Bike other)
+ {
+ return other != null &&
+ Id == other.Id &&
+ WheelType == other.WheelType &&
+ TypeOfBike == other.TypeOfBike
+ && Description == other.Description;
+ }
+
+ /// Compares two bike object.
+ /// Object to compare with.
+ /// True if bikes are equal.
+ public static bool operator ==(Bike bike1, Bike bike2)
+ {
+ return EqualityComparer.Default.Equals(bike1, bike2);
+ }
+
+ /// Compares two bike object.
+ /// Object to compare with.
+ /// True if bikes are equal.
+ public static bool operator !=(Bike bike1, Bike bike2)
+ {
+ return !(bike1 == bike2);
+ }
+
+ ///
+ /// Generates hash code for bike object.
+ ///
+ ///
+ 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;
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs
new file mode 100644
index 0000000..2978e71
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using TINK.Model.Bikes.Bike;
+using TINK.Model.State;
+
+namespace TINK.Model.Bike.BluetoothLock
+{
+ public class BikeInfo : BC.BikeInfo, IBikeInfo
+ {
+ ///
+ /// Constructs a bike info object for a available bike.
+ ///
+ /// Unique id of bike.
+ /// Id of the lock.
+ /// GUID specifying the lock.
+ /// Id of station where bike is located.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ /// Trike, two wheels, mono, ....
+ public BikeInfo(
+ int bikeId,
+ int lockId,
+ Guid lockGuid,
+ int? currentStationId,
+ Uri operatorUri = null,
+ TariffDescription tariffDescription = null,
+ bool? isDemo = DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null) : base(
+ new StateInfo(),
+ bikeId,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ currentStationId,
+ operatorUri,
+ tariffDescription)
+ {
+ LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid }.Build();
+ }
+
+ ///
+ /// Constructs a bike info object for a requested bike.
+ ///
+ /// Provider for current date time to calculate remainig time on demand for state of type reserved.
+ /// Unique id of bike.
+ /// Id of the lock.
+ /// GUID specifying the lock.
+ /// Date time when bike was requested
+ /// Mail address of user which requested bike.
+ /// Name of station where bike is located, null if bike is on the road.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ /// Date time provider to calculate reaining time.
+ ///
+ public BikeInfo(
+ int id,
+ int lockId,
+ Guid lockGuid,
+ byte[] userKey,
+ byte[] adminKey,
+ byte[] seed,
+ DateTime requestedAt,
+ string mailAddress,
+ int? currentStationId,
+ Uri operatorUri,
+ TariffDescription tariffDescription,
+ Func dateTimeProvider,
+ bool? isDemo = DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null) : base(
+ new StateInfo(
+ dateTimeProvider,
+ requestedAt,
+ mailAddress,
+ ""),
+ id,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ currentStationId,
+ operatorUri,
+ tariffDescription)
+ {
+ LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
+ }
+
+ ///
+ /// Constructs a bike info object for a booked bike.
+ ///
+ /// Unique id of bike.
+ /// Id of the lock.
+ /// GUID specifying the lock.
+ /// Date time when bike was booked
+ /// Mail address of user which booked bike.
+ /// Name of station where bike is located, null if bike is on the road.
+ /// Holds the uri of the operator or null, in case of single operator setup.
+ /// Hold tariff description of bike.
+ ///
+ public BikeInfo(
+ int id,
+ int lockId,
+ Guid lockGuid,
+ byte[] userKey,
+ byte[] adminKey,
+ byte[] seed,
+ DateTime bookedAt,
+ string mailAddress,
+ int? currentStationId,
+ Uri operatorUri,
+ TariffDescription tariffDescription = null,
+ bool? isDemo = DEFAULTVALUEISDEMO,
+ IEnumerable group = null,
+ WheelType? wheelType = null,
+ TypeOfBike? typeOfBike = null,
+ string description = null) : base(
+ new StateInfo(
+ bookedAt,
+ mailAddress,
+ ""),
+ id,
+ isDemo,
+ group,
+ wheelType,
+ typeOfBike,
+ description,
+ 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; }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs
new file mode 100644
index 0000000..fbc49cd
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs
@@ -0,0 +1,36 @@
+using System;
+using TINK.Model.Bikes.Bike;
+using TINK.Model.Bikes.Bike.BluetoothLock;
+
+namespace TINK.Model.Bike.BluetoothLock
+{
+ public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
+ {
+ /// Constructs a bike object from source.
+ public BikeInfoMutable(BikeInfo bike) : base(
+ bike.Id,
+ bike.IsDemo,
+ bike.Group,
+ bike.WheelType,
+ bike.TypeOfBike,
+ bike.Description,
+ bike.CurrentStation,
+ 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);
+ }
+
+ public LockInfoMutable LockInfo { get; }
+
+ ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs
new file mode 100644
index 0000000..e0664db
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs
@@ -0,0 +1,6 @@
+namespace TINK.Model.Bike.BluetoothLock
+{
+ public interface IBikeInfo : BC.IBikeInfo
+ {
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs
new file mode 100644
index 0000000..5467ec7
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs
@@ -0,0 +1,7 @@
+namespace TINK.Model.Bikes.Bike.BluetoothLock
+{
+ public interface IBikeInfoMutable : BC.IBikeInfoMutable
+ {
+ ILockInfoMutable LockInfo { get; }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs
new file mode 100644
index 0000000..109aa30
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs
@@ -0,0 +1,24 @@
+using System;
+using TINK.Model.Bike.BluetoothLock;
+
+namespace TINK.Model.Bikes.Bike.BluetoothLock
+{
+ public interface ILockInfoMutable
+ {
+ /// Identification number of bluetooth lock.
+ int Id { get; }
+
+ /// Gets the user key.
+ byte[] UserKey { get; }
+
+ LockingState State { get; set; }
+
+ /// Holds the percentage of lock battery.
+ double BatteryPercentage { get; set; }
+
+ /// Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.
+ Guid Guid { get; set; }
+
+ byte[] Seed { get; }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs
new file mode 100644
index 0000000..c53e0f6
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs
@@ -0,0 +1,54 @@
+using System;
+using TINK.Model.Bikes.Bike.BluetoothLock;
+
+namespace TINK.Model.Bike.BluetoothLock
+{
+ public class LockInfoMutable : ILockInfoMutable
+ {
+ /// Lock info object.
+ private LockInfo LockInfo { get; set; }
+
+ /// Constructs a bluetooth lock info object.
+ /// Id of lock must always been known when constructing an lock info object.
+ public LockInfoMutable(
+ int id,
+ Guid guid,
+ byte[] userKey,
+ byte[] adminKey,
+ byte[] seed,
+ LockingState state)
+ {
+ LockInfo = new LockInfo.Builder() { Id = id, Guid = guid, UserKey = userKey, AdminKey = adminKey, Seed = seed, State = state }.Build();
+ }
+
+ public int Id => LockInfo.Id;
+
+ /// Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.
+ 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;
+
+ public LockingState State
+ {
+ get => LockInfo.State;
+ set => LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
+ }
+
+ /// Holds the percentage of lock battery.
+ public double BatteryPercentage { get; set; } = double.NaN;
+
+ /// Loads lock info object from values.
+ 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();
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/Bike/TariffDescription.cs b/TINKLib/Model/Bikes/Bike/TariffDescription.cs
new file mode 100644
index 0000000..beeb6d2
--- /dev/null
+++ b/TINKLib/Model/Bikes/Bike/TariffDescription.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace TINK.Model.Bikes.Bike
+{
+ ///
+ /// Holds tariff info for a single bike.
+ ///
+ public record TariffDescription
+ {
+ ///
+ /// Name of the tariff.
+ ///
+ public string Name { get; init; }
+
+ ///
+ /// Number of the tariff.
+ ///
+ public int? Number { get; init; }
+
+ ///
+ /// Costs per hour in euro.
+ ///
+ public double FeeEuroPerHour { get; init; }
+
+ ///
+ /// Costs of the abo per month.
+ ///
+ public double AboEuroPerMonth { get; init; }
+
+ ///
+ /// Costs per hour in euro.
+ ///
+ public TimeSpan FreeTimePerSession { get; init; }
+
+ ///
+ /// Max. costs per day in euro.
+ ///
+ public double MaxFeeEuroPerDay { get; init; }
+ }
+}
diff --git a/TINKLib/Model/Bikes/BikeCollection.cs b/TINKLib/Model/Bikes/BikeCollection.cs
new file mode 100644
index 0000000..18486e3
--- /dev/null
+++ b/TINKLib/Model/Bikes/BikeCollection.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using System.Linq;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model.Bike
+{
+ public class BikeCollection : IBikeDictionary
+ {
+ /// Holds the bike dictionary object.
+ private Dictionary BikeDictionary { get; }
+
+ /// Constructs an empty bike info dictionary object.
+ public BikeCollection()
+ {
+ BikeDictionary = new Dictionary();
+ }
+
+ /// Constructs a bike collection object.
+ ///
+ public BikeCollection(Dictionary bikeDictionary)
+ {
+ BikeDictionary = bikeDictionary ??
+ throw new ArgumentNullException(nameof(bikeDictionary), "Can not construct BikeCollection object.");
+ }
+
+ /// Gets a bike by its id.
+ /// Id of the bike to get.
+ ///
+ public BikeInfo GetById(int p_iId)
+ {
+ return BikeDictionary.FirstOrDefault(x => x.Key == p_iId).Value;
+ }
+
+ /// Gets the count of bikes.
+ public int Count => BikeDictionary.Count;
+
+ /// Gets if a bike with given id exists.
+ /// Id of bike.
+ /// True if bike is contained, false otherwise.
+ public bool ContainsKey(int p_iId) => BikeDictionary.Keys.Contains(p_iId);
+
+ /// Gets the enumerator.
+ /// Enumerator object.
+ public IEnumerator GetEnumerator() => BikeDictionary.Values.GetEnumerator();
+
+ /// Gets the enumerator.
+ /// Enumerator object.
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/TINKLib/Model/Bikes/BikeCollectionFilter.cs b/TINKLib/Model/Bikes/BikeCollectionFilter.cs
new file mode 100644
index 0000000..7dc62f0
--- /dev/null
+++ b/TINKLib/Model/Bikes/BikeCollectionFilter.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.Linq;
+using TINK.Model.Bike;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model
+{
+ public static class BikeCollectionFilter
+ {
+ /// Filters bikes by station.
+ /// Bikes available, requested and/ or occupied bikes to filter.
+ /// Id of station, might be null
+ /// BikeCollection holding bikes at given station or empty BikeCollection, if there are no bikes.
+ public static BikeCollection GetAtStation(
+ this BikeCollection bikesAtAnyStation,
+ int? selectedStation)
+ {
+ return new BikeCollection(bikesAtAnyStation?
+ .Where(bike => selectedStation.HasValue && bike.CurrentStation == selectedStation.Value)
+ .ToDictionary(x => x.Id) ?? new Dictionary());
+ }
+
+ /// Filters bikes by bike type.
+ /// Bikes available, requested and/ or occupied bikes to filter.
+ /// BikeCollection holding LockIt-bikes empty BikeCollection, if there are no LockIt-bikes.
+ public static BikeCollection GetLockIt(this BikeCollection bcAndLockItBikes)
+ {
+ return new BikeCollection(bcAndLockItBikes?
+ .Where(bike => bike is Bike.BluetoothLock.BikeInfo)
+ .ToDictionary(x => x.Id) ?? new Dictionary());
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/BikeCollectionMutable.cs b/TINKLib/Model/Bikes/BikeCollectionMutable.cs
new file mode 100644
index 0000000..99f299a
--- /dev/null
+++ b/TINKLib/Model/Bikes/BikeCollectionMutable.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
+
+namespace TINK.Model.Bike
+{
+ /// Holds entity of bikes.
+ public class BikeCollectionMutable : ObservableCollection, IBikeDictionaryMutable
+ {
+ /// Constructs a mutable bike collection object.
+ public BikeCollectionMutable()
+ {
+ SelectedBike = null;
+ }
+
+ ///
+ /// Updates bikes dictionary from bikes response, i.e.
+ /// - removes bikes which are no more contained in bikes response
+ /// - updates state of all bikes from state contained in bikes response
+ ///
+ /// Object holding bikes info from copri to update from.
+ /// Provices date time information.
+ public void Update(
+ IEnumerable bikesAll)
+ {
+ // Get list of current bikes by state(s) to update.
+ // Needed to remove bikes which switched state and have to be removed from collection.
+ var bikesToBeRemoved = this.Select(x => x.Id).ToList();
+
+ foreach (var bikeInfo in (bikesAll ?? new List()))
+ {
+ /// Check if bike has to be added to list of existing station.
+ if (ContainsKey(bikeInfo.Id) == false)
+ {
+ // Bike does not yet exist in list of bikes.
+ Add(BikeInfoMutableFactory.Create(bikeInfo));
+ continue;
+ }
+
+ // Update bike.
+ GetById(bikeInfo.Id).State.Load(bikeInfo.State);
+
+ if (bikesToBeRemoved.Contains(bikeInfo.Id))
+ {
+ // Remove list from obsolete list.
+ bikesToBeRemoved.Remove(bikeInfo.Id);
+ }
+ }
+
+ // Remove obsolete bikes.
+ foreach (var l_oId in bikesToBeRemoved)
+ {
+ RemoveById(l_oId);
+ }
+ }
+
+ ///
+ /// Adds a new bike to collecion of bike.
+ ///
+ /// New bike to add.
+ /// Thrown if bike is not unique.
+ public new void Add(BikeInfoMutable p_oNewBike)
+ {
+ // Ensure that bike id of new bike is is unique
+ foreach (BikeInfoMutable l_oBike in Items)
+ {
+ if (l_oBike.Id == p_oNewBike.Id)
+ {
+ throw new Exception(string.Format("Can not add bike with {0} to collection ob bike. Id is not unnique.", p_oNewBike));
+ }
+ }
+
+ base.Add(p_oNewBike);
+ }
+
+ ///
+ /// Bike selected by user for regerving or cancel reservation.
+ ///
+ public BikeInfoMutable SelectedBike
+ {
+ get;
+ private set;
+ }
+
+ public void SetSelectedBike(int p_intId)
+ {
+ SelectedBike = GetById(p_intId);
+ }
+
+ ///
+ /// Gets a bike by its id.
+ ///
+ ///
+ ///
+ public BikeInfoMutable GetById(int p_iId)
+ {
+ {
+ return this.FirstOrDefault(bike => bike.Id == p_iId);
+ }
+ }
+
+ ///
+ /// Deteermines whether a bike by given key exists.
+ ///
+ /// Key to check.
+ /// True if bike exists.
+ public bool ContainsKey(int p_iId)
+ {
+ return GetById(p_iId) != null;
+ }
+
+ ///
+ /// Removes a bike by its id.
+ ///
+ /// Id of bike to be removed.
+ public void RemoveById(int p_iId)
+ {
+ var l_oBike = GetById(p_iId);
+ if (l_oBike == null)
+ {
+ // Nothing to do if bike does not exists.
+ return;
+ }
+
+ Remove(l_oBike);
+ }
+
+ ///
+ /// Create mutable objects from immutable objects.
+ ///
+ private static class BikeInfoMutableFactory
+ {
+ public static BikeInfoMutable Create(BikeInfo bikeInfo)
+ {
+ return (bikeInfo is BluetoothLock.BikeInfo bluetoothLockBikeInfo)
+ ? new BluetoothLock.BikeInfoMutable(bluetoothLockBikeInfo)
+ : new BikeInfoMutable(bikeInfo);
+ }
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/BikeCollectionUpdater.cs b/TINKLib/Model/Bikes/BikeCollectionUpdater.cs
new file mode 100644
index 0000000..edf7b4d
--- /dev/null
+++ b/TINKLib/Model/Bikes/BikeCollectionUpdater.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TINK.Model.Bike
+{
+ public static class BikeCollectionUpdater
+ {
+ /// Updates bikes lock info with the latest lock info from bluetooth service.
+ /// bikes to be updated.
+ /// locks info to be used for updating bikes.
+ ///
+ public static BikeCollection UpdateLockInfo(
+ this BikeCollection bikes,
+ IEnumerable locksInfo)
+ {
+
+ var updatedBikesCollection = new Dictionary();
+
+ foreach (var bikeInfo in bikes)
+ {
+ if (!(bikeInfo is BluetoothLock.BikeInfo bluetoothBikeInfo))
+ {
+ // No processing needed because bike is not a bluetooth bike
+ updatedBikesCollection.Add(bikeInfo.Id, bikeInfo);
+ continue;
+ }
+
+ // Check if to update current bike lock info using state from bluetooth service or not.
+ var currentLockInfo = locksInfo.FirstOrDefault(x => x.Id == bluetoothBikeInfo.LockInfo.Id) // Update bike info with latest info from bluethooth service if available
+ ?? bluetoothBikeInfo.LockInfo; // Use lock info state object from copri which holds a lock id and a state of value unknown.
+
+ updatedBikesCollection.Add(bluetoothBikeInfo.Id, new BluetoothLock.BikeInfo(bluetoothBikeInfo, currentLockInfo));
+ }
+
+ return new BikeCollection(updatedBikesCollection);
+ }
+ }
+}
diff --git a/TINKLib/Model/Bikes/IBikeCollection.cs b/TINKLib/Model/Bikes/IBikeCollection.cs
new file mode 100644
index 0000000..3e2aa6d
--- /dev/null
+++ b/TINKLib/Model/Bikes/IBikeCollection.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace TINK.Model.Bike
+{
+ public interface IBikeDictionary : IReadOnlyCollection
+ {
+ ///
+ /// Gets a bike by its id.
+ ///
+ ///
+ ///
+ T GetById(int p_iId);
+
+ ///
+ /// Deteermines whether a bike by given key exists.
+ ///
+ /// Key to check.
+ /// True if bike exists.
+ bool ContainsKey(int p_iId);
+ }
+ public interface IBikeDictionaryMutable : IBikeDictionary
+ {
+ ///
+ /// Removes a bike by its id.
+ ///
+ /// Id of bike to be removed.
+ void RemoveById(int p_iId);
+
+ ///
+ /// Adds a new element to dictinary.
+ ///
+ /// New element to add.
+ void Add(T p_oNewElement);
+ }
+}
diff --git a/TINKLib/Model/Connector/Command/Command.cs b/TINKLib/Model/Connector/Command/Command.cs
new file mode 100644
index 0000000..b81ba6d
--- /dev/null
+++ b/TINKLib/Model/Connector/Command/Command.cs
@@ -0,0 +1,141 @@
+using Serilog;
+using System;
+using System.Threading.Tasks;
+using TINK.Model.Repository;
+using TINK.Model.Repository.Request;
+using TINK.Model.Repository.Response;
+using TINK.Model.User.Account;
+
+namespace TINK.Model.Connector
+{
+ public class Command : Base, ICommand
+ {
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => CopriServer.IsConnected;
+
+ /// No user is logged in.
+ public string SessionCookie => null;
+
+ /// Is raised whenever login state has changed.
+ public event LoginStateChangedEventHandler LoginStateChanged;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public Command(
+ ICopriServerBase p_oCopriServer) : base(p_oCopriServer)
+ {
+ }
+
+ ///
+ /// Logs user in.
+ /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
+ /// If log in fails (password modified) session cookie is set to empty.
+ /// If communication fails an exception is thrown.
+ ///
+ public async Task DoLogin(
+ string p_strMail,
+ string p_strPassword,
+ string p_strDeviceId)
+ {
+ if (string.IsNullOrEmpty(p_strMail))
+ {
+ throw new ArgumentNullException("Can not loging user. Mail address must not be null or empty.");
+ }
+
+ if (string.IsNullOrEmpty(p_strPassword))
+ {
+ throw new ArgumentNullException("Can not loging user. Password must not be null or empty.");
+ }
+
+ if (string.IsNullOrEmpty(p_strDeviceId))
+ {
+ throw new ArgumentNullException("Can not loging user. Device not be null or empty.");
+ }
+
+ AuthorizationResponse l_oResponse;
+ try
+ {
+ l_oResponse = (await CopriServer.DoAuthorizationAsync(p_strMail, p_strPassword, p_strDeviceId)).GetIsResponseOk(p_strMail);
+ }
+ catch (System.Exception)
+ {
+ throw;
+ }
+
+ var l_oAccount = l_oResponse.GetAccount(MerchantId, p_strMail, p_strPassword);
+
+ // Log in state changes. Notify parent object to update.
+ LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs(l_oAccount.SessionCookie, l_oAccount.Mail));
+
+ return l_oAccount;
+ }
+
+ /// Logs user out.
+ public async Task DoLogout()
+ {
+ Log.ForContext().Error("Unexpected log out request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// Request to reserve a bike.
+ ///
+ /// Bike to book.
+ public async Task DoReserve(
+ Bikes.Bike.BC.IBikeInfoMutable p_oBike)
+ {
+ Log.ForContext().Error("Unexpected booking request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ /// Request to cancel a reservation.
+ /// Bike to book.
+ public async Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable bike)
+ {
+ Log.ForContext().Error("Unexpected cancel reservation request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ /// Get authentication keys.
+ /// Bike to book.
+ public async Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike)
+ {
+ Log.ForContext().Error("Unexpected request to get authenticatin keys detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ /// Updates COPRI lock state for a booked bike.
+ /// Bike to update locking state for.
+ /// Location where lock was opened/ changed.
+ /// Response on updating locking state.
+ public async Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location)
+ {
+ Log.ForContext().Error("Unexpected request to update locking state detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ public async Task DoBook(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike)
+ {
+ Log.ForContext().Error("Unexpected booking request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ public async Task DoReturn(
+ Bikes.Bike.BluetoothLock.IBikeInfoMutable bike,
+ LocationDto location)
+ {
+ Log.ForContext().Error("Unexpected returning request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// Submits feedback to copri server.
+ ///
+ /// Feedback to submit.
+ public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
+ {
+ Log.ForContext().Error("Unexpected submit feedback request detected. No user logged in.");
+ await Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TINKLib/Model/Connector/Command/CommandLoggedIn.cs b/TINKLib/Model/Connector/Command/CommandLoggedIn.cs
new file mode 100644
index 0000000..dc5bc95
--- /dev/null
+++ b/TINKLib/Model/Connector/Command/CommandLoggedIn.cs
@@ -0,0 +1,279 @@
+using System;
+using System.Threading.Tasks;
+using TINK.Model.Bike.BluetoothLock;
+using TINK.Model.Repository;
+using TINK.Model.Repository.Exception;
+using TINK.Model.Repository.Request;
+using TINK.Model.Repository.Response;
+using TINK.Model.User.Account;
+
+namespace TINK.Model.Connector
+{
+ public class CommandLoggedIn : BaseLoggedIn, ICommand
+ {
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => CopriServer.IsConnected;
+
+ /// Is raised whenever login state has changed.
+ public event LoginStateChangedEventHandler LoginStateChanged;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public CommandLoggedIn(ICopriServerBase p_oCopriServer,
+ string p_strSessionCookie,
+ string p_strMail,
+ Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider)
+ {
+ }
+
+ ///
+ /// Logs user in.
+ /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
+ /// If log in fails (password modified) session cookie is set to empty.
+ /// If communication fails an TINK.Model.Repository.Exception is thrown.
+ ///
+ /// Account to use for login.
+ public Task DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId)
+ {
+ if (string.IsNullOrEmpty(p_strMail))
+ {
+ throw new ArgumentNullException("Can not loging user. Mail address must not be null or empty.");
+ }
+
+ throw new Exception($"Fehler beim Anmelden von unter {p_strMail}. Benutzer {Mail} ist bereits angemeldet.");
+ }
+
+ /// Logs user out.
+ public async Task DoLogout()
+ {
+ AuthorizationoutResponse l_oResponse = null;
+ try
+ {
+ l_oResponse = (await CopriServer.DoAuthoutAsync()).GetIsResponseOk();
+ }
+ catch (AuthcookieNotDefinedException)
+ {
+ // Cookie is no more defined, i.e. no need to logout user at copri because user is already logged out.
+ // Just ignore this error.
+ // User logged out, log in state changed. Notify parent object to update.
+ LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs());
+ return;
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+
+ // User logged out, log in state changed. Notify parent object to update.
+ LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs());
+ }
+
+ ///
+ /// Request to reserve a bike.
+ ///
+ /// Bike to book.
+ public async Task DoReserve(Bikes.Bike.BC.IBikeInfoMutable bike)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException("Can not reserve bike. No bike object available.");
+ }
+
+ BikeInfoReservedOrBooked response;
+ try
+ {
+ response = (await CopriServer.DoReserveAsync(bike.Id, bike.OperatorUri)).GetIsReserveResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent excepitons detected.
+ throw;
+ }
+
+ bike.Load(response, Mail, DateTimeProvider, Bikes.Bike.BC.NotifyPropertyChangedLevel.None);
+ }
+
+ /// Request to cancel a reservation.
+ /// Bike to cancel reservation.
+ public async Task DoCancelReservation(
+ Bikes.Bike.BC.IBikeInfoMutable bike)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException("Can not cancel reservation of bike. No bike object available.");
+ }
+
+ ReservationCancelReturnResponse response;
+ try
+ {
+ response = (await CopriServer.DoCancelReservationAsync(bike.Id, bike.OperatorUri)).GetIsCancelReservationResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent excepitons detected.
+ throw;
+ }
+
+ bike.Load(Bikes.Bike.BC.NotifyPropertyChangedLevel.None);
+ }
+
+ /// Get authentication keys.
+ /// Bike to get new keys for.
+ public async Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException("Can not calculate auth keys. No bike object available.");
+ }
+
+ switch (bike.State.Value)
+ {
+ case State.InUseStateEnum.Reserved:
+ case State.InUseStateEnum.Booked:
+ break;
+
+ default:
+ throw new ArgumentNullException($"Can not calculate auth keys. Unexpected bike state {bike.State.Value} detected.");
+ }
+
+ BikeInfoReservedOrBooked response;
+ Guid guid = (bike is BikeInfoMutable btBike) ? btBike.LockInfo.Guid : new Guid();
+ try
+ {
+ response = (await CopriServer.CalculateAuthKeysAsync(bike.Id, bike.OperatorUri)).GetIsBookingResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent excepitons detected.
+ throw;
+ }
+
+ UpdaterJSON.Load(
+ bike,
+ response,
+ Mail,
+ DateTimeProvider,
+ Bikes.Bike.BC.NotifyPropertyChangedLevel.None);
+ }
+
+ /// Updates COPRI lock state for a booked bike.
+ /// Bike to update locking state for.
+ /// Response on updating locking state.
+ public async Task UpdateLockingStateAsync(
+ Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException("Can not book bike. No bike object available.");
+ }
+
+ if (bike.State.Value != State.InUseStateEnum.Booked)
+ {
+ throw new ArgumentNullException($"Can not update locking state of bike. Unexpected booking state {bike.State} detected.");
+ }
+
+ lock_state? state = null;
+ switch (bike.LockInfo.State)
+ {
+ case LockingState.Open:
+ state = lock_state.unlocked;
+ break;
+
+ case LockingState.Closed:
+ state = lock_state.locked;
+ break;
+ }
+
+ if (!state.HasValue)
+ {
+ throw new ArgumentNullException($"Can not update locking state of bike. Unexpected locking state {bike.LockInfo.State} detected.");
+ }
+
+ try
+ {
+ (await CopriServer.UpdateLockingStateAsync(
+ bike.Id,
+ location,
+ state.Value,
+ bike.LockInfo.BatteryPercentage,
+ bike.OperatorUri)).GetIsBookingResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent excepitons detected.
+ throw;
+ }
+ }
+
+
+ /// Request to book a bike.
+ /// Bike to book.
+ public async Task DoBook(
+ Bikes.Bike.BluetoothLock.IBikeInfoMutable bike)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException(nameof(bike), "Can not book bike. No bike object available.");
+ }
+
+ BikeInfoReservedOrBooked response;
+ var btBike = bike as BikeInfoMutable;
+ Guid guid = btBike != null ? btBike.LockInfo.Guid : new Guid();
+ double batteryPercentage = btBike != null ? btBike.LockInfo.BatteryPercentage : double.NaN;
+ try
+ {
+ response = (await CopriServer.DoBookAsync(
+ bike.Id,
+ guid,
+ batteryPercentage,
+ bike.OperatorUri)).GetIsBookingResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent excepitons detected.
+ throw;
+ }
+
+ bike.Load(
+ response,
+ Mail,
+ DateTimeProvider,
+ Bikes.Bike.BC.NotifyPropertyChangedLevel.None);
+ }
+
+ /// Request to return a bike.
+ /// Latitude of the bike.
+ /// Longitude of the bike.
+ /// Bike to return.
+ public async Task DoReturn(
+ Bikes.Bike.BluetoothLock.IBikeInfoMutable bike,
+ LocationDto location)
+ {
+ if (bike == null)
+ {
+ throw new ArgumentNullException("Can not return bike. No bike object available.");
+ }
+
+ ReservationCancelReturnResponse l_oResponse;
+ try
+ {
+ l_oResponse = (await CopriServer.DoReturn(bike.Id, location, bike.OperatorUri)).GetIsReturnBikeResponseOk(bike.Id);
+ }
+ catch (Exception)
+ {
+ // Exception was not expected or too many subsequent exceptions detected.
+ throw;
+ }
+
+ bike.Load(Bikes.Bike.BC.NotifyPropertyChangedLevel.None);
+ }
+
+ ///
+ /// Submits feedback to copri server.
+ ///
+ /// Feedback to submit.
+ public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
+ => await CopriServer.DoSubmitFeedback(userFeedback.Message, userFeedback.IsBikeBroken, opertorUri);
+
+ }
+}
\ No newline at end of file
diff --git a/TINKLib/Model/Connector/Command/ICommand.cs b/TINKLib/Model/Connector/Command/ICommand.cs
new file mode 100644
index 0000000..b2f8be4
--- /dev/null
+++ b/TINKLib/Model/Connector/Command/ICommand.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Threading.Tasks;
+using TINK.Model.Repository.Request;
+using TINK.Model.User.Account;
+
+namespace TINK.Model.Connector
+{
+ public interface ICommand
+ {
+ /// Is raised whenever login state has changed.
+ event LoginStateChangedEventHandler LoginStateChanged;
+
+ ///
+ /// Logs user in.
+ /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
+ /// If log in fails (password modified) session cookie is set to empty.
+ /// If communication fails an exception is thrown.
+ ///
+ Task DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId);
+
+ /// Logs user out.
+ Task DoLogout();
+
+ /// Request to reserve a bike.
+ /// Bike to book.
+ Task DoReserve(Bikes.Bike.BC.IBikeInfoMutable p_oBike);
+
+ /// Request to cancel a reservation.
+ /// Bike to book.
+ Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable p_oBike);
+
+ /// Get authentication keys to connect to lock.
+ /// Bike to book.
+ Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike);
+
+ /// Updates COPRI lock state for a booked bike.
+ /// Id of the bike to update locking state for.
+ /// Geolocation of lock when returning bike.
+ /// Response on updating locking state.
+ Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location = null);
+
+ /// Request to book a bike.
+ /// Bike to book.
+ Task DoBook(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike);
+
+ /// Request to return a bike.
+ /// Geolocation of lock when returning bike.
+ /// Bike to return.
+ Task DoReturn(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto geolocation = null);
+
+ /// True if connector has access to copri server, false if cached values are used.
+ bool IsConnected { get; }
+
+ /// True if user is logged in false if not.
+ string SessionCookie { get; }
+
+ Task DoSubmitFeedback(IUserFeedback userFeedback, Uri opertorUri);
+
+ ///
+ /// Feedback given by user when returning bike.
+ ///
+ public interface IUserFeedback
+ {
+ ///
+ /// Holds whether bike is broken or not.
+ ///
+ bool IsBikeBroken { get; }
+
+ ///
+ /// Holds either
+ /// - general feedback
+ /// - error description of broken bike
+ /// or both.
+ ///
+ string Message { get; }
+ }
+ }
+
+ /// Defines delegate to be raised whenever login state changes.
+ /// Holds session cookie and mail address if user logged in successfully.
+ public delegate void LoginStateChangedEventHandler(object p_oSender, LoginStateChangedEventArgs p_oEventArgs);
+
+ /// Event arguments to notify about changes of logged in state.
+ public class LoginStateChangedEventArgs : EventArgs
+ {
+ public LoginStateChangedEventArgs() : this(string.Empty, string.Empty)
+ { }
+
+ public LoginStateChangedEventArgs(string p_strSessionCookie, string p_strMail)
+ {
+ SessionCookie = p_strSessionCookie;
+ Mail = p_strMail;
+ }
+
+ public string SessionCookie { get; }
+
+ public string Mail { get; }
+ }
+}
diff --git a/TINKLib/Model/Connector/Command/UserFeedbackDto.cs b/TINKLib/Model/Connector/Command/UserFeedbackDto.cs
new file mode 100644
index 0000000..b9c15ff
--- /dev/null
+++ b/TINKLib/Model/Connector/Command/UserFeedbackDto.cs
@@ -0,0 +1,9 @@
+
+namespace TINK.Model.Connector
+{
+ public record UserFeedbackDto : ICommand.IUserFeedback
+ {
+ public bool IsBikeBroken { get; init; }
+ public string Message { get; init; }
+ }
+}
diff --git a/TINKLib/Model/Connector/Connector.cs b/TINKLib/Model/Connector/Connector.cs
new file mode 100644
index 0000000..f2c6b5a
--- /dev/null
+++ b/TINKLib/Model/Connector/Connector.cs
@@ -0,0 +1,57 @@
+using System;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ ///
+ /// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...)
+ ///
+ public class Connector : IConnector
+ {
+ /// Constructs a copri connector object.
+ /// Uri to connect to.
+ /// Holds the name and version of the TINKApp.
+ /// /// Holds the session cookie.
+ /// Mail of user.
+ /// Timespan which holds value after which cache expires.
+ /// Provides cached addess to copri.
+ public Connector(
+ Uri activeUri,
+ string userAgent,
+ string sessionCookie,
+ string mail,
+ TimeSpan? expiresAfter = null,
+ ICachedCopriServer server = null )
+ {
+ Command = GetCommand(
+ server ?? new CopriProviderHttps(activeUri, TinkApp.MerchantId, userAgent, sessionCookie),
+ sessionCookie,
+ mail);
+
+ Query = GetQuery(
+ server ?? new CopriProviderHttps(activeUri, TinkApp.MerchantId, userAgent, sessionCookie, expiresAfter),
+ sessionCookie,
+ mail);
+ }
+
+ /// Object for queriying stations and bikes.
+ public ICommand Command { get; private set; }
+
+ /// Object for queriying stations and bikes.
+ public IQuery Query { get; private set; }
+
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => Command.IsConnected;
+
+ /// Gets a command object to perform copri commands.
+ public static ICommand GetCommand(ICopriServerBase copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
+ ? new Command(copri)
+ : new CommandLoggedIn(copri, sessioncookie, mail, () => DateTime.Now) as ICommand;
+
+ /// Gets a command object to perform copri queries.
+ private static IQuery GetQuery(ICachedCopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
+ ? new CachedQuery(copri) as IQuery
+ : new CachedQueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now);
+ }
+}
diff --git a/TINKLib/Model/Connector/ConnectorCache.cs b/TINKLib/Model/Connector/ConnectorCache.cs
new file mode 100644
index 0000000..48b03a7
--- /dev/null
+++ b/TINKLib/Model/Connector/ConnectorCache.cs
@@ -0,0 +1,47 @@
+using System;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ ///
+ /// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...)
+ ///
+ public class ConnectorCache : IConnector
+ {
+ /// Constructs a copri connector object.
+ /// Holds the session cookie.
+ /// Mail of user.
+ /// Provides addess to copri.
+ public ConnectorCache(
+ string sessionCookie,
+ string mail,
+ ICopriServer server = null)
+ {
+
+ Command = Connector.GetCommand(
+ server ?? new CopriProviderMonkeyStore(TinkApp.MerchantId, sessionCookie),
+ sessionCookie,
+ mail);
+
+ Query = GetQuery(
+ server ?? new CopriProviderMonkeyStore(TinkApp.MerchantId, sessionCookie),
+ sessionCookie,
+ mail);
+ }
+
+ /// Object for queriying stations and bikes.
+ public ICommand Command { get; private set; }
+
+ /// Object for queriying stations and bikes.
+ public IQuery Query { get; private set; }
+
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => Command.IsConnected;
+
+ /// Gets a command object to perform copri queries.
+ private static IQuery GetQuery(ICopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
+ ? new Query(copri) as IQuery
+ : new QueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now);
+ }
+}
diff --git a/TINKLib/Model/Connector/ConnectorFactory.cs b/TINKLib/Model/Connector/ConnectorFactory.cs
new file mode 100644
index 0000000..2526580
--- /dev/null
+++ b/TINKLib/Model/Connector/ConnectorFactory.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace TINK.Model.Connector
+{
+ public class ConnectorFactory
+ {
+ ///
+ /// Gets a connector object depending on whether beein onlin or offline.
+ ///
+ /// True if online, false if offline
+ ///
+ public static IConnector Create(bool isConnected, Uri activeUri, string userAgent, string sessionCookie, string mail, TimeSpan? expiresAfter = null)
+ {
+ return isConnected
+ ? new Connector(activeUri, userAgent, sessionCookie, mail, expiresAfter: expiresAfter) as IConnector
+ : new ConnectorCache(sessionCookie, mail);
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs b/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs
new file mode 100644
index 0000000..12362c8
--- /dev/null
+++ b/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs
@@ -0,0 +1,13 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace TINK.Model.Connector.Filter
+{
+ public static class GroupFilterFactory
+ {
+ public static IGroupFilter Create(IEnumerable group)
+ {
+ return group != null ? (IGroupFilter) new IntersectGroupFilter(group) : new NullGroupFilter();
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Filter/IGroupFilter.cs b/TINKLib/Model/Connector/Filter/IGroupFilter.cs
new file mode 100644
index 0000000..f32e4ca
--- /dev/null
+++ b/TINKLib/Model/Connector/Filter/IGroupFilter.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace TINK.Model.Connector.Filter
+{
+ public interface IGroupFilter
+ {
+ IEnumerable DoFilter(IEnumerable filter);
+ }
+}
diff --git a/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs b/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs
new file mode 100644
index 0000000..6500fb9
--- /dev/null
+++ b/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TINK.Model.Connector.Filter
+{
+ /// Filters to enumerations of string by intersecting.
+ public class IntersectGroupFilter : IGroupFilter
+ {
+ private IEnumerable Group { get; set; }
+
+ public IntersectGroupFilter(IEnumerable group) => Group = group ?? new List();
+
+ /// Applies filtering.
+ /// Enumeration of filter values to filter with or null if no filtering has to be applied.
+ ///
+ public IEnumerable DoFilter(IEnumerable filter) => filter != null
+ ? Group.Intersect(filter)
+ : Group;
+ }
+}
diff --git a/TINKLib/Model/Connector/Filter/NullGroupFilter.cs b/TINKLib/Model/Connector/Filter/NullGroupFilter.cs
new file mode 100644
index 0000000..e7ddb36
--- /dev/null
+++ b/TINKLib/Model/Connector/Filter/NullGroupFilter.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace TINK.Model.Connector.Filter
+{
+ public class NullGroupFilter : IGroupFilter
+ {
+ public IEnumerable DoFilter(IEnumerable filter) => filter;
+ }
+}
diff --git a/TINKLib/Model/Connector/FilterHelper.cs b/TINKLib/Model/Connector/FilterHelper.cs
new file mode 100644
index 0000000..58a2565
--- /dev/null
+++ b/TINKLib/Model/Connector/FilterHelper.cs
@@ -0,0 +1,11 @@
+namespace TINK.Model.Connector
+{
+ public static class FilterHelper
+ {
+ /// Holds the Konrad group (city bikes).
+ public const string FILTERKONRAD = "Konrad";
+
+ /// Holds the tink group (Lastenräder).
+ public const string FILTERTINKGENERAL = "TINK";
+ }
+}
diff --git a/TINKLib/Model/Connector/FilteredConnector.cs b/TINKLib/Model/Connector/FilteredConnector.cs
new file mode 100644
index 0000000..cbecfbf
--- /dev/null
+++ b/TINKLib/Model/Connector/FilteredConnector.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Connector.Filter;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Station;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model.Connector
+{
+ /// Filters connector respones.
+ /// Former name: Filter
+ public class FilteredConnector : IFilteredConnector
+ {
+ /// Constructs a filter object.
+ /// Filter group.
+ /// Connector object.
+ public FilteredConnector(
+ IEnumerable group,
+ IConnector connector)
+ {
+ Connector = connector;
+
+ if (Connector == null)
+ {
+ throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null.");
+ }
+
+ Query = new QueryProvider(Connector.Query, GroupFilterFactory.Create(group));
+ }
+
+ /// Inner connector object.
+ public IConnector Connector { get; }
+
+ /// Command object.
+ public ICommand Command => Connector.Command;
+
+ /// Object to query information.
+ public IQuery Query { get; }
+
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => Connector.IsConnected;
+
+ /// Object to perform filtered queries.
+ private class QueryProvider : IQuery
+ {
+ /// Holds the filter.
+ private IGroupFilter Filter { get; }
+
+ /// Holds the reference to object which performs copry queries.
+ private IQuery m_oInnerQuery;
+
+ /// Constructs a query object.
+ ///
+ ///
+ public QueryProvider(IQuery innerQuerry, IGroupFilter filter)
+ {
+ m_oInnerQuery = innerQuerry;
+ Filter = filter;
+ }
+
+ /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in.
+ public async Task> GetBikesAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesAsync();
+ return new Result(
+ result.Source,
+ new BikeCollection(DoFilter(result.Response, Filter)),
+ result.Exception);
+ }
+
+ /// Gets bikes occupied if a user is logged in.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesOccupiedAsync();
+ return new Result(
+ result.Source,
+ new BikeCollection(result.Response.ToDictionary(x => x.Id)),
+ result.Exception);
+ }
+
+ /// Gets all station applying filter rules.
+ ///
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesAndStationsAsync();
+
+ return new Result(
+ result.Source,
+ new StationsAndBikesContainer(
+ new StationDictionary(result.Response.StationsAll.CopriVersion, DoFilter(result.Response.StationsAll, Filter)),
+ new BikeCollection(DoFilter(result.Response.Bikes, Filter))),
+ result.Exception);
+ }
+
+ /// Filter bikes by group.
+ /// Bikes to filter.
+ /// Filtered bikes.
+ private static Dictionary DoFilter(BikeCollection p_oBikes, IGroupFilter filter)
+ {
+ return p_oBikes.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary(x => x.Id);
+ }
+
+ /// Filter stations by broup.
+ ///
+ private static Dictionary DoFilter(StationDictionary p_oStations, IGroupFilter filter)
+ {
+ return p_oStations.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary((x => x.Id));
+ }
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/FilteredConnectorFactory.cs b/TINKLib/Model/Connector/FilteredConnectorFactory.cs
new file mode 100644
index 0000000..8005f01
--- /dev/null
+++ b/TINKLib/Model/Connector/FilteredConnectorFactory.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace TINK.Model.Connector
+{
+ public static class FilteredConnectorFactory
+ {
+ /// Creates a filter object.
+ ///
+ public static IFilteredConnector Create(IEnumerable group, IConnector connector)
+ {
+ return group != null
+ ? (IFilteredConnector) new FilteredConnector(group, connector)
+ : new NullFilterConnector(connector);
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/IConnector.cs b/TINKLib/Model/Connector/IConnector.cs
new file mode 100644
index 0000000..7cebd34
--- /dev/null
+++ b/TINKLib/Model/Connector/IConnector.cs
@@ -0,0 +1,14 @@
+namespace TINK.Model.Connector
+{
+ public interface IConnector
+ {
+ /// Object for queriying stations and bikes.
+ ICommand Command { get; }
+
+ /// Object for queriying stations and bikes.
+ IQuery Query { get; }
+
+ /// True if connector has access to copri server, false if cached values are used.
+ bool IsConnected { get; }
+ }
+}
diff --git a/TINKLib/Model/Connector/IFilteredConnector.cs b/TINKLib/Model/Connector/IFilteredConnector.cs
new file mode 100644
index 0000000..d58149d
--- /dev/null
+++ b/TINKLib/Model/Connector/IFilteredConnector.cs
@@ -0,0 +1,7 @@
+namespace TINK.Model.Connector
+{
+ public interface IFilteredConnector : IConnector
+ {
+ IConnector Connector { get; }
+ }
+}
diff --git a/TINKLib/Model/Connector/NullFilterConnector.cs b/TINKLib/Model/Connector/NullFilterConnector.cs
new file mode 100644
index 0000000..9cce17f
--- /dev/null
+++ b/TINKLib/Model/Connector/NullFilterConnector.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Station;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model.Connector
+{
+ /// Filters connector respones.
+ public class NullFilterConnector : IFilteredConnector
+ {
+ /// Constructs a filter object.
+ /// Filter group.
+ /// Connector object.
+ public NullFilterConnector(
+ IConnector connector)
+ {
+ Connector = connector;
+
+ if (Connector == null)
+ {
+ throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null.");
+ }
+
+ Query = new QueryProvider(Connector.Query);
+ }
+
+ /// Inner connector object.
+ public IConnector Connector { get; }
+
+ /// Command object.
+ public ICommand Command => Connector.Command;
+
+ /// Object to query information.
+ public IQuery Query { get; }
+
+ /// True if connector has access to copri server, false if cached values are used.
+ public bool IsConnected => Connector.IsConnected;
+
+ /// Object to perform filtered queries.
+ private class QueryProvider : IQuery
+ {
+ /// Holds the reference to object which performs copry queries.
+ private IQuery m_oInnerQuery;
+
+ /// Constructs a query object.
+ ///
+ ///
+ public QueryProvider(IQuery p_oInnerQuery)
+ {
+ m_oInnerQuery = p_oInnerQuery;
+ }
+
+ /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in.
+ public async Task> GetBikesAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesAsync();
+ return new Result(
+ result.Source,
+ new BikeCollection(result.Response.ToDictionary(x => x.Id)),
+ result.Exception);
+ }
+
+ /// Gets bikes occupied if a user is logged in.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesOccupiedAsync();
+ return new Result(
+ result.Source,
+ new BikeCollection(result.Response.ToDictionary(x => x.Id)),
+ result.Exception);
+ }
+
+ /// Gets all station applying filter rules.
+ ///
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var result = await m_oInnerQuery.GetBikesAndStationsAsync();
+
+ return new Result(
+ result.Source,
+ new StationsAndBikesContainer(
+ new StationDictionary(result.Response.StationsAll.CopriVersion, result.Response.StationsAll.ToDictionary(x => x.Id)),
+ new BikeCollection(result.Response.Bikes.ToDictionary(x => x.Id))),
+ result.Exception);
+ }
+
+ /// Filter bikes by group.
+ /// Bikes to filter.
+ /// Filtered bikes.
+ public static Dictionary DoFilter(BikeCollection p_oBikes, IEnumerable p_oFilter)
+ {
+ return p_oBikes.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary(x => x.Id);
+ }
+
+ /// Filter stations by broup.
+ ///
+ public static Dictionary DoFilter(StationDictionary p_oStations, IEnumerable p_oFilter)
+ {
+ return p_oStations.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary((x => x.Id));
+ }
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/Base.cs b/TINKLib/Model/Connector/Query/Base.cs
new file mode 100644
index 0000000..62daab0
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/Base.cs
@@ -0,0 +1,27 @@
+using System;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ ///
+ /// Provides information required for copri commands/ query operations.
+ ///
+ public class Base
+ {
+ /// Reference to object which provides access to copri server.
+ protected ICopriServerBase CopriServer { get; }
+
+ /// Gets the merchant id.
+ protected string MerchantId => CopriServer.MerchantId;
+
+ /// Constructs a query base object.
+ /// Server which implements communication.
+ /// Object which hold communication objects.
+ protected Base(
+ ICopriServerBase p_oCopriServer)
+ {
+ CopriServer = p_oCopriServer
+ ?? throw new ArgumentException("Can not instantiate command/ query base- object. Copri server object must never be null or emtpy.");
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/BaseLoggedIn.cs b/TINKLib/Model/Connector/Query/BaseLoggedIn.cs
new file mode 100644
index 0000000..604b138
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/BaseLoggedIn.cs
@@ -0,0 +1,39 @@
+using System;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ /// Holds user infromation required for copri related commands/ query operations.
+ public class BaseLoggedIn : Base
+ {
+ /// Session cookie used to sign in to copri.
+ public string SessionCookie { get; }
+
+ /// Mail address of the user.
+ protected string Mail { get; }
+
+ /// Object which provides date time info.
+ protected readonly Func DateTimeProvider;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public BaseLoggedIn(ICopriServerBase p_oCopriServer,
+ string p_strSessionCookie,
+ string p_strMail,
+ Func p_oDateTimeProvider) : base(p_oCopriServer)
+ {
+ if (string.IsNullOrEmpty(p_strSessionCookie))
+ throw new ArgumentException("Can not instantiate query object- object. Session cookie must never be null or emtpy.");
+
+ if (string.IsNullOrEmpty(p_strMail))
+ throw new ArgumentException("Can not instantiate query object- object. Mail address must never be null or emtpy.");
+
+ DateTimeProvider = p_oDateTimeProvider
+ ?? throw new ArgumentException("Can not instantiate connector- object. No date time provider object available.");
+
+ SessionCookie = p_strSessionCookie;
+
+ Mail = p_strMail;
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/CachedQuery.cs b/TINKLib/Model/Connector/Query/CachedQuery.cs
new file mode 100644
index 0000000..aab461b
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/CachedQuery.cs
@@ -0,0 +1,86 @@
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model.Connector
+{
+ public class CachedQuery : Base, IQuery
+ {
+ /// Cached copri server.
+ private readonly ICachedCopriServer server;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public CachedQuery(
+ ICopriServerBase p_oCopriServer) : base(p_oCopriServer)
+ {
+ server = p_oCopriServer as ICachedCopriServer;
+ if (server == null)
+ {
+ throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}.");
+ }
+ }
+
+ /// Gets all stations including postions and bikes.
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var resultStations = await server.GetStations();
+
+ if (resultStations.Source == typeof(CopriCallsMonkeyStore))
+ {
+ // Communication with copri in order to get stations failed.
+ return new Result(
+ resultStations.Source,
+ new StationsAndBikesContainer(
+ resultStations.Response.GetStationsAllMutable(),
+ (await server.GetBikesAvailable(true)).Response.GetBikesAvailable()),
+ resultStations.Exception);
+ }
+
+ var resultBikes = await server.GetBikesAvailable();
+ if (resultBikes.Source == typeof(CopriCallsMonkeyStore))
+ {
+ // Communication with copri in order to get bikes failed.
+ return new Result(
+ resultBikes.Source,
+ new StationsAndBikesContainer(
+ (await server.GetStations(true)).Response.GetStationsAllMutable(),
+ resultBikes.Response.GetBikesAvailable()),
+ resultBikes.Exception);
+ }
+
+ // Communicatin with copri succeeded.
+ server.AddToCache(resultStations);
+ server.AddToCache(resultBikes);
+
+ return new Result(
+ resultStations.Source,
+ new StationsAndBikesContainer(resultStations.Response.GetStationsAllMutable(), resultBikes.Response.GetBikesAvailable()));
+ }
+
+ /// Gets bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ Log.ForContext().Error("Unexpected call to get be bikes occpied detected. No user is logged in.");
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ await Task.Run(() => new BikeCollection(new Dictionary())),
+ new System.Exception("Abfrage der reservierten/ gebuchten Räder nicht möglich. Kein Benutzer angemeldet."));
+ }
+
+ /// Gets bikes available.
+ /// Collection of bikes.
+ public async Task> GetBikesAsync()
+ {
+ var result = await server.GetBikesAvailable();
+ server.AddToCache(result);
+ return new Result(result.Source, result.Response.GetBikesAvailable(), result.Exception);
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs b/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs
new file mode 100644
index 0000000..7f03d6b
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs
@@ -0,0 +1,159 @@
+using MonkeyCache.FileStore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ /// Provides query functionality for a logged in user.
+ public class CachedQueryLoggedIn : BaseLoggedIn, IQuery
+ {
+ /// Cached copri server.
+ private readonly ICachedCopriServer server;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public CachedQueryLoggedIn(ICopriServerBase p_oCopriServer,
+ string p_strSessionCookie,
+ string p_strMail,
+ Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider)
+ {
+ server = p_oCopriServer as ICachedCopriServer;
+ if (server == null)
+ {
+ throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}.");
+ }
+ }
+
+ /// Gets all stations including postions.
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var resultStations = await server.GetStations();
+
+ if (resultStations.Source == typeof(CopriCallsMonkeyStore)
+ || resultStations.Exception != null)
+ {
+ // Stations were read from cache ==> get bikes availbalbe and occupied from cache as well to avoid inconsistencies
+ return new Result(
+ resultStations.Source,
+ new StationsAndBikesContainer(
+ resultStations.Response.GetStationsAllMutable(),
+ UpdaterJSON.GetBikesAll(
+ (await server.GetBikesAvailable(true)).Response,
+ (await server.GetBikesOccupied(true)).Response,
+ Mail,
+ DateTimeProvider)),
+ resultStations.Exception);
+ }
+
+ var l_oBikesAvailableResponse = await server.GetBikesAvailable();
+ if (l_oBikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore)
+ || l_oBikesAvailableResponse.Exception != null)
+ {
+ // Bikes avilable were read from cache ==> get bikes occupied from cache as well to avoid inconsistencies
+ return new Result(
+ l_oBikesAvailableResponse.Source,
+ new StationsAndBikesContainer(
+ (await server.GetStations(true)).Response.GetStationsAllMutable(),
+ UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse.Response,
+ (await server.GetBikesOccupied(true)).Response,
+ Mail,
+ DateTimeProvider)),
+ l_oBikesAvailableResponse.Exception);
+ }
+
+ var l_oBikesOccupiedResponse = await server.GetBikesOccupied();
+ if (l_oBikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore)
+ || l_oBikesOccupiedResponse.Exception != null)
+ {
+ // Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies
+ return new Result(
+ l_oBikesOccupiedResponse.Source,
+ new StationsAndBikesContainer(
+ (await server.GetStations(true)).Response.GetStationsAllMutable(),
+ UpdaterJSON.GetBikesAll(
+ (await server.GetBikesAvailable(true)).Response,
+ l_oBikesOccupiedResponse.Response,
+ Mail,
+ DateTimeProvider)),
+ l_oBikesOccupiedResponse.Exception);
+ }
+
+ // Both types bikes could read from copri => update cache
+ server.AddToCache(resultStations);
+ server.AddToCache(l_oBikesAvailableResponse);
+ server.AddToCache(l_oBikesOccupiedResponse);
+
+ var exceptions = new[] { resultStations?.Exception, l_oBikesAvailableResponse?.Exception, l_oBikesOccupiedResponse?.Exception }.Where(x => x != null).ToArray();
+
+ return new Result(
+ resultStations.Source,
+ new StationsAndBikesContainer(
+ resultStations.Response.GetStationsAllMutable(),
+ UpdaterJSON.GetBikesAll(
+ l_oBikesAvailableResponse.Response,
+ l_oBikesOccupiedResponse.Response,
+ Mail,
+ DateTimeProvider)),
+ exceptions.Length > 0 ? new AggregateException(exceptions) : null);
+ }
+
+ /// Gets bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ var result = await server.GetBikesOccupied();
+ server.AddToCache(result);
+ return new Result(result.Source, result.Response.GetBikesOccupied(Mail, DateTimeProvider), result.Exception);
+ }
+
+ /// Gets bikes available and bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesAsync()
+ {
+ var l_oBikesAvailableResponse = await server.GetBikesAvailable();
+
+ if (l_oBikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore)
+ || l_oBikesAvailableResponse.Exception != null)
+ {
+ // Bikes avilable were read from cache ==> get bikes occupied from cache as well to avoid inconsistencies
+ return new Result(
+ l_oBikesAvailableResponse.Source,
+ UpdaterJSON.GetBikesAll(
+ l_oBikesAvailableResponse.Response,
+ (await server.GetBikesOccupied(true)).Response,
+ Mail,
+ DateTimeProvider),
+ l_oBikesAvailableResponse.Exception);
+ }
+
+ var l_oBikesOccupiedResponse = await server.GetBikesOccupied();
+ if (l_oBikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore)
+ || l_oBikesOccupiedResponse.Exception != null)
+ {
+ // Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies
+ return new Result(
+ l_oBikesOccupiedResponse.Source,
+ UpdaterJSON.GetBikesAll(
+ (await server.GetBikesAvailable(true)).Response,
+ l_oBikesOccupiedResponse.Response,
+ Mail,
+ DateTimeProvider),
+ l_oBikesOccupiedResponse.Exception);
+
+ }
+
+ // Both types bikes could read from copri => update cache
+ server.AddToCache(l_oBikesAvailableResponse);
+ server.AddToCache(l_oBikesOccupiedResponse);
+
+ return new Result(
+ l_oBikesAvailableResponse.Source,
+ UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse.Response, l_oBikesOccupiedResponse.Response, Mail, DateTimeProvider),
+ l_oBikesAvailableResponse.Exception != null || l_oBikesOccupiedResponse.Exception != null ? new AggregateException(new[] { l_oBikesAvailableResponse.Exception, l_oBikesOccupiedResponse.Exception }) : null);
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/IQuery.cs b/TINKLib/Model/Connector/Query/IQuery.cs
new file mode 100644
index 0000000..4aed98b
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/IQuery.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+
+namespace TINK.Model.Connector
+{
+ public interface IQuery
+ {
+ /// Gets all stations including postions.
+ Task> GetBikesAndStationsAsync();
+
+ /// Gets bikes occupied is a user is logged in.
+ /// Collection of bikes.
+ Task> GetBikesOccupiedAsync();
+
+ /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in.
+ /// Collection of bikes.
+ Task> GetBikesAsync();
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/Query.cs b/TINKLib/Model/Connector/Query/Query.cs
new file mode 100644
index 0000000..c645cfd
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/Query.cs
@@ -0,0 +1,60 @@
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+
+namespace TINK.Model.Connector
+{
+ /// Provides query functionality without login.
+ public class Query : Base, IQuery
+ {
+ /// Cached copri server.
+ private readonly ICopriServer server;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public Query(ICopriServerBase p_oCopriServer) : base(p_oCopriServer)
+ {
+ server = p_oCopriServer as ICopriServer;
+ if (server == null)
+ {
+ throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}.");
+ }
+ }
+
+ /// Gets all stations including postions.
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var stationsAllResponse = await server.GetStationsAsync();
+ var bikesAvailableResponse = await server.GetBikesAvailableAsync();
+
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ new StationsAndBikesContainer( stationsAllResponse.GetStationsAllMutable(), bikesAvailableResponse.GetBikesAvailable()));
+ }
+
+ /// Gets bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ Log.ForContext().Error("Unexpected call to get be bikes occpied detected. No user is logged in.");
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ await Task.Run(() => new BikeCollection(new Dictionary())),
+ new System.Exception("Abfrage der reservierten/ gebuchten Räder fehlgeschlagen. Kein Benutzer angemeldet."));
+ }
+
+ /// Gets bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesAsync()
+ {
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ (await server.GetBikesAvailableAsync()).GetBikesAvailable());
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Query/QueryLoggedIn.cs b/TINKLib/Model/Connector/Query/QueryLoggedIn.cs
new file mode 100644
index 0000000..a8d6b39
--- /dev/null
+++ b/TINKLib/Model/Connector/Query/QueryLoggedIn.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Threading.Tasks;
+using TINK.Model.Bike;
+using TINK.Model.Services.CopriApi;
+using TINK.Model.Repository;
+
+namespace TINK.Model.Connector
+{
+ /// Provides query functionality for a logged in user.
+ public class QueryLoggedIn : BaseLoggedIn, IQuery
+ {
+ /// Cached copri server.
+ private readonly ICopriServer server;
+
+ /// Constructs a copri query object.
+ /// Server which implements communication.
+ public QueryLoggedIn(ICopriServerBase p_oCopriServer,
+ string p_strSessionCookie,
+ string p_strMail,
+ Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider)
+ {
+ server = p_oCopriServer as ICopriServer;
+ if (server == null)
+ {
+ throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}.");
+ }
+
+ server = p_oCopriServer as ICopriServer;
+ }
+
+ /// Gets all stations including postions.
+ public async Task> GetBikesAndStationsAsync()
+ {
+ var stationResponse = await server.GetStationsAsync();
+ var bikesAvailableResponse = await server.GetBikesAvailableAsync();
+ var bikesOccupiedResponse = await server.GetBikesOccupiedAsync();
+
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ new StationsAndBikesContainer(
+ stationResponse.GetStationsAllMutable(),
+ UpdaterJSON.GetBikesAll(bikesAvailableResponse, bikesOccupiedResponse, Mail, DateTimeProvider)));
+ }
+
+ /// Gets bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesOccupiedAsync()
+ {
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ (await server.GetBikesOccupiedAsync()).GetBikesOccupied(Mail, DateTimeProvider));
+ }
+ /// Gets bikes available and bikes occupied.
+ /// Collection of bikes.
+ public async Task> GetBikesAsync()
+ {
+ var l_oBikesAvailableResponse = await server.GetBikesAvailableAsync();
+ var l_oBikesOccupiedResponse = await server.GetBikesOccupiedAsync();
+
+ return new Result(
+ typeof(CopriCallsMonkeyStore),
+ UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse, l_oBikesOccupiedResponse, Mail, DateTimeProvider));
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/TextToTypeHelper.cs b/TINKLib/Model/Connector/TextToTypeHelper.cs
new file mode 100644
index 0000000..99468d2
--- /dev/null
+++ b/TINKLib/Model/Connector/TextToTypeHelper.cs
@@ -0,0 +1,358 @@
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.RegularExpressions;
+using TINK.Model.Bike;
+using TINK.Model.Repository.Exception;
+using TINK.Model.Repository.Response;
+using TINK.Model.Services.CopriApi.ServerUris;
+using TINK.Model.State;
+
+namespace TINK.Model.Connector
+{
+ ///
+ /// Conversion helper functionality.
+ ///
+ public static class TextToTypeHelper
+ {
+ /// Holds the text for demo bikes.
+ private const string DEMOBIKEMARKER = "DEMO";
+
+ /// Part text denoting two wheel cargo bike..
+ private const string TWOWHEELCARGOMARKERFRAGMENT = "LONG";
+
+ ///
+ /// Gets the position from StationInfo object.
+ ///
+ /// Object to get information from.
+ /// Position information.
+ public static Station.Position GetPosition(this StationsAllResponse.StationInfo p_oStationInfo)
+ {
+ return GetPosition(p_oStationInfo.gps);
+ }
+
+ /// Gets the position from StationInfo object.
+ /// Object to get information from.
+ /// Position information.
+ public static IEnumerable GetGroup(this AuthorizationResponse p_oAuthorizationResponse)
+ {
+ try
+ {
+ return p_oAuthorizationResponse.user_group.GetGroup();
+ }
+ catch (Exception l_oException)
+ {
+ throw new Exception($"Can not get group of user from text \"{p_oAuthorizationResponse.user_group}\".", l_oException);
+ }
+ }
+
+ /// Gets the position from StationInfo object.
+ /// Object to get information from.
+ /// Position information.
+ public static IEnumerable GetGroup(this string p_oGroup)
+ {
+ if (string.IsNullOrEmpty(p_oGroup))
+ {
+ throw new ArgumentException("Can not get goup form string. Group text can not be null.");
+ }
+
+ return new HashSet(p_oGroup.Split(',')).ToList();
+ }
+
+ /// Gets the position from StationInfo object.
+ /// Object to get information from.
+ /// Position information.
+ public static string GetGroup(this IEnumerable p_oGroup)
+ {
+ return string.Join(",", p_oGroup);
+ }
+
+ /// Gets the position from StationInfo object.
+ /// Object to get information from.
+ /// Position information.
+ public static IEnumerable GetGroup(this StationsAllResponse.StationInfo p_oStationInfo)
+ {
+ try
+ {
+ return p_oStationInfo.station_group.GetGroup();
+ }
+ catch (System.Exception l_oException)
+ {
+ throw new System.Exception($"Can not get group of stations from text \"{p_oStationInfo.station_group}\".", l_oException);
+ }
+ }
+
+ ///
+ /// Gets the position from StationInfo object.
+ ///
+ /// Object to get information from.
+ /// Position information.
+ public static Station.Position GetPosition(this BikeInfoAvailable p_oBikeInfo)
+ {
+ return GetPosition(p_oBikeInfo.gps);
+ }
+
+ ///
+ /// Gets the position from StationInfo object.
+ ///
+ /// Object to get information from.
+ /// Position information.
+ public static InUseStateEnum GetState(this BikeInfoBase p_oBikeInfo)
+ {
+ var l_oState = p_oBikeInfo.state;
+
+ if (string.IsNullOrEmpty(l_oState))
+ {
+ throw new InvalidResponseException(
+ string.Format("Unknown reservation state detected. Member {0}.{1}.", typeof(BikeInfoBase), nameof(BikeInfoBase.state)),
+ p_oBikeInfo);
+ }
+
+ if (l_oState == "available")
+ {
+ return InUseStateEnum.Disposable;
+ }
+ else if (l_oState == "reserved" ||
+ l_oState == "requested")
+ {
+ return InUseStateEnum.Reserved;
+ }
+ else if (l_oState == "booked" ||
+ l_oState == "occupied")
+ {
+ return InUseStateEnum.Booked;
+ }
+
+ throw new CommunicationException(string.Format("Unknown bike state detected. State is {0}.", l_oState));
+ }
+
+ ///
+ /// Gets the from date information from JSON.
+ ///
+ /// JSON to get information from..
+ /// From information.
+ public static DateTime GetFrom(this BikeInfoReservedOrBooked p_oBikeInfo)
+ {
+ return DateTime.Parse(p_oBikeInfo.start_time);
+ }
+
+ ///
+ /// Gets whether the bike is a trike or not.
+ ///
+ /// JSON to get information from..
+ /// From information.
+ public static bool? GetIsDemo(this BikeInfoBase p_oBikeInfo)
+ {
+ return p_oBikeInfo?.description != null
+ ? p_oBikeInfo.description.ToUpper().Contains(DEMOBIKEMARKER)
+ : (bool?) null;
+ }
+
+ ///
+ /// Gets whether the bike is a trike or not.
+ ///
+ /// JSON to get information from..
+ /// From information.
+ public static IEnumerable GetGroup(this BikeInfoBase p_oBikeInfo)
+ {
+ try
+ {
+ return p_oBikeInfo?.bike_group?.GetGroup()?.ToList() ?? new List();
+ }
+ catch (System.Exception l_oException)
+ {
+ throw new System.Exception($"Can not get group of user from text \"{p_oBikeInfo.bike_group}\".", l_oException);
+ }
+ }
+
+ /// Gets whether the bike has a bord computer or not.
+ /// JSON to get information from.
+ /// From information.
+ public static bool GetIsManualLockBike(this BikeInfoBase p_oBikeInfo)
+ {
+ return !string.IsNullOrEmpty(p_oBikeInfo.system)
+ && p_oBikeInfo.system.ToUpper().StartsWith("LOCK");
+ }
+
+ /// Gets whether the bike has a bord computer or not.
+ /// JSON to get information from..
+ /// From information.
+ public static bool GetIsBluetoothLockBike(this BikeInfoBase p_oBikeInfo)
+ {
+ return !string.IsNullOrEmpty(p_oBikeInfo.system)
+ && p_oBikeInfo.system.ToUpper().StartsWith("ILOCKIT");
+ }
+
+ /// Gets whether the bike has a bord computer or not.
+ /// JSON to get information from..
+ /// From information.
+ public static int GetBluetoothLockId(this BikeInfoAvailable bikeInfo)
+ {
+ return TextToLockItTypeHelper.GetBluetoothLockId(bikeInfo?.Ilockit_ID);
+ }
+
+ /// Gets whether the bike has a bord computer or not.
+ /// JSON to get information from..
+ /// From information.
+ public static Guid GetBluetoothLockGuid(this BikeInfoAvailable bikeInfo)
+ {
+ // return new Guid("00000000-0000-0000-0000-e57e6b9aee16");
+ return Guid.TryParse(bikeInfo?.Ilockit_GUID, out Guid lockGuid)
+ ? lockGuid
+ : TextToLockItTypeHelper.INVALIDLOCKGUID;
+ }
+
+ public static byte[] GetUserKey(this BikeInfoReservedOrBooked bikeInfo)
+ {
+ return GetKey(bikeInfo.K_u);
+ }
+
+ public static byte[] GetAdminKey(this BikeInfoReservedOrBooked bikeInfo)
+ {
+ return GetKey(bikeInfo.K_a);
+ }
+
+ public static byte[] GetSeed(this BikeInfoReservedOrBooked bikeInfo)
+ {
+ return GetKey(bikeInfo.K_seed);
+ }
+
+ ///
+ /// Get array of keys from string of format "[12, -9, 5]"
+ ///
+ ///
+ ///
+ private static byte[] GetKey(string keyArrayText)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(keyArrayText))
+ return new byte[0];
+
+ return Regex.Replace(keyArrayText, @"\[(.*)\]", "$1").Split(',').Select(x => (byte)sbyte.Parse(x)).ToArray();
+ }
+ catch (Exception exception)
+ {
+ Log.Error("Can not extract K_u/ K_a/ or K_seed. Key {ArrayText} does not is not of expected format. {Exception}", keyArrayText, exception);
+ return new byte[0];
+ }
+ }
+
+ ///
+ /// Gets whether the bike is a trike or not.
+ ///
+ /// JSON to get information from..
+ /// From information.
+ public static WheelType? GetWheelType(this BikeInfoBase bikeInfo)
+ {
+ var l_oDescription = bikeInfo.description;
+
+ if (l_oDescription == null)
+ {
+ // Can not get type of wheel if description text is empty.
+ return null;
+ }
+
+ foreach (WheelType l_oWheelType in Enum.GetValues(typeof(WheelType)))
+ {
+
+ if (l_oDescription.ToUpper().Contains(l_oWheelType.ToString().ToUpper()))
+ {
+ return l_oWheelType;
+ }
+ }
+
+ // Check for custom value "Long".
+ if (l_oDescription.ToUpper().Contains(TWOWHEELCARGOMARKERFRAGMENT))
+ {
+ return WheelType.Two;
+ }
+
+ // Check for Stadrad.
+ if (GetTypeOfBike(bikeInfo) == TypeOfBike.Citybike)
+ {
+ return WheelType.Two;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the type of bike.
+ ///
+ /// Object to get bike type from.
+ /// Type of bike.
+ public static TypeOfBike? GetTypeOfBike(this BikeInfoBase bikeInfo)
+ {
+ var l_oDescription = bikeInfo?.description;
+
+ if (l_oDescription == null)
+ {
+ // Can not get type of wheel if description text is empty.
+ return null;
+ }
+
+ foreach (TypeOfBike l_oTypeOfBike in Enum.GetValues(typeof(TypeOfBike)))
+ {
+ if (l_oDescription.ToUpper().Contains(l_oTypeOfBike.GetCopriText().ToUpper()))
+ {
+ return l_oTypeOfBike;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Get position from a ,- separated string.
+ ///
+ /// Text to extract positon from.
+ /// Position object.
+ public static Station.Position GetPosition(string p_strGps)
+ {
+ if (p_strGps == null)
+ {
+ return null;
+ }
+
+ var l_oPosition = p_strGps.Split(',');
+
+ if (l_oPosition.Length != 2)
+ return null;
+
+ double l_oLatitude;
+ if (!double.TryParse(l_oPosition[0], NumberStyles.Float, CultureInfo.InvariantCulture, out l_oLatitude))
+ return null;
+
+ double l_oLongitude;
+ if (!double.TryParse(l_oPosition[1], NumberStyles.Float, CultureInfo.InvariantCulture, out l_oLongitude))
+ return null;
+
+ return new Station.Position(l_oLatitude, l_oLongitude);
+ }
+
+ /// Gets text of bike from.
+ /// Type to get text for.
+ ///
+ public static string GetCopriText(this TypeOfBike p_eType)
+ {
+ switch (p_eType)
+ {
+ case TypeOfBike.Citybike:
+ return "Stadtrad";
+
+ default:
+ return p_eType.ToString();
+ }
+ }
+
+ public static Uri GetOperatorUri(this BikeInfoBase bikeInfo)
+ {
+ return bikeInfo?.uri_operator != null && !string.IsNullOrEmpty(bikeInfo?.uri_operator)
+ ? new Uri($"{bikeInfo.uri_operator}/{CopriServerUriList.REST_RESOURCE_ROOT}")
+ : null;
+ }
+ }
+}
diff --git a/TINKLib/Model/Connector/Updater/UpdaterJSON.cs b/TINKLib/Model/Connector/Updater/UpdaterJSON.cs
new file mode 100644
index 0000000..3e4fd21
--- /dev/null
+++ b/TINKLib/Model/Connector/Updater/UpdaterJSON.cs
@@ -0,0 +1,496 @@
+using System;
+using TINK.Model.Bike;
+using TINK.Model.Station;
+using TINK.Model.Repository.Response;
+using TINK.Model.User.Account;
+using System.Collections.Generic;
+using TINK.Model.State;
+using TINK.Model.Repository.Exception;
+using Serilog;
+
+using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
+using IBikeInfoMutable = TINK.Model.Bikes.Bike.BC.IBikeInfoMutable;
+using System.Globalization;
+
+namespace TINK.Model.Connector
+{
+ ///
+ /// Connects TINK app to copri using JSON as input data format.
+ ///
+ /// Rename to UpdateFromCopri.
+ public static class UpdaterJSON
+ {
+ /// Loads a bike object from copri server cancel reservation/ booking update request.
+ /// Bike object to load response into.
+ /// Controls whether notify property changed events are fired or not.
+ public static void Load(
+ this IBikeInfoMutable bike,
+ Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel)
+ {
+
+ bike.State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel);
+ }
+ ///
+ /// Gets all statsion for station provider and add them into station list.
+ ///
+ /// List of stations to update.
+ public static StationDictionary GetStationsAllMutable(this StationsAllResponse p_oStationsAllResponse)
+ {
+ // Get stations from Copri/ file/ memory, ....
+ if (p_oStationsAllResponse == null
+ || p_oStationsAllResponse.stations == null)
+ {
+ // Latest list of stations could not be retrieved from provider.
+ return new StationDictionary();
+ }
+
+ Version.TryParse(p_oStationsAllResponse.copri_version, out Version l_oCopriVersion);
+
+ var l_oStations = new StationDictionary(p_oVersion: l_oCopriVersion);
+
+ foreach (var l_oStation in p_oStationsAllResponse.stations)
+ {
+ if (l_oStations.GetById(l_oStation.Value.station) != null)
+ {
+ // Can not add station to list of station. Id is not unique.
+ throw new InvalidResponseException(
+ string.Format("Station id {0} is not unique.", l_oStation.Value.station), p_oStationsAllResponse);
+ }
+
+ l_oStations.Add(new Station.Station(
+ l_oStation.Value.station,
+ l_oStation.Value.GetGroup(),
+ l_oStation.Value.GetPosition(),
+ l_oStation.Value.description));
+ }
+
+ return l_oStations;
+ }
+
+ /// Gets account object from login response.
+ /// Needed to extract cookie from autorization response.
+ /// Response to get session cookie and debug level from.
+ /// Mail address needed to construct a complete account object (is not part of response).
+ /// Password needed to construct a complete account object (is not part of response).
+ public static IAccount GetAccount(
+ this AuthorizationResponse loginResponse,
+ string merchantId,
+ string mail,
+ string password)
+ {
+ if (loginResponse == null)
+ {
+ throw new ArgumentNullException("p_oLoginResponse");
+ }
+
+ return new Account(
+ mail,
+ password,
+ loginResponse.authcookie?.Replace(merchantId, ""),
+ loginResponse.GetGroup(),
+ loginResponse.debuglevel == 1
+ ? Permissions.All :
+ (Permissions)loginResponse.debuglevel) ;
+ }
+
+ /// Load bike object from booking response.
+ /// Bike object to load from response.
+ /// Booking response.
+ /// Mail address of user which books bike.
+ /// Session cookie of user which books bike.
+ /// Controls whether notify property changed events are fired or not.
+ public static void Load(
+ this IBikeInfoMutable bike,
+ BikeInfoReservedOrBooked bikeInfo,
+ string mailAddress,
+ Func dateTimeProvider,
+ Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All)
+ {
+
+ var l_oDateTimeProvider = dateTimeProvider != null
+ ? dateTimeProvider
+ : () => DateTime.Now;
+
+ if (bike is Bike.BluetoothLock.BikeInfoMutable btBikeInfo)
+ {
+ btBikeInfo.LockInfo.Load(
+ bikeInfo.GetBluetoothLockId(),
+ bikeInfo.GetBluetoothLockGuid(),
+ bikeInfo.GetSeed(),
+ bikeInfo.GetUserKey(),
+ bikeInfo.GetAdminKey());
+ }
+
+ var l_oState = bikeInfo.GetState();
+ switch (l_oState)
+ {
+ case InUseStateEnum.Disposable:
+ bike.State.Load(
+ InUseStateEnum.Disposable,
+ notifyLevel: notifyLevel);
+ break;
+
+ case InUseStateEnum.Reserved:
+ bike.State.Load(
+ InUseStateEnum.Reserved,
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.timeCode,
+ notifyLevel);
+ break;
+
+ case InUseStateEnum.Booked:
+ bike.State.Load(
+ InUseStateEnum.Booked,
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.timeCode,
+ notifyLevel);
+ break;
+
+ default:
+ throw new Exception(string.Format("Unexpected bike state detected. state is {0}.", l_oState));
+ }
+ }
+
+ /// Gets bikes available from copri server response.
+ /// Response to create collection from.
+ /// New collection of available bikes.
+ public static BikeCollection GetBikesAvailable(
+ this BikesAvailableResponse p_oBikesAvailableResponse)
+ {
+ return GetBikesAll(
+ p_oBikesAvailableResponse,
+ new BikesReservedOccupiedResponse(), // There are no occupied bikes.
+ string.Empty,
+ () => DateTime.Now);
+ }
+
+ /// Gets bikes occupied from copri server response.
+ /// Response to create bikes from.
+ /// New collection of occupied bikes.
+ public static BikeCollection GetBikesOccupied(
+ this BikesReservedOccupiedResponse p_oBikesOccupiedResponse,
+ string p_strMail,
+ Func p_oDateTimeProvider)
+ {
+ return GetBikesAll(
+ new BikesAvailableResponse(),
+ p_oBikesOccupiedResponse,
+ p_strMail,
+ p_oDateTimeProvider);
+ }
+
+ /// Gets bikes occupied from copri server response.
+ /// Response to create bikes from.
+ /// New collection of occupied bikes.
+ public static BikeCollection GetBikesAll(
+ BikesAvailableResponse p_oBikesAvailableResponse,
+ BikesReservedOccupiedResponse p_oBikesOccupiedResponse,
+ string p_strMail,
+ Func p_oDateTimeProvider)
+ {
+ var l_oBikesDictionary = new Dictionary();
+ var l_oDuplicates = new Dictionary();
+
+ // Get bikes from Copri/ file/ memory, ....
+ if (p_oBikesAvailableResponse != null
+ && p_oBikesAvailableResponse.bikes != null)
+ {
+ foreach (var bikeInfoResponse in p_oBikesAvailableResponse.bikes.Values)
+ {
+ var l_oBikeInfo = BikeInfoFactory.Create(bikeInfoResponse);
+ if (l_oBikeInfo == null)
+ {
+ // Response is not valid.
+ continue;
+ }
+
+ if (l_oBikesDictionary.ContainsKey(l_oBikeInfo.Id))
+ {
+ // Duplicates are not allowed.
+ Log.Error($"Duplicate bike with id {l_oBikeInfo.Id} detected evaluating bikes available. Bike status is {l_oBikeInfo.State.Value}.");
+
+ if (!l_oDuplicates.ContainsKey(l_oBikeInfo.Id))
+ {
+ l_oDuplicates.Add(l_oBikeInfo.Id, l_oBikeInfo);
+ }
+
+ continue;
+ }
+
+ l_oBikesDictionary.Add(l_oBikeInfo.Id, l_oBikeInfo);
+ }
+ }
+
+ // Get bikes from Copri/ file/ memory, ....
+ if (p_oBikesOccupiedResponse != null
+ && p_oBikesOccupiedResponse.bikes_occupied != null)
+ {
+ foreach (var l_oBikeInfoResponse in p_oBikesOccupiedResponse.bikes_occupied.Values)
+ {
+ BikeInfo l_oBikeInfo = BikeInfoFactory.Create(
+ l_oBikeInfoResponse,
+ p_strMail,
+ p_oDateTimeProvider);
+
+ if (l_oBikeInfo == null)
+ {
+ continue;
+ }
+
+ if (l_oBikesDictionary.ContainsKey(l_oBikeInfo.Id))
+ {
+ // Duplicates are not allowed.
+ Log.Error($"Duplicate bike with id {l_oBikeInfo.Id} detected evaluating bikes occupied. Bike status is {l_oBikeInfo.State.Value}.");
+ if (!l_oDuplicates.ContainsKey(l_oBikeInfo.Id))
+ {
+ l_oDuplicates.Add(l_oBikeInfo.Id, l_oBikeInfo);
+ }
+
+ continue;
+ }
+
+
+ l_oBikesDictionary.Add(l_oBikeInfo.Id, l_oBikeInfo);
+ }
+ }
+
+ // Remove entries which are not unique.
+ foreach (var l_oDuplicate in l_oDuplicates)
+ {
+ l_oBikesDictionary.Remove(l_oDuplicate.Key);
+ }
+
+ return new BikeCollection(l_oBikesDictionary);
+ }
+ }
+
+
+ ///
+ /// Constructs bike info instances/ bike info derived instances.
+ ///
+ public static class BikeInfoFactory
+ {
+ public static BikeInfo Create(BikeInfoAvailable bikeInfo)
+ {
+ if (bikeInfo.GetIsManualLockBike())
+ {
+ // Manual lock bikes are no more supported.
+ Log.Error(
+ $"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
+ "Manual lock bikes are no more supported." +
+ $"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $"station number {bikeInfo.station}" : string.Empty)}."
+ );
+ return null;
+ }
+
+ switch (bikeInfo.GetState())
+ {
+ case InUseStateEnum.Disposable:
+ break;
+
+ default:
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
+ return null;
+ }
+
+ if (bikeInfo.station == null)
+ {
+ // Bike available must always have a station id because bikes can only be returned at a station.
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. No station info set.");
+ return null;
+ }
+
+ try
+ {
+ return !bikeInfo.GetIsBluetoothLockBike()
+ ? new BikeInfo(
+ bikeInfo.bike,
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription) null),
+#endif
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike(),
+ bikeInfo.description)
+ : new Bike.BluetoothLock.BikeInfo(
+ bikeInfo.bike,
+ bikeInfo.GetBluetoothLockId(),
+ bikeInfo.GetBluetoothLockGuid(),
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription)null),
+#endif
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike(),
+ bikeInfo.description);
+
+ }
+ catch (ArgumentException ex)
+ {
+ // Contructor reported invalid arguemts (missing lock id, ....).
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Invalid response detected. Available bike with id {bikeInfo.bike} skipped. {ex.Message}");
+ return null;
+ }
+ }
+
+ /// Creates a bike info object from copri response.
+ /// Copri response.
+ /// Mail address of user.
+ /// Date and time provider function.
+ ///
+ public static BikeInfo Create(
+ BikeInfoReservedOrBooked bikeInfo,
+ string mailAddress,
+ Func dateTimeProvider)
+ {
+ if (bikeInfo.GetIsManualLockBike())
+ {
+ // Manual lock bikes are no more supported.
+ Log.Error(
+ $"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
+ "Manual lock bikes are no more supported." +
+ $"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $", station number {bikeInfo.station}" : string.Empty)}."
+ );
+ return null;
+ }
+
+ // Check if bike is a bluetooth lock bike.
+ var isBluetoothBike = bikeInfo.GetIsBluetoothLockBike();
+ int lockSerial = bikeInfo.GetBluetoothLockId();
+ Guid lockGuid = bikeInfo.GetBluetoothLockGuid();
+
+ switch (bikeInfo.GetState())
+ {
+ case InUseStateEnum.Reserved:
+ try
+ {
+ return !isBluetoothBike
+ ? new BikeInfo(
+ bikeInfo.bike,
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike(),
+ bikeInfo.description,
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription)null),
+#endif
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.timeCode,
+ dateTimeProvider)
+ : new Bike.BluetoothLock.BikeInfo(
+ bikeInfo.bike,
+ lockSerial,
+ lockGuid,
+ bikeInfo.GetUserKey(),
+ bikeInfo.GetAdminKey(),
+ bikeInfo.GetSeed(),
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription)null),
+#endif
+ dateTimeProvider,
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike());
+ }
+ catch (ArgumentException ex)
+ {
+ // Contructor reported invalid arguemts (missing lock id, ....).
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Reserved bike with id {bikeInfo.bike} skipped. {ex.Message}");
+ return null;
+ }
+
+ case InUseStateEnum.Booked:
+ try
+ {
+ return !isBluetoothBike
+ ? new BikeInfo(
+ bikeInfo.bike,
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike(),
+ bikeInfo.description,
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription)null),
+#endif
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.timeCode)
+ : new Bike.BluetoothLock.BikeInfo(
+ bikeInfo.bike,
+ lockSerial,
+ bikeInfo.GetBluetoothLockGuid(),
+ bikeInfo.GetUserKey(),
+ bikeInfo.GetAdminKey(),
+ bikeInfo.GetSeed(),
+ bikeInfo.GetFrom(),
+ mailAddress,
+ bikeInfo.station,
+ bikeInfo.GetOperatorUri(),
+#if !NOTARIFFDESCRIPTION
+ Create(bikeInfo.tariff_description),
+#else
+ Create((TINK.Repository.Response.TariffDescription)null),
+#endif
+ bikeInfo.GetIsDemo(),
+ bikeInfo.GetGroup(),
+ bikeInfo.GetWheelType(),
+ bikeInfo.GetTypeOfBike());
+ }
+ catch (ArgumentException ex)
+ {
+ // Contructor reported invalid arguemts (missing lock id, ....).
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Booked bike with id {bikeInfo.bike} skipped. {ex.Message}");
+ return null;
+ }
+
+ default:
+ Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
+ return null;
+ }
+ }
+
+ public static Bikes.Bike.TariffDescription Create(this TINK.Repository.Response.TariffDescription tariffDesciption)
+ {
+ return new Bikes.Bike.TariffDescription
+ {
+ Name = tariffDesciption?.name,
+ Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null,
+ FreeTimePerSession = double.TryParse(tariffDesciption?.free_hours, NumberStyles.Any, CultureInfo.InvariantCulture, out double freeHours) ? TimeSpan.FromHours(freeHours) : TimeSpan.Zero,
+ FeeEuroPerHour = double.TryParse(tariffDesciption?.eur_per_hour, NumberStyles.Any, CultureInfo.InvariantCulture, out double euroPerHour) ? euroPerHour : double.NaN,
+ AboEuroPerMonth = double.TryParse(tariffDesciption?.abo_eur_per_month, NumberStyles.Any, CultureInfo.InvariantCulture, out double aboEuroPerMonth) ? aboEuroPerMonth : double.NaN,
+ MaxFeeEuroPerDay = double.TryParse(tariffDesciption?.max_eur_per_day, NumberStyles.Any, CultureInfo.InvariantCulture, out double maxEuroPerDay) ? maxEuroPerDay : double.NaN,
+ };
+ }
+ }
+}
diff --git a/TINKLib/Model/Device/IAppInfo.cs b/TINKLib/Model/Device/IAppInfo.cs
new file mode 100644
index 0000000..77b0759
--- /dev/null
+++ b/TINKLib/Model/Device/IAppInfo.cs
@@ -0,0 +1,14 @@
+using System;
+namespace TINK.Model.Device
+{
+ /// Interface to get version info.
+ public interface IAppInfo
+ {
+ /// Gets the app version to display to user.
+ Version Version { get; }
+
+ /// Gets the URL to the app store.
+ /// The store URL.
+ string StoreUrl { get; }
+ }
+}
diff --git a/TINKLib/Model/Device/IDevice.cs b/TINKLib/Model/Device/IDevice.cs
new file mode 100644
index 0000000..787dce5
--- /dev/null
+++ b/TINKLib/Model/Device/IDevice.cs
@@ -0,0 +1,9 @@
+namespace TINK.Model.Device
+{
+ public interface IDevice
+ {
+ /// Gets unitque device identifier.
+ /// Gets the identifies specifying device.
+ string GetIdentifier();
+ }
+}
diff --git a/TINKLib/Model/Device/IExternalBrowserService.cs b/TINKLib/Model/Device/IExternalBrowserService.cs
new file mode 100644
index 0000000..4a7a9ae
--- /dev/null
+++ b/TINKLib/Model/Device/IExternalBrowserService.cs
@@ -0,0 +1,9 @@
+namespace TINK.Model.Device
+{
+ public interface IExternalBrowserService
+ {
+ /// Opens an external browser.
+ /// Url to open.
+ void OpenUrl(string url);
+ }
+}
diff --git a/TINKLib/Model/Device/IGeolodationDependent.cs b/TINKLib/Model/Device/IGeolodationDependent.cs
new file mode 100644
index 0000000..736b437
--- /dev/null
+++ b/TINKLib/Model/Device/IGeolodationDependent.cs
@@ -0,0 +1,7 @@
+namespace TINK.Model.Device
+{
+ public interface IGeolodationDependent
+ {
+ bool IsGeolcationEnabled { get; }
+ }
+}
diff --git a/TINKLib/Model/Device/ISpecialFolder.cs b/TINKLib/Model/Device/ISpecialFolder.cs
new file mode 100644
index 0000000..ed6ac85
--- /dev/null
+++ b/TINKLib/Model/Device/ISpecialFolder.cs
@@ -0,0 +1,15 @@
+namespace TINK.Model.Device
+{
+ public interface ISpecialFolder
+ {
+ ///
+ /// Get the folder name of external folder to write to.
+ ///
+ /// External directory.
+ string GetExternalFilesDir();
+
+ /// Gets the folder name of the personal data folder dir on internal storage.
+ /// Directory name.
+ string GetInternalPersonalDir();
+ }
+}
diff --git a/TINKLib/Model/Device/IWebView.cs b/TINKLib/Model/Device/IWebView.cs
new file mode 100644
index 0000000..bda2202
--- /dev/null
+++ b/TINKLib/Model/Device/IWebView.cs
@@ -0,0 +1,8 @@
+namespace TINK.Model.Device
+{
+ public interface IWebView
+ {
+ /// Clears the cookie cache for all web views.
+ void ClearCookies();
+ }
+}
diff --git a/TINKLib/Model/FileOperationException.cs b/TINKLib/Model/FileOperationException.cs
new file mode 100644
index 0000000..909c8ac
--- /dev/null
+++ b/TINKLib/Model/FileOperationException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace TINK.Model
+{
+ /// Operation fired when a file operation fails.
+ public class FileOperationException : Exception
+ {
+ public FileOperationException(string message, Exception innerException) : base(message, innerException)
+ { }
+ }
+}
diff --git a/TINKLib/Model/FilterCollectionStore.cs b/TINKLib/Model/FilterCollectionStore.cs
new file mode 100644
index 0000000..ff9bca0
--- /dev/null
+++ b/TINKLib/Model/FilterCollectionStore.cs
@@ -0,0 +1,14 @@
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TINK.Model
+{
+ public static class FilterCollectionStore
+ {
+ /// Writes filter collection state to string.
+ /// Filter collection to write to string.
+ /// P o dictionary.
+ public static string ToString(this IDictionary p_oDictionary) => "{" + string.Join(", ", p_oDictionary.Select(x => "(" + x.Key + "= " + x.Value + ")")) + "}";
+ }
+}
diff --git a/TINKLib/Model/GroupFilterHelper.cs b/TINKLib/Model/GroupFilterHelper.cs
new file mode 100644
index 0000000..665ac59
--- /dev/null
+++ b/TINKLib/Model/GroupFilterHelper.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using TINK.Model.Connector;
+using TINK.ViewModel.Map;
+using TINK.ViewModel.Settings;
+
+namespace TINK.Model
+{
+ /// Holds collecion of filters to filter options (TINK, Konrad, ....).
+ /// Former name: FilterCollection.
+ public static class GroupFilterHelper
+ {
+ /// Gets default filter set.
+ public static IGroupFilterSettings GetSettingsFilterDefaults
+ {
+ get
+ {
+ return new GroupFilterSettings(new Dictionary {
+ { FilterHelper.FILTERTINKGENERAL, FilterState.On },
+ {FilterHelper.FILTERKONRAD, FilterState.On }
+ });
+ }
+ }
+
+ /// Gets default filter set.
+ public static IGroupFilterMapPage GetMapPageFilterDefaults
+ {
+ get
+ {
+ return new ViewModel.Map.GroupFilterMapPage(new Dictionary {
+ { FilterHelper.FILTERTINKGENERAL, FilterState.On },
+ {FilterHelper.FILTERKONRAD, FilterState.Off }
+ });
+ }
+ }
+ }
+
+ /// Holds value whether filter (on TINK, Konrad, ....) is on or off.
+ public enum FilterState
+ {
+ /// Option (TINK, Konrad, ....) is available.
+ On,
+ /// Option is off
+ Off,
+ }
+}
diff --git a/TINKLib/Model/ITinkApp.cs b/TINKLib/Model/ITinkApp.cs
new file mode 100644
index 0000000..67ba1dc
--- /dev/null
+++ b/TINKLib/Model/ITinkApp.cs
@@ -0,0 +1,98 @@
+using Plugin.Permissions.Abstractions;
+using Serilog.Events;
+using System;
+using System.Threading;
+using TINK.Model.Connector;
+using TINK.Model.Device;
+using TINK.Services.BluetoothLock;
+using TINK.Model.Services.CopriApi.ServerUris;
+using TINK.Model.Services.Geolocation;
+using TINK.Settings;
+using TINK.ViewModel.Map;
+using TINK.ViewModel.Settings;
+using TINK.Services;
+
+namespace TINK.Model
+{
+ public interface ITinkApp
+ {
+ /// Update connector from depending on whether user is logged in or not.
+ void UpdateConnector();
+
+ /// Saves object to file.
+ void Save();
+
+ /// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed.
+ IGroupFilterMapPage GroupFilterMapPage { get; set; }
+
+ /// Holds the user of the app.
+ User.User ActiveUser { get; }
+
+ /// Sets flag whats new page was already shown to true.
+ void SetWhatsNewWasShown();
+
+ /// Holds the system to copri.
+ IFilteredConnector GetConnector(bool isConnected);
+
+ /// Name of the station which is selected.
+ int? SelectedStation { get; set; }
+
+ /// Polling periode.
+ PollingParameters Polling { get; set; }
+
+ TimeSpan ExpiresAfter { get; set; }
+
+ /// Holds status about whants new page.
+ WhatsNew WhatsNew { get; }
+
+ /// Gets whether device is connected to internet or not.
+ bool GetIsConnected();
+
+ /// Action to post to GUI thread.
+ Action PostAction { get; }
+
+ /// Holds the uri which is applied after restart.
+ Uri NextActiveUri { get; set; }
+
+ /// Holds the filters loaded from settings.
+ IGroupFilterSettings FilterGroupSetting { get; set; }
+
+ /// Value indicating whether map is centerted to current position or not.
+ bool CenterMapToCurrentLocation { get; set; }
+
+ bool LogToExternalFolder { get; set; }
+
+ bool IsSiteCachingOn { get; set; }
+
+ /// Gets the minimum logging level.
+ LogEventLevel MinimumLogEventLevel { get; set; }
+
+ /// Updates logging level.
+ /// New level to set.
+ void UpdateLoggingLevel(LogEventLevel p_oNewLevel);
+
+ /// Holds uris of copri servers.
+ CopriServerUriList Uris { get; }
+
+ /// Holds the different lock service implementations.
+ LocksServicesContainerMutable LocksServices { get; }
+
+ /// Holds the different geo location service implementations.
+ ServicesContainerMutable GeolocationServices { get; }
+
+ /// Holds available app themes.
+ ServicesContainerMutable