using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MDP.SID.Scripting.Grpc; using MDP.SID.Scripting.Shared.Http; using MDP.SID.Shared.Public.Events; using MDP.SID.Shared.Public.Readers; using Event = MDP.SID.Scripting.Grpc.Event; public class Tenant { public string Name { get; set; } public int Id { get; set; } public int CurrentOccupancy { get; set; } public int MaxOccupancy { get; set; } } internal class ScriptConfiguration { public int AccessLevelId { get; set; } public int[] EntryReaderIds { get; set; } public int[] ExitReaderIds { get; set; } public int RefreshInterval { get; set; } } public class Update { public int NewOccupancyValue { get; set; } public int MaxOccupancyValue { get; set; } } public class AccessLevel { public int Id { get; set; } public string Name { get; set; } public bool IsEntry { get; set; } } private readonly SemaphoreSlim _syncLock = new(1, 1); Context.OnEventReceived += OnEventReceived; Configuration = await ReadConfiguration(); // setup script HTTP API endpoints SetupHttpEndpoints(); // run script till it receives stop signal CancellationToken.WaitHandle.WaitOne(); private async Task ReadConfiguration() { if (!await Database.ContainsAsync("Configuration")) { await Database.InsertAsync("Configuration", new ScriptConfiguration() { AccessLevelId = 3 }); } return await Database.GetAsync("Configuration"); } private Task CreateCompanyFilterAsync(int companyId) { return Context.CreateFilterAsync(new Filter() { Name = Guid.NewGuid().ToString(), Companies = { companyId } }); } public ScriptConfiguration Configuration { get; set; } private void SetupHttpEndpoints() { Http.MapPost("/sync", async _ => { await SyncTenantsAsync(); }); Http.MapGet("/tenants", async context => { // avoid retrieving during sync await _syncLock.WaitAsync(); try { var tenants = await Database.GetAllAsync(); return tenants; } finally { _syncLock.Release(); } }); Http.MapGet("/ping", async _ => "pong"); Http.MapPost("/reset", async context => { await Database.ClearAsync(); var scriptConfiguration = new ScriptConfiguration() { AccessLevelId = 0, EntryReaderIds = null, ExitReaderIds = null }; await Database.UpdateAsync("Configuration", scriptConfiguration); Configuration = scriptConfiguration; await SyncTenantsAsync(); }); Http.MapGet("/access-levels", async context => { var accessLevels = await Context.GetAccessLevelsAsync(); return accessLevels.Where(e => !e.BuiltIn).Select(e => new AccessLevel() { Id = e.Id, Name = $"{e.Name} ({e.LocationName})" }); }); Http.MapGet("/readers", async context => { var doorReaders = await Context.GetDoorReadersAsync(); return doorReaders .Where(e => e.DeviceType == (int)DoorDeviceType.EntryReader || e.DeviceType == (int)DoorDeviceType.ExitReader).Select(e => new AccessLevel { Id = e.Id, Name = $"{e.DoorName}", IsEntry = e.DeviceType == (int)DoorDeviceType.EntryReader }); }); Http.MapPut("/settings", async context => { var settings = await context.GetBodyAsync(); Configuration = settings; await Database.UpdateAsync("Configuration", settings); }); Http.MapGet("/settings", async _ => Configuration); Http.MapPut("/tenants/{id}/set-occupancy", async context => { var id = context.Get("id"); var value = await context.GetBodyAsync(); var tenant = await Database.GetAsync(int.Parse(id)); if (value == null) { context.FailResponse(); return; } await EvaluateOccupancyAsync(tenant, value.NewOccupancyValue, value.MaxOccupancyValue, true); await Database.UpdateAsync(tenant.Id, tenant); }); Http.Run(); } /// /// Gets called when system receives event /// /// private async void OnEventReceived(Event received) { Guid typeUid = Guid.Parse(received.TypeUid); if (Configuration.EntryReaderIds == null || Configuration.ExitReaderIds == null) { return; } if (typeUid == EventTypes.AccessGranted && received.UserId.HasValue) { var user = await Context.GetUserAsync(received.UserId.Value); if (!user.CompanyId.HasValue || !received.ReaderId.HasValue || !Configuration.EntryReaderIds.Contains(received.ReaderId.Value) && !Configuration.ExitReaderIds.Contains(received.ReaderId.Value)) { // event without user or reader return; } var isEntry = Configuration.EntryReaderIds.Contains(received.ReaderId.Value); var tenant = await Database.GetAsync(user.CompanyId.Value); await EvaluateOccupancyAsync(tenant, tenant.CurrentOccupancy + (isEntry ? 1 : -1), tenant.MaxOccupancy); } } /// /// Validate tenant state and act based on occupancy values /// NOTE: this function is optimized and will trigger al addition/removal only when upper/lower limit is reached unless /// explicitly set to force update /// /// /// /// /// Force update relevant user access levels private async Task EvaluateOccupancyAsync(Tenant tenant, int newOccupancyValue, int newMaxOccupancyValue, bool forceAccessLevelUpdate = false) { newOccupancyValue = Math.Clamp(newOccupancyValue, 0, int.MaxValue); if (newOccupancyValue >= newMaxOccupancyValue) { await AddRemoveOccupancyAccessLevelAsync(tenant.Id, true); } else if (newOccupancyValue < newMaxOccupancyValue && (tenant.CurrentOccupancy >= newMaxOccupancyValue || forceAccessLevelUpdate)) { await AddRemoveOccupancyAccessLevelAsync(tenant.Id, false); } // normalize tenant.CurrentOccupancy = Math.Min(tenant.MaxOccupancy, newOccupancyValue); tenant.MaxOccupancy = newMaxOccupancyValue; await Database.UpdateAsync(tenant.Id, tenant); } private async Task AddRemoveOccupancyAccessLevelAsync(int tenantId, bool remove) { int filterId = await CreateCompanyFilterAsync(tenantId); if (filterId <= 0) { await Context.LogErrorAsync("Invalid filter id"); return; } if (tenantId <= 0) { await Context.LogErrorAsync("Invalid tenant id"); return; } try { IEnumerable als = await Context.GetAccessLevelsAsync(); List toUpdate = als.Where(e => e.Id != Configuration.AccessLevelId && !e.BuiltIn).Select( e => new EntityUpdate() { Id = e.Id, Remove = false, Update = false }).ToList(); toUpdate.Add(new EntityUpdate { Id = Configuration.AccessLevelId, Remove = remove, Update = true } ); var bulkUpdateUserRequest = new BulkUpdateUser(); bulkUpdateUserRequest.AccessLevels.AddRange(toUpdate); await Context.BulkUpdateUsersAsync(0, 1000, Array.Empty(), bulkUpdateUserRequest, filterId); } finally { await Context.DeleteFilterAsync(filterId); } } /// /// Synchronize tenants/companies with script storage and return up to date list /// /// private async Task SyncTenantsAsync() { // this request can't happen twice at the same time as there might state where database await _syncLock.WaitAsync(); try { var existing = (await Database.GetAllAsync()).ToDictionary(e => e.Id); List companies = await Context.GetCompaniesAsync(); foreach (Company company in companies) { if (!existing.TryGetValue(company.Id, out var existingCompany)) { var tenant = new Tenant { Id = company.Id, Name = company.Name }; await Database.InsertAsync(tenant.Id, tenant); } else { var tenant = new Tenant { Name = company.Name, Id = company.Id, CurrentOccupancy = existingCompany.CurrentOccupancy, MaxOccupancy = existingCompany.MaxOccupancy }; await Database.UpdateAsync(tenant.Id, tenant); existing.Remove(company.Id); } foreach (int key in existing.Keys) { await Database.RemoveAsync(key); } } } finally { _syncLock.Release(); } }