Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop

This commit is contained in:
jrojas
2026-06-26 09:52:35 +02:00
commit 319fd3dfb0
746 changed files with 106610 additions and 0 deletions
@@ -0,0 +1,641 @@
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Serilog;
namespace adas_core.Application.Customizations.HPAZ;
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IAlarmService> _alarmService = serviceProvider.GetRequiredService<Lazy<IAlarmService>>();
private readonly ApiSettings
_apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>().Value; //apiSettings;
private readonly IConfigObservationService _configObservationService =
serviceProvider.GetRequiredService<IConfigObservationService>(); //configObservationService;
private readonly ILogger<CalculatedObservations> _logger =
serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
//Use GroupedObservation to avoid circular dependency with _observationService
private readonly Lazy<IObservationService> _observationService =
serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
private readonly List<string> _pressBloodArteryMean = ["TAm"];
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
}
/// <summary>
/// Maps the obs to an event or alarm depending on its type and inserts it if needed
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="source">Observation</param>
/// <param name="onlyByName">True if the observation needs to generate an alert</param>
/// <returns>Mapped observation</returns>
public async Task<T?> Map<T>(T source, bool onlyByName) where T : BasePatientObservation
{
_logger.LogDebug("Mapping {obsName} mode observation {obs}", source.Name, source);
if (source is not BasePatientObservationValue obs) return source;
var name = obs is PatientObservationAlarm obsAlarm ? obsAlarm.Event : obs.Name;
name ??= string.Empty;
//No generamos alertas si viene onlyByName
if (name == "Resp_Mode")
obs = CalculateVentilationMode(obs);
if (name is "Sattc" or "FiO2") await CalculateSf(obs, name);
if (name is "FiO2" or "P_VAM" or "PaO2_Tidal") await CalculateOxygenationIndex(obs, name);
if (name is "FiO2" or "PaO2_Tidal") await CalculatePf(obs, name);
if (name == "TEST_ALARMA") await _alarmService.Value.CalculateAlarmTest(obs, name);
obs = await CheckObsWithSameTimeExistsAndIncrementTime(obs);
if (obs is PatientObservation pobs)
{
pobs = await CheckAlarmConfig(pobs);
return pobs as T;
}
return obs as T;
}
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
try
{
if (pumpObservation.Status is PumpEnum.Status.Alarm or PumpEnum.Status.Warning)
{
PatientObservation? patientObservation = null;
string? name = null;
//La oclusión es para todos los tipos
var volumetricAirOcclusionAlarm = _apiSettings.VolumetricAirOcclusionAlarm ?? null;
if (volumetricAirOcclusionAlarm != null && pumpObservation.AlarmType != null &&
volumetricAirOcclusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "VolumetricAirOcclusion";
//alerta oclusión aire para bombas volumétricas
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var listInotropicMedicines = _apiSettings.InotropicMedicines ?? null;
if (listInotropicMedicines is { Count: > 0 } && pumpObservation.DrugName != null &&
listInotropicMedicines.Contains(pumpObservation.DrugName)) //INOTRÓPICOS
{
var inotropicEndInfusionAlarm = _apiSettings.InotropicEndInfusionAlarm ?? null;
if (inotropicEndInfusionAlarm != null && pumpObservation.AlarmType != null &&
inotropicEndInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
//Inotrópicos próxima a fin de infusión
// Y
//proximo fin de infusión, oclusión, y presencia aire….
//Solo inotrópicos
//Está en la línea 19 del excel y abarca la que hay en la línea 9
// TODO: revisar en los logs que nombre de type (Buscar por crud xmlns)
name = "InotropicEndInfusion";
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
}
else
{
var pumPressureAlarm = _apiSettings.PumPressureAlarm ?? null;
if (pumPressureAlarm != null && pumpObservation.AlarmType != null &&
pumPressureAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "Pressure";
//alerta de presión(la alerta de la bomba se programa + -30mmHg por encima de la presión de la línea).
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var endContinuousInfusionAlarm = _apiSettings.EndContinuousInfusionAlarm ?? null;
if (endContinuousInfusionAlarm != null && pumpObservation.AlarmType != null &&
endContinuousInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "EndContinuousInfusion";
//próximo a fin de infusión /fin de infusión
//información de bombas solo perfusión continua
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var volumetricAirInLineAlarm = _apiSettings.VolumetricAirInLineAlarm ?? null;
if (volumetricAirInLineAlarm != null && pumpObservation.AlarmType != null &&
volumetricAirInLineAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "VolumetricAirInLine";
//alerta aire para bombas volumétricas (burbujas de aire para la bomba)
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
}
if (patientObservation != null && name != null)
{
_ = _observationService.Value.InsertObservation(patientObservation);
_ = _alarmService.Value.SendAlarm(patientObservation, name, AlarmEnum.Name.Pump,
AlarmEnum.Severity.None, AlarmEnum.Type.Auto);
}
}
}
catch (Exception ex)
{
_logger.LogError("Error mapping alarm pump observation. Exception: {exMessage}", ex.Message);
}
return pumpObservation;
}
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError(
"Error FixTimeInconsistencyWithLast new observation name is null or empty: {newObservation}.",
newObservation);
return newObservation;
}
var lasObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1,
[newObservation.Name]);
var lastObservation = lasObservations.FirstOrDefault();
if (lastObservation != null && DateTime.Compare(
new DateTime(lastObservation.Time.Year, lastObservation.Time.Month,
lastObservation.Time.Day, lastObservation.Time.Hour,
lastObservation.Time.Minute, lastObservation.Time.Second),
new DateTime(newObservation.Time.Year, newObservation.Time.Month,
newObservation.Time.Day, newObservation.Time.Hour,
newObservation.Time.Minute, newObservation.Time.Second)
) >= 0)
newObservation.Time = lastObservation.Time.AddSeconds(1);
return newObservation;
}
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToMap)
{
var mappedObsList = listToMap
.Select(obs => obs.DeepCopy())
.Select(obs => _configObservationService.Map(obs).Result)
.Where(obs => obs != null)
.ToList();
// mappedObsList contains observation mapped
// Check blue code
_ = CalculateBlueCodeList(mappedObsList);
return Task.FromResult(listToMap);
}
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
if (!string.IsNullOrEmpty(alarmToInsert.EventId))
obs.Code = alarmToInsert.EventId;
if (!string.IsNullOrEmpty(alarmToInsert.Event))
obs.Name = alarmToInsert.Event;
obs.Value = alarmToInsert.Value;
return Task.FromResult(obs);
}
Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type)
{
await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type);
}
/// <summary>
/// Initial check to ignore old observations generating blue codes
/// </summary>
/// <param name="obs">Observation</param>
/// <returns>True if it needs to be ignored</returns>
private bool CheckIfObservationIsOlderAndShouldBeIgnored(PatientObservation obs)
{
if (obs.Time.ToUniversalTime().AddMinutes(_apiSettings.IgnoreCalcObservationsOlderInMinutesThan) <
DateTime.UtcNow)
{
Log.Debug(
"ignoring calc old observation: name:{obsName} obs_time:{obsTime} system_time {DateTimeNow} id:{obsId}",
obs.Name ?? "null", obs.Time, DateTime.Now, obs.Id);
return true;
}
return false;
}
private async Task<PatientObservation> CheckAlarmConfig(PatientObservation pobs)
{
var configObs = await _configObservationService.Get(new PatientObservation
{
Name = pobs.Name,
PatientId = pobs.PatientId
});
pobs.Alarm = configObs?.Alarm ?? null;
return pobs;
}
private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs)
{
return new PatientObservation
{
CodingSystem = "ADAS_ALARM",
Code = name,
Name = $"Alarm_{name}",
Value = pobs.Value,
PatientId = pobs.PatientId,
Time = pobs.Time,
Alarm = pobs.Alarm
};
}
/// <summary>
/// Checks if a blue code observation Lists needs to be generated and inserts it.
/// </summary>
/// <param name="obsList"></param>
public async Task CalculateBlueCodeList(List<PatientObservation?>? obsList)
{
//Las 3 observaciones vienen siempre juntas en el mismo mensaje
//Solo se comprueba si se cumplen 2 de las 3 condiciones
var condition = 0;
var patientId = obsList?.FirstOrDefault()?.PatientId;
if (patientId == null) return;
var fcObs = obsList?.FirstOrDefault(o => o is { Name: "FC" });
var sattcObs = obsList?.FirstOrDefault(o => o is { Name: "Sattc" });
var tamObs = obsList?.FirstOrDefault(o => o is { Name: "TAm" });
// FC < 60
if (fcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(fcObs) &&
double.TryParse(fcObs.Value.ToString(), out var cardiacBeatRate) && cardiacBeatRate < 60)
{
Log.Debug("BlueCode condition: FC {fc} for patient: {obsPatientid} fc increase limit", cardiacBeatRate,
fcObs.PatientId);
condition++;
}
// SpO2 < 80 (Sattc)
if (sattcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(sattcObs) &&
double.TryParse(sattcObs.Value.ToString(), out var spO2Value) && spO2Value < 80)
{
Log.Debug("Blue code condition: SpO2 {sp} for patient: {obsPatientid} Sattc increase limit", spO2Value,
sattcObs.PatientId);
condition++;
}
if (condition == 0) return;
// Early exit if 2 conditions are met
if (condition == 2)
{
_ = SendBlueCode(fcObs);
return;
}
// TAm actual es un 50% por debajo de la medición de 30 seg antes
if (tamObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(tamObs) &&
double.TryParse(tamObs.Value.ToString(), out var tamObsValue))
{
// Buscamos el anterior
var lastPressBloodObsList =
await _observationService.Value.FindLastObservations(tamObs.PatientId, 1, _pressBloodArteryMean);
var lastPressBloodObs = lastPressBloodObsList.FirstOrDefault();
// Diferencia entre la última observación es < 30s
if (lastPressBloodObs != null &&
double.TryParse(lastPressBloodObs.Value.ToString(), out var lastPressBloodValue) &&
(tamObs.Time - lastPressBloodObs.Time).TotalSeconds <= 35 &&
tamObsValue <= lastPressBloodValue / 2)
{
Log.Debug("Blue code condition: TAm decrease limit over 30s for patient: {obsPatientid}",
tamObs.PatientId);
condition++;
}
}
if (condition >= 2) _ = SendBlueCode(tamObs);
}
private async Task SendBlueCode(PatientObservation? pobs)
{
if (pobs == null)
return;
Log.Debug("Blue code Alarm for patient: {obsPatientid}", pobs.PatientId);
var blueCodeObservation = CreateAlarmObservation("BlueCode", pobs);
blueCodeObservation = await CheckAlarmConfig(blueCodeObservation);
_ = _observationService.Value.InsertObservation(blueCodeObservation);
_ = SendAlarm(blueCodeObservation, "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Type.Auto);
}
//PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP
//FLUJO ALTO: poner OAF en su lugar
private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs)
{
var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value
: obs is PatientObservation o ? o.Value : null;
if (value != null && value.ToString()!.Contains("PRVC"))
obs.Value = value.ToString()!.Replace("PRVC", "VCRP");
else if (value != null && value.ToString()!.Contains("FLUJ.ALTO")) obs.Value = "OAF";
_logger.LogDebug("Calculating patient id: {id} ventilation mode: {obsName}, value : {obsValue}", obs.PatientId,
obs.Name, obs.Value);
return obs;
}
//TODO duplicado con el calculated del 12o, sacar a un utils
private async Task<BasePatientObservationValue> CheckObsWithSameTimeExistsAndIncrementTime(
BasePatientObservationValue obs)
{
if (obs is not PatientObservation pobs) return obs;
var existObsWithSameTime =
await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name);
if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1);
return pobs;
}
//S/F Calc ( craneal saturation / FiO2
private async Task CalculateSf(BasePatientObservationValue obs, string name)
{
try
{
PatientObservation? saturation = null;
PatientObservation? fio2 = null;
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["FiO2", "Sattc"]);
switch (name)
{
case "FiO2":
fio2 = (PatientObservation)obs;
saturation = values.FirstOrDefault(o => o.Name == "Sattc");
break;
case "Sattc":
saturation = (PatientObservation)obs;
fio2 = values.FirstOrDefault(o => o.Name == "FiO2");
break;
}
if (fio2 != null && saturation != null && !fio2.Expired &&
double.TryParse(fio2.Value.ToString(), out var fio) && !saturation.Expired &&
double.TryParse(saturation.Value.ToString(), out var sat) && sat <= 97)
{
if (fio == 0)
{
_logger.LogError("Error calculating SF. FiO2: {fio}", fio);
return;
}
var sfValue = sat / fio;
sfValue = Math.Round(sfValue, 4);
var sfObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Sattc_FiO2",
CodingSystem = "ADAS",
Value = sfValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(sfObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate S/F observation. Exception: {exMessage}", ex.Message);
}
}
/// <summary>
/// Oxygenation index is calculated => P_VAM x FiO2 x 100 / PaO2_Tidal
/// the method is called when it receives P_VAM or FiO2 or PaO2_Tidal and tries to take the other values to perform the
/// calculation if they are not
/// in database then does nothing.
/// </summary>
public async Task CalculateOxygenationIndex(BasePatientObservationValue obs, string name)
{
var toSearchList = new List<string>();
PatientObservation? pVam = null;
PatientObservation? fiO2 = null;
PatientObservation? paO2 = null;
toSearchList.AddRange(new List<string> { "P_VAM", "FiO2", "PaO2_Tidal" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
try
{
switch (name)
{
case "P_VAM":
pVam = (PatientObservation)obs;
fiO2 = values.FirstOrDefault(o => o.Name == "FiO2");
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "FiO2":
fiO2 = (PatientObservation)obs;
pVam = values.FirstOrDefault(o => o.Name == "P_VAM");
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "PaO2_Tidal":
paO2 = (PatientObservation)obs;
fiO2 = values.FirstOrDefault(o => o.Name == "FiO2");
pVam = values.FirstOrDefault(o => o.Name == "P_VAM");
break;
}
//Controlar que FiO2 no haya expirada al recogerla
if (fiO2 != null && paO2 != null && pVam != null &&
int.TryParse(pVam.Value.ToString(), out var pvam) &&
int.TryParse(fiO2.Value.ToString(), out var fio2) &&
int.TryParse(paO2.Value.ToString(), out var pao2))
{
if (pao2 == 0)
{
_logger.LogError(
"Error calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2,
name);
return;
}
var oxygenationIndexValue = pvam * fio2 * 100 / pao2;
var oxygenationIndexObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Oxygenation_Index",
CodingSystem = "ADAS",
Value = oxygenationIndexValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(oxygenationIndexObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message);
}
}
//P/F Calc ( PaO2_Tidal / FiO2
private async Task CalculatePf(BasePatientObservationValue obs, string name)
{
try
{
PatientObservation? paO2 = null;
PatientObservation? fio2 = null;
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["FiO2", "PaO2_Tidal"]);
switch (name)
{
case "FiO2":
fio2 = (PatientObservation)obs;
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "PaO2_Tidal":
paO2 = (PatientObservation)obs;
fio2 = values.FirstOrDefault(o => o.Name == "FiO2");
break;
}
if (fio2 != null && paO2 != null && !fio2.Expired && !paO2.Expired &&
double.TryParse(paO2.Value.ToString(), out var pa) &&
double.TryParse(fio2.Value.ToString(), out var fi))
{
if (fi == 0)
{
_logger.LogError("Error Calculating PF. FiO2 value is 0. Observation:{obs}", obs);
return;
}
var sfValue = pa / fi;
sfValue = Math.Round(sfValue, 4);
var sfObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "PaO2_FiO2",
CodingSystem = "ADAS",
Value = sfValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(sfObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate P/F observation. Exception: {exMessage}", ex.Message);
}
}
private async Task<PatientObservation?> CreatePumpAlarmObservation(string name,
PumpObservation pumpObservation)
{
var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value
? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"
: $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}";
PatientObservation patientObservation = new()
{
CodingSystem = "ADAS_ALARM",
Code = $"Pump_{name}",
Name = $"Alarm_Pump_{name}",
Value = pumpObservation.DrugName ?? positionPump,
Time = pumpObservation.Time
};
if (pumpObservation.PatientId != null)
patientObservation.PatientId = pumpObservation.PatientId.Value;
return await CheckAlarmConfig(patientObservation);
}
}