Files
adas-core/adas-core.Application/Subscriptions/WsSubscriberGrouped.cs
2026-06-26 10:29:23 +02:00

337 lines
16 KiB
C#

using System.Timers;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using MongoDB.Bson;
using Serilog;
using static adas_core.Domain.Models.GroupedObservation;
using Timer = System.Timers.Timer;
namespace adas_core.Application.Subscriptions;
/**
* This class is used to group the subscribers by the same group
*/
/// <summary>
/// Represents a grouped WebSocket subscriber that aggregates and manages multiple related subscription registrations.
/// </summary>
public class WsSubscriberGrouped
{
private readonly EventHandler<string> _sendEvent;
public WsSubscriberGrouped(ObjectId patientId, string wsId, string? timeZoneId,
GroupedField gf, GroupedObservation lastGroupedObservationObs, EventHandler<string> sendEvent)
{
_sendEvent = sendEvent;
if (WsSubscriber.ToList().FirstOrDefault(s => s == wsId) == null) WsSubscriber.Add(wsId);
TimeZoneId = timeZoneId ?? "Romance Standard Time";
PatientId = patientId;
Names = CollectionsUtils.IfEmptyOrNull(gf.Names, [gf.Name ?? string.Empty]);
Max = gf.Max;
Since = gf.Since;
StartTimeShift = gf.StartTimeShift;
Regularity = gf.Regularity;
Result = gf.Result;
Group.Add(wsId, gf.Group ?? string.Empty);
UpdateLastGo(lastGroupedObservationObs);
HashCode = CryptoAdas.CreateMd5GroupedObs(gf, patientId);
Timer = new Timer
{
Interval = SetUpTimerInterval()
};
Timer.Elapsed += Timer_Elapsed;
if (Regularity != GroupedObservationEnum.Regularity.Times) Timer.Start();
}
public List<string> WsSubscriber { get; set; } = [];
public ObjectId PatientId { get; set; }
public List<string> Names { get; set; }
public int Max { get; set; }
public GroupedObservationEnum.Regularity? Regularity { get; set; }
public List<GroupedObservationEnum.Result> Result { get; set; }
public GroupedObservationEnum.Since Since { get; set; }
public List<string> StartTimeShift { get; set; }
public string HashCode { get; set; }
public string TimeZoneId { get; set; }
public List<GroupedObservationObs> LastGroupedObservationObs { get; set; } = [];
public Dictionary<string, string> Group { get; set; } = new();
public Timer Timer { get; set; }
/// <summary>
/// Refreshes the cached last observations by matching each known name against the supplied grouped observation, falling back to the most recent observation when no match is found.
/// </summary>
/// <param name="lastGroupedObservationObs">The source grouped observation whose observations are searched to populate the last-observation cache.</param>
public void UpdateLastGo(GroupedObservation lastGroupedObservationObs)
{
LastGroupedObservationObs.Clear();
foreach (var name in Names)
{
var lastObs = lastGroupedObservationObs.Observations.LastOrDefault(c => c.Name == name);
if (lastObs != null)
{
LastGroupedObservationObs.Add(lastObs);
}
else
{
lastObs = lastGroupedObservationObs.Observations.LastOrDefault();
if (lastObs != null) LastGroupedObservationObs.Add(lastObs);
}
}
}
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
var currentDateTime = DateTime.Now;
Timer.Stop();
try
{
bool hasCurrentObs;
switch (Regularity)
{
case GroupedObservationEnum.Regularity.Second:
// Filtra observaciones con el mismo segundo actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Second == currentDateTime.Second
&& obs.Time.Minute == currentDateTime.Minute
&& obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
case GroupedObservationEnum.Regularity.Minute:
// Filtra observaciones con el mismo minuto actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Minute == currentDateTime.Minute
&& obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
case GroupedObservationEnum.Regularity.Day:
// Filtra observaciones con la misma fecha actual (día completo)
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Date == currentDateTime.Date);
break;
default:
// Filtra observaciones con la misma hora actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
}
// Si no hay al menos una observación que coincide, invoca el evento
if (!hasCurrentObs) _sendEvent(this, HashCode);
}
catch (Exception ex)
{
Log.Warning(
$"Exception on WsSubsciptor for patient: {PatientId} message: {ex.Message} exception: {ex}");
}
TimerReestart();
}
/// <summary>
/// Restarts the timer by recalculating its interval through <c>SetUpTimerInterval</c> and then starting it.
/// </summary>
private void TimerReestart()
{
Timer.Interval = SetUpTimerInterval();
Timer.Start();
}
/// <summary>
/// Calculates the timer interval in milliseconds until the next scheduled occurrence based on the configured <see cref="Regularity"/>, supporting Second, Minute, Day, and a default (Hour) case, and adds a per-regularity grace time.
/// </summary>
/// <returns>The number of milliseconds to wait until the next interval, including the grace time.</returns>
private int SetUpTimerInterval()
{
var currentDateTime = DateTime.Now;
int graceTime;
DateTime nextIntervalTime;
switch (Regularity)
{
case GroupedObservationEnum.Regularity.Second:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
currentDateTime.Second
).AddSeconds(1);
graceTime = 500;
break;
case GroupedObservationEnum.Regularity.Minute:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
0
).AddMinutes(1);
graceTime = 30000;
break;
case GroupedObservationEnum.Regularity.Day:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
0,
0,
0
).AddDays(1);
graceTime = 60000;
break;
default:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
0,
0
).AddHours(1);
graceTime = 30000;
break;
}
var timeSpanToNextInterval = nextIntervalTime - currentDateTime;
Log.Information(
"Obs {Obs}, currentDateTime: {CurrentDateTime}, nextIntervalTime: {NextIntervalTime}, timeSpanToNextInterval: {TimeSpanToNextInterval}, timeSpanToNextInterval.TotalMilliseconds+graceTime: {Tt}",
Group, currentDateTime, nextIntervalTime, timeSpanToNextInterval,
(int)timeSpanToNextInterval.TotalMilliseconds + graceTime);
return (int)timeSpanToNextInterval.TotalMilliseconds + graceTime;
}
}
/// <summary>
/// Provides static extension methods to enhance WebSocket subscriber functionality.
/// </summary>
public static class WsSubscriberExtension
{
/// <summary>
/// Determines whether the specified <see cref="GroupedField"/> and patient identifier match the current <see cref="WsSubscriberGrouped"/> instance by comparing regularity, since, start time shift, maximum, result sequence, patient identifier, and names (treating a null <paramref name="r"/>.Name as an empty string).
/// </summary>
/// <param name="source">The current <see cref="WsSubscriberGrouped"/> instance being compared.</param>
/// <param name="r">The <see cref="GroupedField"/> to compare against the source.</param>
/// <param name="patientId">The patient identifier to match against the source patient identifier.</param>
/// <returns><c>true</c> if all compared properties are equal; otherwise, <c>false</c>.</returns>
public static bool Compare(this WsSubscriberGrouped source, GroupedField r, ObjectId patientId)
{
return r.Regularity == source.Regularity &&
r.Since == source.Since &&
CompareStringShiftList(r.StartTimeShift, source.StartTimeShift) &&
r.Max == source.Max &&
r.Result.SequenceEqual(source.Result) &&
patientId.ToString() == source.PatientId.ToString() &&
CompareNamesStringList(r.Names, [r.Name ?? string.Empty], source.Names);
}
/// <summary>
/// Compares two string shift lists for equality, returning true when both lists are null or when both contain the same elements in the same order, and false when only one of the lists is null.
/// </summary>
/// <param name="shift">The target shift list to compare.</param>
/// <param name="sourceShift">The source shift list to compare against.</param>
/// <returns><c>true</c> if both lists are null or if their sequence of strings is equal; otherwise, <c>false</c>.</returns>
private static bool CompareStringShiftList(List<string>? shift, List<string>? sourceShift)
{
if (shift != null && sourceShift != null) return sourceShift.SequenceEqual(shift);
if (shift == null && sourceShift == null) return true;
return false;
}
/// <summary>
/// Compares the <paramref name="sourceNames"/> list against two candidate name lists and returns whether either one matches.
/// Falls back to the second list (<paramref name="name"/>) when the first (<paramref name="names"/>) is empty or null, or does not match.
/// </summary>
/// <param name="names">The primary candidate list of names to compare against <paramref name="sourceNames"/>.</param>
/// <param name="name">The fallback candidate list of names to compare against <paramref name="sourceNames"/> when <paramref name="names"/> cannot be used.</param>
/// <param name="sourceNames">The source list of names being compared.</param>
/// <returns><c>true</c> if <paramref name="sourceNames"/> is a sequence match for either <paramref name="names"/> or <paramref name="name"/>; otherwise, <c>false</c>.</returns>
private static bool CompareNamesStringList(List<string> names, List<string> name, List<string> sourceNames)
{
if (!CollectionsUtils.IsEmptyOrNull(names) && sourceNames.SequenceEqual(names)) return true;
return !CollectionsUtils.IsEmptyOrNull(name) && sourceNames.SequenceEqual(name);
}
/// <summary>
/// Check if the incoming observation affect to the group and is new info or is irrelevant
/// </summary>
/// <returns></returns>
public static bool IsNewObservationRelevantForGroup(this WsSubscriberGrouped group, PatientObservation obs)
{
double.TryParse(obs.Value.ToString(), out var pobsValue);
switch (group.Regularity)
{
case GroupedObservationEnum.Regularity.Shift:
case GroupedObservationEnum.Regularity.Hour:
var obsInThisHour = group.LastGroupedObservationObs.Where(c =>
new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, 0, 0) ==
new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0) && !c.IsFilled);
//Obs is in grouped time, don't generate group for observation outside of range
//Change to don't cut is obs time is a future
var obsInTime = DateTime.UtcNow.AddHours(group.Max) > obs.Time &&
DateTime.UtcNow.AddHours(group.Max * -1) < obs.Time;
//future data is relevant because maybe nothing is inserted in the same hour after it and we cant lose it
/* if (DateTime.UtcNow.AddHours(group.Max) > obs.time)
{
}
*/
if (!obsInTime) return false;
var groupedObservationObsEnumerable = obsInThisHour.ToList();
if (!groupedObservationObsEnumerable.Any() && obsInTime) return true;
//If obs in this hour check if is relevant by Result
if (groupedObservationObsEnumerable.Any())
foreach (var result in group.Result)
switch (result)
{
case GroupedObservationEnum.Result.Max:
var value = groupedObservationObsEnumerable.Max(s => s.Max?.Value);
var parsed = double.TryParse(value?.ToString(), out var valueParsed);
//actual value is higher, is relevant
if (parsed && pobsValue > valueParsed) return true;
break;
case GroupedObservationEnum.Result.Min:
var minValue = groupedObservationObsEnumerable.Min(s => s.Min?.Value);
var isParsed = double.TryParse(minValue?.ToString(), out var minValueParsed);
//actual value is higher, is relevant
if (isParsed && pobsValue < minValueParsed) return true;
break;
case GroupedObservationEnum.Result.Last:
var lastObsInTime = groupedObservationObsEnumerable.OrderBy(t => t.Last?.Time).First();
//is the new latest in the hour, relevant
if (lastObsInTime.Time < obs.Time) return true;
break;
case GroupedObservationEnum.Result.LastFilled:
var lastFilledObsInTime = groupedObservationObsEnumerable
.OrderBy(t => t.LastFilled?.Time).First();
//is the new latest in the hour, relevant
if (lastFilledObsInTime.Time < obs.Time) return true;
break;
//always is relevant because SUM everything in the hour
//TODO halfHour should check if any obs already in the halfHour
case GroupedObservationEnum.Result.Sum:
case GroupedObservationEnum.Result.Average:
case GroupedObservationEnum.Result.HalfHour:
return true;
}
return false;
default:
return true;
}
}
}