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.MongoModels;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Customizations.HRYC;
///
/// Implements the HRYC-specific clinical calculations that derive secondary
/// values (NEWS alarms, IROX, Resp_Rate_Calculated, Hydric_Balance_Calculated,
/// Weight_Diff, Diuresis_Weight, Delta_Pressure, Daily_Balance_Calculated, Allergies, DVE, Drainage_Height,
/// Resp_Type, and Hour_Balance) from incoming raw observations and active configurations loaded from
/// .
///
public class CalculatedObservations : ICalculatedObservations
{
private readonly IOptions _apiSettings;
private readonly List _codesForInvasiveVentilation = [];
private readonly IConfigObservationService _configObservationService;
//private readonly Lazy _boxService;
private readonly List _highFrequencyVentilation = [];
private readonly Lazy _lightBeaconService;
private readonly ILogger _logger;
private readonly List _nonInvasiveVentilation = [];
private readonly Lazy _observationService;
private readonly Lazy _patientService;
///
/// Initializes a new instance of the class,
/// resolving its dependencies (observation, patient, light-beacon, and config-observation services,
/// plus the logger) from the supplied and loading the configured
/// code catalogues (high-frequency ventilation, non-invasive ventilation, and invasive ventilation)
/// from .
///
/// The application's service provider used to resolve the required dependencies and configuration.
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService>(); //observationService;
_patientService = serviceProvider.GetRequiredService>();
_apiSettings = serviceProvider.GetRequiredService>(); //apiSettings;
//_boxService = serviceProvider.GetRequiredService>();
_logger = serviceProvider.GetRequiredService>();
_lightBeaconService = serviceProvider.GetRequiredService>();
_configObservationService =
serviceProvider.GetRequiredService(); //configObservationService;
var highFrenquencyVentilation = _apiSettings.Value.HighFrequencyVentilation ?? null;
highFrenquencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim()));
var nonInvasiveVentilation = _apiSettings.Value.NonInvasiveVentilation ?? null;
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
var codesForInvasiveVentilation = //TODO comprobar si se refiere a InvasiveVentilation
_apiSettings.Value.InvasiveVentilation ?? null;
codesForInvasiveVentilation?.ForEach(x => _codesForInvasiveVentilation.Add(x.Trim()));
}
///
/// Dispatches the supplied raw observation to the appropriate HRYC calculation based on its
/// . The set of supported calculations includes
/// Resp_Mode (ventilation type), Diuresis / Weight_Current
/// (weight difference and diuresis-per-kilogram), AllergiesObs, DrainagesObs,
/// PEEP / Pleateu_Pressure (driving pressure), Daily_Balance,
/// Hydric_Balance / Hour_Balance (time shifting), FR / Vent_Rate
/// (respiratory rate), SpO2 / FiO2 (IROX), and NEWS (alarms).
///
/// The concrete observation type, deriving from .
/// The observation to map or transform in-place.
///
/// Reserved for future use. When , restricts the mapping strategy to
/// name-based lookups only.
///
///
/// A that resolves to the (possibly transformed) observation,
/// or when the input observation has no name.
///
public async Task Map(T obs, bool onlyByName) where T : BasePatientObservation
{
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
switch (obs.Name)
{
case "Resp_Mode":
_logger.LogTrace("Mapping Resp mode observation {obs}", obs);
await CalculateVentilationMode(obs);
break;
case "Diuresis":
case "Weight_Current":
if (obs.Name == "Weight_Current") await CalculateWeight_DiffObservation(obs);
await CalculateDiureis_WeightObservation(obs);
break;
case "AllergiesObs":
await CalculateAllergiesObservation(obs);
break;
case "DrainagesObs":
await CalculateDrainagesObservation(obs);
break;
case "PEEP":
case "Pleateu_Pressure":
await CalculateDelta_PressureObservation(obs);
break;
case "Daily_Balance":
await CalculateDaily_BalanceObservation(obs);
break;
case "Hydric_Balance":
await CalculateHydricBalanceCalculated(obs);
obs = (T)await CalculateHydricBalance(obs);
break;
case "Hour_Balance":
obs = (T)await CalculateHourBalance(obs);
break;
case "FR":
await CalculateRespRate(obs);
await CalculateIrox(obs);
break;
case "Vent_Rate":
await CalculateRespRate(obs);
break;
case "SpO2":
case "FiO2":
await CalculateIrox(obs);
break;
case "NEWS":
await CalculateNews(obs);
break;
}
return obs;
}
///
/// Placeholder mapping for a . The HRYC customization does not
/// currently derive observations from treatments.
///
/// The treatment to map.
/// Never returns a result.
/// Always thrown because treatment mapping is not implemented in the HRYC customization.
public Task Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
///
/// Identity mapping for a : returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
/// do not require HRYC-specific calculation.
///
/// The pump observation to map.
/// A task containing the same instance that was passed in.
public async Task Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
///
/// No-op implementation of the medicine-observation calculation hook. The HRYC customization does
/// not derive Medication or ERMedication observations from the active medicine list.
///
/// The list of active medicines (ignored).
/// The unique identifier of the patient (ignored).
/// A completed task.
public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
///
/// No-op implementation of the active-bolus calculation hook. The HRYC customization does not
/// derive an OpiateBoluses observation.
///
/// The unique identifier of the patient (ignored).
/// A completed task.
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
///
/// Returns an empty enumerable of active treatments. The HRYC customization does not currently
/// maintain a per-patient active-treatment cache.
///
/// The unique identifier of the patient (ignored).
/// A completed task containing an empty .
public Task> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult(new List().AsEnumerable());
}
///
/// Identity mapping for a : returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract.
///
/// The to map.
/// A task containing the same instance that was passed in.
public Task Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
///
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
/// observation's Time is shifted forward by one second to avoid duplicate-key collisions.
///
/// The new patient observation to evaluate for time inconsistencies.
///
/// A task that resolves to the (possibly time-shifted) .
/// Returns the input unchanged when its Name is null or empty, logging the error.
///
public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError(
"Error FixTimeInconsistencyWithLast newObservation name ia null or empty. Observation: {newObservation}",
newObservation);
return newObservation;
}
var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1,
[newObservation.Name]);
var lastObservation = lastObservations.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;
}
///
/// Pre-maps a batch of patient observations, optimising the order in which they are persisted
/// when both Vent_Rate and FR are present. If both observations are present with
/// non-zero values, Vent_Rate is inserted first (and removed from the returned list) so
/// that can derive Resp_Rate_Calculated correctly.
///
/// The list of patient observations to pre-map. Modified in-place.
///
/// A task that resolves to the resulting list of observations (with Vent_Rate removed
/// when it was inserted synchronously, or the original list otherwise).
///
public async Task> PreMapList(List listToInsert)
{
var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE");
var respRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_RESP_RATE");
// Check if obs for Vent_Rate and FR is on the list to insert
if (ventRate == null || respRate == null) return listToInsert;
var ventRateValue = double.TryParse(ventRate.Value.ToString(), out var v1) ? v1 : 0;
// ventRate is on the list to insert but its value is 0
if (ventRateValue == 0)
{
_logger.LogDebug("Find ventRate on PreMap request not processed because its value is 0");
return listToInsert;
}
var respRateValue = double.TryParse(ventRate.Value.ToString(), out var v2) ? v2 : 0;
// respRate is on the list to insert but its value is 0
if (respRateValue == 0)
{
_logger.LogDebug("Find respRate on PreMap request not processed because its value is 0");
return listToInsert;
}
// Exists both obs and have value != 0
// In that case we need to calculate Resp_Rate_Calculated just with the value of Vent_Rate
try
{
_logger.LogDebug(
"Find on PreMap Vent_Rate and FR in the same request process Vent_Rate first to calculate Resp_Rate_Calculated");
await _observationService.Value.InsertObservation(ventRate);
listToInsert.Remove(ventRate);
}
catch (Exception e)
{
_logger.LogError("Error while PreMap on insert obs: {ventRate} exception: {e}", ventRate, e);
}
return listToInsert;
}
///
/// Identity mapping for a source alarm observation paired with a :
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
/// customization contract.
///
/// The patient observation that triggered the alarm.
/// The alarm metadata to be inserted alongside the observation.
/// A task containing the same instance that was passed in.
public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
///
/// Placeholder alarm dispatch hook used by the customization contract. The HRYC customization
/// dispatches NEWS alarms through its private SendAlarm(BasePatientObservation, AlarmEnum.Name)
/// overload; calling this public overload always throws.
///
/// The patient observation that triggered the alarm.
/// The alarm display name.
/// The optional alarm code from .
/// Never returns a result.
/// Always thrown because this overload is not used in the HRYC customization.
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
///
/// Translates a NEWS (National Early Warning Score) value into an alarm level: a value below 5
/// (or an unparsable value) is reported as NewsOff, between 5 and 7 as NewsWarning,
/// and 7 or above as NewsAlert. Returns early when the patient cannot be located.
///
/// The base patient observation that triggered the calculation, expected to be a whose Name is NEWS.
/// A task that represents the asynchronous alarm evaluation.
private async Task CalculateNews(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var parsed = int.TryParse(pobs.Value.ToString(), out var valueParsed);
var patient = await _patientService.Value.FindById(obs.PatientId);
if (patient == null) return;
//var beaconConfig = await _boxService.Value.GetBeaconConfig(new PatientLocation(patient.PointOfCare, patient.Bed));
//power off beacon
//recuperar la beacon del paciente y encenderla apagarla si el valor es > 5&6 o 7 alert
if ((parsed && valueParsed < 5) || !parsed)
//await _balizaService.Value.PowerOffLed(beaconCnf);
//await _observationService.Value.SendObsBroadcast(obs);
await SendAlarm(obs, AlarmEnum.Name.NewsOff);
else
switch (valueParsed)
{
case >= 5 and < 7:
_logger.LogDebug("beacon yellow alert by NEWS for patient: {patientid}", obs.PatientId);
await SendAlarm(obs, AlarmEnum.Name.NewsWarning);
//await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.YELLOW);
break;
case >= 7:
_logger.LogDebug("beacon red alert by NEWS for patient: {patientid}", obs.PatientId);
await SendAlarm(obs, AlarmEnum.Name.NewsAlert);
//await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.RED);
break;
}
//if (obsAlarm == null) return;
}
///
/// Builds a synthetic Alarm_<Name> observation, looks up its configuration, and when
/// the configured alarm and its beacon are enabled, dispatches a light-beacon colour change for
/// the patient's point-of-care. The alarm observation is always persisted. Errors are logged and
/// swallowed.
///
/// The base patient observation that triggered the alarm (used to derive PatientId and Time).
/// The describing the alarm kind (for example, NewsOff, NewsWarning, NewsAlert).
/// A task that represents the asynchronous alarm dispatch.
private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name)
{
var pobs = (PatientObservation)obs;
PatientObservation nObs = new()
{
CodingSystem = "ADAS_ALARM",
Code = name.ToString(),
Name = $"Alarm_{name}",
PatientId = pobs.PatientId,
Time = pobs.Time
};
try
{
//ConfigObservations
var configObs = await _configObservationService.Get(new PatientObservation
{
Name = nObs.Name,
PatientId = obs.PatientId
}
);
if (configObs == null)
{
_logger.LogError("Config observation is null on send alarm NEWS for {nObs}", nObs);
return;
}
if (configObs is { Alarm.Enabled: true })
{
nObs.Alarm = configObs.Alarm;
if (configObs.Alarm.Beacon is { Enabled: true })
{
var patient = await _patientService.Value.FindById(obs.PatientId);
if (patient != null)
{
_logger.LogDebug(
"PatientId: {nObsPatientId}. Send Beacon alarmName {configObsAlarmBeaconBeaconColorValue}",
nObs.PatientId, configObs.Alarm.Beacon.BeaconColor);
nObs.Value = configObs.Alarm.Beacon.BeaconColor.ToString();
SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient);
}
}
}
_logger.LogDebug(
"Insert obs alarm for news at: {DateTimeNow} patientId: {nObsPatientId} color value: {nObsValue}",
DateTime.Now, nObs.PatientId, nObs.Value);
await _observationService.Value.InsertObservation(nObs);
}
catch (Exception ex)
{
_logger.LogError("Exception sending alarm: {exMessage}", ex.Message);
}
}
///
/// Sends a colour command to the light beacon associated with the patient's point-of-care.
/// Maps values to commands
/// (blue, yellow, red, or off) and logs an error when the patient has no point-of-care identifier.
///
/// The beacon colour to apply.
/// The patient whose associated beacon should be updated.
private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient)
{
if (!patient.PointOfCareId.HasValue)
{
_logger.LogError("Error sending beacon color on calculateObservations poc id on patient is null {Patient}",
patient.ToString());
return;
}
switch (color)
{
case AlarmEnum.BeaconColor.Blue:
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue);
break;
case AlarmEnum.BeaconColor.Yellow:
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow);
break;
case AlarmEnum.BeaconColor.Red:
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red);
break;
case AlarmEnum.BeaconColor.None:
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off);
break;
}
}
///
/// Computes the IROX index (SpO2 / FiO2 / FR) when SpO2, FiO2, and FR
/// are all present in the latest observations and were recorded within the last 10 minutes.
/// Persists the resulting IROX observation. Skips silently when any input is missing,
/// zero, or unparsable. Errors are logged and swallowed.
///
/// The base patient observation that triggered the calculation (SpO2, FiO2, or FR).
/// A task that represents the asynchronous calculation operation.
private async Task CalculateIrox(BasePatientObservation obs)
{
try
{
var pob = (PatientObservation)obs;
var obsToCalc = new List { pob };
switch (pob.Name)
{
case "SpO2":
var fio2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["FiO2", "FR"]);
if (fio2Frvalues.Count > 0) obsToCalc.AddRange(fio2Frvalues);
break;
case "FiO2":
var spo2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["SpO2", "FR"]);
if (spo2Frvalues.Count > 0) obsToCalc.AddRange(spo2Frvalues);
break;
case "FR":
var fio2Spo2Values = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["FiO2", "SpO2"]);
if (fio2Spo2Values.Count > 0) obsToCalc.AddRange(fio2Spo2Values);
break;
}
if (!obsToCalc.All(o => o.Time.CompareTo(DateTime.UtcNow.AddMinutes(10)) <= 0)) return;
var fio2 = obsToCalc.FirstOrDefault(o => o.Name == "FiO2");
var fr = obsToCalc.FirstOrDefault(o => o.Name == "FR");
var spo2 = obsToCalc.FirstOrDefault(o => o.Name == "SpO2");
if (fio2 == null || fr == null || spo2 == null) return;
if (!double.TryParse(spo2.Value.ToString(), out var nSpo2) ||
!double.TryParse(fio2.Value.ToString(), out var nFio2) ||
!double.TryParse(fr.Value.ToString(), out var nFr))
{
_logger.LogWarning("Error parsing values. fio2: {fio2}, fr: {fr}, spo2: {spo2}", fio2, fr, spo2);
return;
}
var spo2Value = nSpo2;
var fio2Value = nFio2;
var frValue = nFr;
if (spo2Value == 0 || fio2Value == 0 || frValue == 0) return;
var value = nSpo2 / nFio2 / nFr;
var irox = new PatientObservation
{
Name = "IROX",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time,
Value = value
};
await _observationService.Value.InsertObservation(irox);
}
catch (Exception e)
{
_logger.LogError("Exception calculating IROX, error: {eMessage}", e.Message);
}
}
///
/// Persists a Resp_Rate_Calculated observation when the supplied observation's value is
/// non-zero. For FR, the insert is skipped if a recent (within
/// CalculateRespRateVentExpires minutes) non-zero Vent_Rate observation exists;
/// the inserted observation's Time is set to the original Time for FR, or
/// shifted one second forward for any other name.
///
/// The base patient observation that triggered the calculation (FR or Vent_Rate).
/// A task that represents the asynchronous insert operation.
private async Task CalculateRespRate(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
_logger.LogDebug("CalculateRespRate {obsPatientid} wait {obsName} {obsTime} {pobsValue}", obs.PatientId,
obs.Name, obs.Time, pobs.Value);
if (Convert.ToDouble(pobs.Value) == 0) return;
if (Convert.ToDouble(pobs.Value) == 0) return;
var insert = true;
if (obs.Name == "FR")
{
// Insertamos si no existe una obs Vent_Rate o si es existe tiene más de X" y su valor es > 0
var lastVentRate =
(await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Vent_Rate"]))
.FirstOrDefault();
if (lastVentRate == null || (double.TryParse(lastVentRate.Value.ToString(), out var v) ? v : 0) == 0)
{
_logger.LogDebug("CalculateRespRate {id} lastVentRate: null", obs.PatientId);
}
else
{
var diff = DateTime.UtcNow.Subtract(lastVentRate.Time.ToUniversalTime());
insert =
lastVentRate.Time.ToUniversalTime().AddMinutes(_apiSettings.Value.CalculateRespRateVentExpires) <
DateTime.UtcNow;
_logger.LogDebug(
"CalculateRespRate {patientid} lastVentRate: {value} - {time} passed {diff} {insert}",
obs.PatientId, lastVentRate.Value, lastVentRate.Time, diff, insert ? "Inserted" : "Not inserted");
}
}
if (insert)
{
_logger.LogDebug(
"CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
obs.PatientId, obs.Name, obs.Time, pobs.Value);
var calculatedRespRate = new PatientObservation
{
Id = new ObjectId(),
Name = "Resp_Rate_Calculated",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Name == "FR" ? obs.Time : obs.Time.AddSeconds(1),
Value = pobs.Value,
ParentData = new ParentDataClass { Name = obs.Name }
};
await _observationService.Value.InsertObservation(calculatedRespRate);
}
else
{
_logger.LogDebug(
"CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
obs.PatientId, obs.Name, obs.Time, pobs.Value);
}
_logger.LogDebug("CalculateRespRate {patientid} release {name} {time} {value}", obs.PatientId,
obs.Name, obs.Time, pobs.Value);
}
///
/// Shifts the timestamp of a Hydric_Balance observation forward by one second when another
/// Hydric_Balance observation was already recorded within the same hour, so the most
/// recent entry can be identified unambiguously.
///
/// The base patient observation expected to be a with Name Hydric_Balance.
/// A task that resolves to the (possibly time-shifted) base patient observation.
private async Task CalculateHydricBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var lastHydricBalanceFromHour =
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
"Hydric_Balance");
if (lastHydricBalanceFromHour != null &&
DateTime.Compare(new DateTime(lastHydricBalanceFromHour.Time.Year, lastHydricBalanceFromHour.Time.Month,
lastHydricBalanceFromHour.Time.Day, lastHydricBalanceFromHour.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
pobs.Time = lastHydricBalanceFromHour.Time.AddSeconds(1);
return obs;
}
///
/// Shifts the timestamp of a Hour_Balance observation forward by one second when another
/// Hour_Balance observation was already recorded within the same hour.
///
/// The base patient observation expected to be a with Name Hour_Balance.
/// A task that resolves to the (possibly time-shifted) base patient observation.
private async Task CalculateHourBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var lastHourBalanceFromHour =
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
"Hour_Balance");
if (lastHourBalanceFromHour != null &&
DateTime.Compare(new DateTime(lastHourBalanceFromHour.Time.Year, lastHourBalanceFromHour.Time.Month,
lastHourBalanceFromHour.Time.Day, lastHourBalanceFromHour.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
pobs.Time = lastHourBalanceFromHour.Time.AddSeconds(1);
return obs;
}
///
/// Persists a Hydric_Balance_Calculated observation mirroring the supplied
/// Hydric_Balance value, after enforcing three guards: the observation's hour must not be
/// in the future, the latest Hydric_Balance_Calculated for the current hour prevents
/// replaying older hours, and a newer stored Hydric_Balance_Calculated prevents
/// overwriting it. The stored time is shifted one second forward when an existing calculated
/// observation in the same hour is detected.
///
/// The base patient observation that triggered the calculation, expected to be a with Name Hydric_Balance.
/// A task that represents the asynchronous insert operation.
private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs)
{
//Hydric_Balance_Calculated
//No guardamos como calculadas nunca horas futuras. En el obx siempre vienen con la hora y los minutos a 00
var pobs = (PatientObservation)obs;
var nowUniversalTime = DateTime.Now.ToUniversalTime();
//La hora de la observación es mayor que la actual. No generamos la calculada
if (DateTime.Compare(new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0),
new DateTime(nowUniversalTime.Year, nowUniversalTime.Month, nowUniversalTime.Day, nowUniversalTime.Hour,
0, 0)) > 0
)
{
_logger.LogDebug(
"HYDRIC BALANCE: La hora de la observación es mayor que la actual. No generamos la calculada {obs}",
obs);
return;
}
//Para la calculada. Si la última que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada.
var lastHydricBalanceList = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Hydric_Balance_Calculated"]);
var lastHydricBalance = lastHydricBalanceList.FirstOrDefault();
if (lastHydricBalance != null && lastHydricBalance.Time.Hour == DateTime.UtcNow.Hour &&
obs.Time.Hour != DateTime.UtcNow.Hour)
{
_logger.LogDebug(
"HYDRIC BALANCE: ultima que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada {obs}",
obs);
return;
}
//Si el último hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada.
if (lastHydricBalance != null &&
DateTime.Compare(new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month,
lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 1
)
{
_logger.LogDebug(
"HYDRIC BALANCE: Si el ultimo hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada. {obs}",
obs);
return;
}
var calculatedHydricBalance = new PatientObservation
{
Name = "Hydric_Balance_Calculated",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time.AddSeconds(1), //To discriminate updates.
Value = pobs.Value,
Units = obs.Units
};
if (lastHydricBalance != null && DateTime.Compare(
new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, lastHydricBalance.Time.Day,
lastHydricBalance.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
calculatedHydricBalance.Time = lastHydricBalance.Time.AddSeconds(1);
await _observationService.Value.InsertObservation(calculatedHydricBalance);
}
///
/// Maps a Resp_Mode observation to a Resp_Type value: codes listed in
/// InvasiveVentilation yield Invasive, otherwise the observation's value is matched
/// against the configured high-frequency / non-invasive catalogues (yielding
/// HighFrequencyVentilation, NonInvasive, or Invasive as fallback). Values
/// equal to "EnESPERA" are skipped.
///
/// The base patient observation that triggered the calculation, expected to be a with Name Resp_Mode.
/// A task that represents the asynchronous insert operation.
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
_logger.LogDebug("CalculateVentilationMode {obs}", obs);
var pobs = (PatientObservation)obs;
var respTypeObs = new PatientObservation
{
Name = "Resp_Type",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time
};
//https://epigram.teamwork.com/#/tasks/34765750
if (obs.Code != null && _codesForInvasiveVentilation.Contains(obs.Code))
{
//añadir que sea solo cuando el valor es invasiva
respTypeObs.Value = nameof(RespirationType.Invasive);
respTypeObs.Time = obs.Time.AddSeconds(1);
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
else
{
var strValue = pobs.Value.ToString();
if (strValue == null)
return;
//si es en espera tendrá que insertar que es en espera
if (!"EnESPERA".Equals(strValue))
{
respTypeObs.Value = _highFrequencyVentilation.Contains(strValue)
? nameof(RespirationType.HighFrequencyVentilation)
: _nonInvasiveVentilation.Contains(strValue)
? respTypeObs.Value = nameof(RespirationType.NonInvasive)
: nameof(RespirationType.Invasive);
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
}
}
///
/// Computes the Weight_Diff observation (new weight − previous weight, rounded to two
/// decimals) by comparing the supplied Weight_Current observation against the most
/// recently stored one for the same patient. Skipped when either value cannot be parsed.
///
/// The base patient observation that triggered the calculation, expected to be a with Name Weight_Current.
/// A task that represents the asynchronous insert operation.
private async Task CalculateWeight_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List();
toSearchList.AddRange(new List { "Weight_Current" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
var pWeight = values.FirstOrDefault(o => o.Name == "Weight_Current");
var newWeightObs = (PatientObservation)obs;
if (pWeight?.Value != null && double.TryParse(pWeight.Value.ToString(), out var weight) &&
double.TryParse(newWeightObs.Value.ToString(), out var newWeight))
{
var weightDiffValue = Math.Round(newWeight - weight, 2);
var weightDiff = new PatientObservation
{
Name = "Weight_Diff",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time,
Value = weightDiffValue,
Units = obs.Units
};
await _observationService.Value.InsertObservation(weightDiff);
}
}
///
/// Computes the Diuresis_Weight observation (diuresis / current weight in ml/kg) when
/// either a Diuresis or a Weight_Current observation is received. When triggered
/// by Weight_Current, the operation is skipped if the latest Diuresis observation
/// has expired. Skipped silently when the weight is zero or any value is unparsable.
///
/// The base patient observation that triggered the calculation (Diuresis or Weight_Current).
/// A task that represents the asynchronous insert operation.
private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs)
{
var toSearchList = new List();
PatientObservation? diuresisObs;
PatientObservation? weightObs;
toSearchList.AddRange(new List { "Diuresis", "Weight_Current" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
if (obs.Name == "Diuresis")
{
diuresisObs = (PatientObservation)obs;
weightObs = values.FirstOrDefault(o => o.Name == "Weight_Current");
}
else
{
weightObs = (PatientObservation)obs;
diuresisObs = values.FirstOrDefault(o => o.Name == "Diuresis");
if (diuresisObs == null || CheckExpired(diuresisObs)) return;
}
if (weightObs?.Value != null)
if (double.TryParse(weightObs.Value.ToString(), out var nWeight) && nWeight != 0 &&
double.TryParse(diuresisObs.Value.ToString(), out var diuresis))
{
var diruesisWeightValue = Math.Round(diuresis / nWeight, 2);
var diruesisWeight = new PatientObservation
{
Name = "Diuresis_Weight",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time,
Value = diruesisWeightValue,
Units = "ml/kg"
};
await _observationService.Value.InsertObservation(diruesisWeight);
}
}
///
/// Aggregates a AllergiesObs observation into a single Allergies string per type,
/// special-casing drug allergies ("FÁRMACOS") into a single grouped entry and setting the
/// Status to Alert when drug allergies are present. InvalidCastExceptions
/// are logged and swallowed.
///
/// The base patient observation that triggered the calculation, expected to be a with Name AllergiesObs and a collection-valued Value of .
/// A task that represents the asynchronous insert operation.
private async Task CalculateAllergiesObservation(BasePatientObservation obs)
{
try
{
var pobs = (PatientObservation)obs;
var allergiesObs = new PatientObservation
{
Name = "Allergies",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time,
MessageTime = pobs.MessageTime
};
var patientAllergiesValues =
new List((IEnumerable)pobs.Value);
var allergiesValues = patientAllergiesValues.GroupBy(o => o.Type)
.Select(x =>
new PatientAllergiesValue
{
Type = x.Key,
Value = string.Join(", ", x.Select(v => v.Value)),
Notes = string.Join(", ", x.Select(n => n.Notes))
}).ToList();
var farmacosType = false;
var farmacosValues = new List();
var values = new List();
foreach (var allergies in allergiesValues)
{
if (allergies.Value == null)
continue;
var type = allergies.Type?.Replace("Alergia a ", "").Replace("Alergia ", "").ToUpper();
if (type != null)
switch (type)
{
case "FÁRMACOS":
farmacosType = true;
farmacosValues.Add(allergies.Value.ToUpper());
break;
default:
values.Add(type);
break;
}
}
if (farmacosType)
{
values.Add($"FÁRMACOS ({string.Join(", ", farmacosValues)})");
allergiesObs.Status = StatusEnum.Type.Alert;
}
allergiesObs.Value = string.Join(", ", values);
await _observationService.Value.InsertObservation(allergiesObs);
}
catch (InvalidCastException)
{
_logger.LogError("CalculateAllergiesObservation {obs}", obs);
}
}
///
/// For a DrainagesObs observation of type "Drenaje ventricular", persists the
/// derived DVE (volume) and Drainage_Height observations using the underlying
/// properties.
///
/// The base patient observation that triggered the calculation, expected to be a with Name DrainagesObs and a -typed Value.
/// A task that represents the asynchronous insert operation.
private async Task CalculateDrainagesObservation(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var patientDraingesValue = (PatientDrainagesValue)pobs.Value;
if (patientDraingesValue.Type == "Drenaje ventricular")
{
var dve = new PatientObservation
{
Name = "DVE",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time,
Units = obs.Units
};
var drainageHeight = new PatientObservation
{
Name = "Drainage_Height",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time
};
if (patientDraingesValue.Volume != null)
{
dve.Value = patientDraingesValue.Volume;
await _observationService.Value.InsertObservation(dve);
}
if (patientDraingesValue.Height != null)
{
drainageHeight.Value = patientDraingesValue.Height;
await _observationService.Value.InsertObservation(drainageHeight);
}
}
}
///
/// Computes the Delta_Pressure observation (driving pressure = plateau − PEEP) when both
/// PEEP and Pleateu_Pressure observations are available. The stored Time is
/// the most recent of the two source observations, so the result is anchored to the latest input.
///
/// The base patient observation that triggered the calculation (PEEP or Pleateu_Pressure).
/// A task that represents the asynchronous insert operation.
private async Task CalculateDelta_PressureObservation(BasePatientObservation obs)
{
var toSearchList = new List();
PatientObservation? peep;
PatientObservation? pleateuPressure;
toSearchList.AddRange(new List { "PEEP", "Pleateu_Pressure" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
var time = obs.Time;
double pleateuPressureValue = 0;
if (obs.Name == "PEEP")
{
peep = (PatientObservation)obs;
pleateuPressure = values.FirstOrDefault(o => o.Name == "Pleateu_Pressure");
if (pleateuPressure?.Time != null && time.CompareTo(pleateuPressure.Time) > 0) time = pleateuPressure.Time;
}
else
{
pleateuPressure = (PatientObservation)obs;
peep = values.FirstOrDefault(o => o.Name == "PEEP");
if (peep?.Time != null && time.CompareTo(peep.Time) > 0) time = peep.Time;
}
if (peep?.Value != null && pleateuPressure?.Value != null)
{
var peepSuccess = double.TryParse(peep.Value.ToString(), out var peepValue);
if (!peepSuccess) peepValue = 0;
if (!peepSuccess) pleateuPressureValue = 0;
var deltaPressure = Math.Round(pleateuPressureValue - peepValue, 2);
var deltaPressureObs = new PatientObservation
{
Name = "Delta_Pressure",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = time,
Value = deltaPressure
};
await _observationService.Value.InsertObservation(deltaPressureObs);
}
}
///
/// Persists a Daily_Balance_Calculated observation mirroring the supplied
/// Daily_Balance value, but only when the local hour of the observation is 8 (the daily
/// balance cut-off used by the HRYC customization).
///
/// The base patient observation that triggered the calculation, expected to be a with Name Daily_Balance.
/// A task that represents the asynchronous insert operation.
private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs)
{
if (obs.Time.ToLocalTime().Hour == 8)
{
var dailyBalanceCalculated = new PatientObservation
{
Name = "Daily_Balance_Calculated",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = DateTime.Now,
Units = obs.Units,
Value = ((PatientObservation)obs).Value
};
await _observationService.Value.InsertObservation(dailyBalanceCalculated);
}
}
///
/// Determines whether a patient observation has expired by comparing the current time against
/// Time + Expires seconds, when Expires is set.
///
/// The patient observation to evaluate.
///
/// when the observation has an Expires value and the current time
/// is past the expiration instant; otherwise, .
///
private static bool CheckExpired(PatientObservation obs)
{
if (obs.Expires == null) return false;
var timeExpire = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(obs.Expires));
return DateTime.Now.CompareTo(timeExpire) > 0;
}
}