Update
This commit is contained in:
parent
fce05f7dee
commit
118e66ceca
|
@ -1,311 +1,322 @@
|
|||
public class Tenant
|
||||
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 SemaphoreSlim(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"))
|
||||
{
|
||||
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 SemaphoreSlim(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()
|
||||
{
|
||||
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 }
|
||||
AccessLevelId = 3
|
||||
});
|
||||
}
|
||||
|
||||
public ScriptConfiguration Configuration { get; set; }
|
||||
return await Database.GetAsync<ScriptConfiguration>("Configuration");
|
||||
}
|
||||
|
||||
private void SetupHttpEndpoints()
|
||||
private Task<int> CreateCompanyFilterAsync(int companyId)
|
||||
{
|
||||
return Context.CreateFilterAsync(new Filter()
|
||||
{
|
||||
Http.MapPost("/sync", async _ => { await SyncTenantsAsync(); });
|
||||
Name = Guid.NewGuid().ToString(),
|
||||
Companies = { companyId }
|
||||
});
|
||||
}
|
||||
|
||||
Http.MapGet("/tenants", async context =>
|
||||
{
|
||||
// avoid retrieving during sync
|
||||
await _syncLock.WaitAsync();
|
||||
public ScriptConfiguration Configuration { get; set; }
|
||||
|
||||
try
|
||||
{
|
||||
var tenants = await Database.GetAllAsync<Tenant>();
|
||||
return tenants;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
});
|
||||
|
||||
Http.MapGet("/ping", async _ => "pong");
|
||||
private void SetupHttpEndpoints()
|
||||
{
|
||||
Http.MapPost("/sync", async _ => { await SyncTenantsAsync(); });
|
||||
|
||||
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.Select(e => new AccessLevel()
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name
|
||||
});
|
||||
});
|
||||
|
||||
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)
|
||||
Http.MapGet("/tenants", async context =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
var filterId = await CreateCompanyFilterAsync(tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
await Context.BulkUpdateUsersAsync(0, 1000, Array.Empty<int>(), new BulkUpdateUser
|
||||
{
|
||||
AccessLevels =
|
||||
{
|
||||
new EntityUpdate[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Configuration.AccessLevelId,
|
||||
Remove = remove,
|
||||
Update = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 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
|
||||
// avoid retrieving during sync
|
||||
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);
|
||||
}
|
||||
}
|
||||
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.Select(e => new AccessLevel()
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name
|
||||
});
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
var filterId = await CreateCompanyFilterAsync(tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
await Context.BulkUpdateUsersAsync(0, 1000, Array.Empty<int>(), new BulkUpdateUser
|
||||
{
|
||||
AccessLevels =
|
||||
{
|
||||
new EntityUpdate[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Configuration.AccessLevelId,
|
||||
Remove = remove,
|
||||
Update = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 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();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
# europeum
|
||||
# Europeum
|
||||
|
||||
## Project setup
|
||||
```
|
||||
|
@ -21,4 +21,4 @@ npm run lint
|
|||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
Loading…
Reference in New Issue