using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Serilog; using TINK.Model; using TINK.Model.Bikes.BikeInfoNS.CopriLock; using TINK.Model.Connector; using TINK.Model.Connector.Updater; using TINK.Model.Device; using TINK.Model.Services.CopriApi; using TINK.Repository; using TINK.Repository.Response; using TINK.Services.CopriApi.Exception; namespace TINK.Services.CopriApi { public static class Polling { /// Timeout for open/ close operations. private const int OPEN_CLOSE_TIMEOUT_MS = 50000; /// Opens lock. /// Instance to communicate with backend. /// Bike object holding id of bike to open. Lock state of object is updated after open request. public static async Task OpenAync( this ICopriServerBase copriServer, IBikeInfoMutable bike) { if (!(copriServer is ICachedCopriServer cachedServer)) throw new ArgumentNullException(nameof(copriServer)); await cachedServer.OpenAync(bike); } /// Opens lock. /// Instance to communicate with backend. /// Bike object holding id of bike to open. Lock state of object is updated after open request. public static async Task OpenAync( this ICachedCopriServer cachedServer, IBikeInfoMutable bike) { // Send command to close lock await cachedServer.UpdateLockingStateAsync( bike.Id, Repository.Request.lock_state.unlocking, bike.OperatorUri); var lockingState = await cachedServer.GetLockStateAsync(bike.Id); var watch = new Stopwatch(); watch.Start(); while (lockingState != LockingState.Open && lockingState != LockingState.UnknownDisconnected && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { // Delay a litte to reduce load on backend. await Task.Delay(3000); lockingState = await cachedServer.GetLockStateAsync(bike.Id); Log.Information($"Current lock state is {lockingState}."); } // Update locking state. bike.LockInfo.State = lockingState; } /// /// Books a bike and opens the lock. /// /// Instance to communicate with backend. /// Bike to book and open. /// Mail address of user which books bike. public static async Task BookAndOpenAync( this ICopriServerBase corpiServer, IBikeInfoMutable bike, string mailAddress) { if (bike == null) { throw new ArgumentNullException(nameof(bike), "Can not book bike and open lock. No bike object available."); } if (!(corpiServer is ICachedCopriServer cachedServer)) throw new ArgumentNullException(nameof(corpiServer)); // Send command to open lock var response = bike.State.Value == Model.State.InUseStateEnum.Disposable ? (await corpiServer.BookAvailableAndStartOpeningAsync(bike.Id, bike.OperatorUri)).GetIsBookingResponseOk(bike.Id) : (await corpiServer.BookReservedAndStartOpeningAsync(bike.Id, bike.OperatorUri)).GetIsBookingResponseOk(bike.Id); // Upated locking state. var lockingState = await cachedServer.GetOccupiedBikeLockStateAsync(bike.Id); var watch = new Stopwatch(); watch.Start(); while (lockingState.HasValue /* if null bike is no more occupied*/ && lockingState.Value != LockingState.Open && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { // Delay a litte to reduce load on backend. await Task.Delay(3000); lockingState = await cachedServer.GetOccupiedBikeLockStateAsync(bike.Id); Log.Debug($"Current lock state of bike {bike.Id} is {(lockingState.HasValue ? lockingState.Value.ToString() : "-")}."); } // Check if bike is still occupied. if (lockingState == null) { // User did not take bike out of the station throw new BikeStillInStationException("Booking was cancelled because bike is still in station."); } // Upate booking state. bike.Load( response, mailAddress, Model.Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None); // Update locking state. bike.LockInfo.State = lockingState.Value; } /// /// Returns a bike and closes the lock. /// /// Instance to communicate with backend. /// Bike to close. public static async Task CloseAync( this ICopriServerBase corpiServer, IBikeInfoMutable bike) { if (!(corpiServer is ICachedCopriServer cachedServer)) throw new ArgumentNullException(nameof(corpiServer)); // Send command to close lock await corpiServer.UpdateLockingStateAsync( bike.Id, Repository.Request.lock_state.locking, bike.OperatorUri); var lockingState = await cachedServer.GetLockStateAsync(bike.Id); var watch = new Stopwatch(); watch.Start(); while (lockingState != LockingState.Closed && lockingState != LockingState.UnknownDisconnected && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { // Delay a litte to reduce load on backend. await Task.Delay(3000); lockingState = await cachedServer.GetLockStateAsync(bike.Id); Log.Information($"Current lock state is {lockingState}."); } // Update locking state. bike.LockInfo.State = lockingState; } /// /// Returns a bike and closes the lock. /// /// Instance to communicate with backend. /// Smart device on which app runs on. /// Mail address of user which books bike. public static async Task ReturnAndCloseAync( this ICopriServerBase corpiServer, ISmartDevice smartDevice, IBikeInfoMutable bike) { if (!(corpiServer is ICachedCopriServer cachedServer)) throw new ArgumentNullException(nameof(corpiServer)); // Send command to open lock DoReturnResponse response = await corpiServer.ReturnAndStartClosingAsync(bike.Id, bike.OperatorUri); // Upate booking state bike.Load(Model.Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None); var lockingState = await cachedServer.GetLockStateAsync(bike.Id); var watch = new Stopwatch(); watch.Start(); while (lockingState != LockingState.Closed && lockingState != LockingState.UnknownDisconnected && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { // Delay a litte to reduce load on backend. await Task.Delay(3000); lockingState = await cachedServer.GetLockStateAsync(bike.Id); Log.Information($"Current lock state is {lockingState}."); } // Update locking state. bike.LockInfo.State = lockingState; return response?.Create() ?? new BookingFinishedModel(); } /// /// Queries the locking state from copri. /// /// Service to use. /// Bike id to query lock state for. /// Locking state private static async Task GetLockStateAsync( this ICachedCopriServer corpiServer, string bikeId) { // Querry reserved or booked bikes first for performance reasons. var bikeReservedOrBooked = (await corpiServer.GetBikesOccupied(false))?.Response.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); if (bikeReservedOrBooked != null) { return bikeReservedOrBooked.GetCopriLockingState(); } var bikeAvailable = (await corpiServer.GetBikesAvailable(false))?.Response.bikes?.Values?.FirstOrDefault(x => x.bike == bikeId); if (bikeAvailable != null) { return bikeAvailable.GetCopriLockingState(); } return LockingState.UnknownDisconnected; } /// /// Queries the locking state of a occupied bike from copri. /// /// Service to use. /// Bike id to query lock state for. /// Locking state if bike is still occupied, null otherwise. private static async Task GetOccupiedBikeLockStateAsync( this ICachedCopriServer corpiServer, string bikeId) { var bikeReservedOrBooked = (await corpiServer.GetBikesOccupied(false))?.Response.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); if (bikeReservedOrBooked != null) { return bikeReservedOrBooked.GetCopriLockingState(); } return null; } } }