scripts/parking-occupancy-managment/script/script.cs

344 lines
8.1 KiB
C#

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<ScriptConfiguration> ReadConfiguration()
{
if (!await Database.ContainsAsync<ScriptConfiguration>("Configuration"))
{
await Database.InsertAsync<ScriptConfiguration>("Configuration", new ScriptConfiguration()
{
AccessLevelId = 3
});
}
return await Database.GetAsync<ScriptConfiguration>("Configuration");
}
private Task<int> 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<Tenant>();
return tenants;
}
finally
{
_syncLock.Release();
}
});
Http.MapGet("/ping", async _ => "pong");
Http.MapPost("/reset", async context =>
{
await Database.ClearAsync<Tenant>();
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<ScriptConfiguration>();
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<Update>();
var tenant = await Database.GetAsync<Tenant>(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();
}
/// <summary>
/// Gets called when system receives event
/// </summary>
/// <param name="received"></param>
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<Tenant>(user.CompanyId.Value);
await EvaluateOccupancyAsync(tenant, tenant.CurrentOccupancy + (isEntry ? 1 : -1),
tenant.MaxOccupancy);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="tenant"></param>
/// <param name="newOccupancyValue"></param>
/// <param name="newMaxOccupancyValue"></param>
/// <param name="forceAccessLevelUpdate">Force update relevant user access levels</param>
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<MDP.SID.Scripting.Grpc.AccessLevel> als = await Context.GetAccessLevelsAsync();
List<EntityUpdate> 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<int>(), bulkUpdateUserRequest, filterId);
}
finally
{
await Context.DeleteFilterAsync(filterId);
}
}
/// <summary>
/// Synchronize tenants/companies with script storage and return up to date list
/// </summary>
/// <returns></returns>
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<Tenant>()).ToDictionary(e => e.Id);
List<Company> 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<Tenant>(key);
}
}
}
finally
{
_syncLock.Release();
}
}