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; } }