using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; 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.GroupedObservations; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Utils; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Serilog; using Patient = adas_core.Domain.Models.MongoModels.Patient; namespace adas_core.Application.Services; /// /// Provides the concrete implementation of the contract, /// encapsulating the business logic for managing and processing alarms within the application. /// /// public class AlarmService : IAlarmService { private readonly IAlarmRepository _alarmRepository; private readonly IOptions _apiSettings; private readonly ILocalAuditService _auditService; private readonly Lazy _calculatedObservationsService; private readonly IClientMessageService _clientMessageService; private readonly IConfigObservationService _configObservationService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly Lazy _lightBeaconService; private readonly ILogger _logger; private readonly Lazy _observationService; private readonly IPatientService _patientService; private readonly IPointOfCareService _pocService; private readonly Lazy _recordingService; private readonly List _relayAlarmList = []; private readonly Lazy _relayService; private readonly SemaphoreSlim _semaphore = new(1, 1); // Semáforo para evitar la ejecución simultánea del temporizador private readonly ISubscribersService _subscribersService; private readonly IUnitService _unitService; //private readonly string _url; private List _beaconAlarmList = []; private TimeSpan _interval; /// /// Initializes a new instance of the class, injecting required dependencies for alarm processing and optionally starting the internal alarm timer when is . /// /// The used to persist and retrieve alarm data. /// The used for diagnostic logging. /// The used to access patient information. /// The used to retrieve observation configuration. /// A providing deferred access to observation data. /// The used to publish client notifications. /// The used to manage alarm subscribers. /// A providing deferred access to calculated observations. /// A providing deferred access to the light beacon service. /// A providing deferred access to the recording service. /// A providing deferred access to the relay service. /// The providing access to API configuration. /// The used to manage unit information. /// The used to access point-of-care information. /// The used to access the current HTTP context. /// The used to record audit entries. /// A indicating whether the alarm timer should be started during construction. /// Thrown when is . /// public AlarmService(IAlarmRepository alarmRepository, ILogger logger, IPatientService patientService, IConfigObservationService configObservationService, Lazy observationService, IClientMessageService clientMessageService, ISubscribersService subscribersService, Lazy calculatedObservationsService, Lazy lightBeaconService, Lazy recordingService, Lazy relayService, IOptions apiSettings, IUnitService unitService, IPointOfCareService pocService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService, bool startTimer = true ) { _alarmRepository = alarmRepository; _logger = logger; _patientService = patientService; _configObservationService = configObservationService; _observationService = observationService; _clientMessageService = clientMessageService; _subscribersService = subscribersService; _calculatedObservationsService = calculatedObservationsService; _lightBeaconService = lightBeaconService; _recordingService = recordingService; _relayService = relayService; _apiSettings = apiSettings; _unitService = unitService; _pocService = pocService; _httpContextAccessor = httpContextAccessor; _auditService = auditService; if (apiSettings == null) throw new Exception("ApiSettings must be defined"); if (startTimer) StartTimer(); } /// /// Processes and saves an incoming API request, handling ORU_R40 unsolicited alert observation messages. /// Validates that the patient number or location unit name is provided, resolves the patient and unit configuration, /// and processes any alarm observations; requests are ignored when no patient is found or auto-adt management is disabled. /// /// The API request containing patient, location, observation, and alarm data to process. /// Thrown when both the patient number and location unit name are missing. /// Thrown when the API request type is not valid for observations. /// public async Task SaveRequest(ApiRequest apiRequest) { if ( string.IsNullOrEmpty(apiRequest.PatientNumber) && string.IsNullOrEmpty(apiRequest.Location?.UnitName) ) { _logger.LogDebug("Patient and PointOfCare are nulls"); throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestMissingParameters); } var patient = await _patientService.FindPatientByApiRequest(apiRequest); if (patient == null) { // NO PATIENTS OR LOCATIONS WERE FOUND _logger.LogWarning( "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", apiRequest.PatientNumber, apiRequest.Location); return; } var unitConfig = await _unitService.FindById(patient.UnitId); if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Alarm")) return; switch (apiRequest.Type) { /* * ORU_R40 - Unsolicited transmission of an alert observation message */ case "ORU_R40": // UNSOLICITED ALERT OBSERVATION // OBSERVATIONS if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null) apiRequest.Observations = [apiRequest.Observation]; //var obrcode = apiRequest.ObservationData?.Code ?? ""; if (!apiRequest.Alarms.IsNullOrEmpty()) await ProcessAlarmObservations(apiRequest.Alarms ?? [], apiRequest.Observations ?? [], patient, apiRequest.ObservationData?.Time ?? apiRequest.MessageTime, apiRequest.ObservationData); break; default: _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations", apiRequest.Type); throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Observations"); } } /// /// Asynchronously saves the specified API request by delegating to the underlying save operation. /// /// The API request to be saved. /// public async Task SaveRequestAsync(ApiRequest apiRequest) { await SaveRequest(apiRequest); } /// /// Maps a through the configuration observation service and then through the calculated observations service. /// If either mapping step returns null, or an exception occurs, the method logs the issue and returns null instead of propagating the error. /// /// The patient observation alarm to be mapped. /// If true, mapping is performed by name only; otherwise the full mapping is applied. /// A mapped if both mapping steps succeed; otherwise, null. /// public async Task MapObservation(PatientObservationAlarm obs, bool onlyByName = false) { try { _logger.LogTrace("Mapping config Observation obs: {obs} onlyByName: {onlyByName}", obs, onlyByName); var obs2 = await _configObservationService.Map(obs, onlyByName); if (obs2 == null) { _logger.LogTrace("Mapping obs2 {obs}: Ignored", obs); return null; } _logger.LogTrace("Mapping _configObservationService.Map obs2: {obs2}", obs2); var obs3 = await _calculatedObservationsService.Value.Map(obs2, onlyByName); if (obs3 == null) { _logger.LogTrace("Mapping obs3 {obs2}: Ignored", obs2); return null; } return obs3; } catch (Exception ex) { _logger.LogError(ex, "Error Mapping Observation, Ignoring Observation: {obs} Exception:{ex}", obs, ex.Message); return null; } } /// /// Retrieves the most recent patient observation alarms for the specified patient, optionally filtered by a set of fields. /// /// The identifier of the patient whose last observations should be retrieved. /// An optional list of fields used to restrict which observations are returned; when null, no field filter is applied. /// A task that represents the asynchronous operation, containing a list of the patient's last entries. /// public async Task> FindLastObservationsByField(ObjectId patientId, List? filterObservations = null) { var result = await _alarmRepository.AggregatedPatientLastObservationsByField(patientId, filterObservations); return result; } /// /// Retrieves the most recent non-expired patient observation alarms for the specified patient, filtered by the given observation fields and alarm configurations. /// /// The unique identifier of the patient whose alarms are being queried. /// The list of fields used to filter the observations included in the aggregation. /// The list of alarm configurations that define the criteria applied during the aggregation. /// A task that resolves to a list of the latest non-expired entries matching the criteria. /// public async Task> FindLastValuesNotExpired(ObjectId patientId, List filterObservations, List configAlarm) { var result = await _alarmRepository.AggregatedPatientNotExpiredObservationsByField(patientId, filterObservations, configAlarm); return result; } /// /// Maps the provided patient observation alarm to its corresponding configuration by name, returning the mapped alarm if a matching configuration is found. /// /// The patient observation alarm to be mapped by name. /// A task that returns the mapped if a matching configuration is found; otherwise, null. /// public async Task MapObservationsByName(PatientObservationAlarm obs) { return await _configObservationService.Map(obs, true); } /// /// Processes and persists a list of patient observation alarms, enriching each alarm with patient, timing, and coding metadata. When a matching non-alarm observation is found in the provided list, its coding fields are reused; otherwise defaults derived from the alarm priority (Ph/Pm/Pl) are applied. For alarms that carry source data, related observations are mapped and submitted through the observation service before the alarm itself is inserted. /// /// The alarm observations to process and insert. /// Existing non-alarm observations used to look up and reuse coding information when a value match is found. /// The patient the alarms are associated with. /// The message time assigned to each alarm. /// Optional parent observation metadata applied to all alarms in the batch. /// public async Task ProcessAlarmObservations(List alarmObservations, List observations, Patient patient, DateTime messageTime, ObservationData? observationData = null) { _logger.LogDebug( "Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING {observationsCount} OBSERVATIONS", patient.Id, patient.UnitId, patient.PointOfCareId, messageTime, alarmObservations.Count); ParentDataClass? parentData = null; if (observationData != null) parentData = new ParentDataClass { Code = observationData.Code, CodingSystem = observationData.CodingSystem, Name = observationData.Text }; var listToInsert = new List(); foreach (var obs in alarmObservations) { obs.ParentData = parentData; obs.MessageTime = messageTime; obs.PatientId = patient.Id; obs.Patient = patient; obs.Id = ObjectId.GenerateNewId(); var intObsTime = new DateTimeOffset(obs.Time).ToUnixTimeSeconds(); if (obs.Time == DateTime.MinValue || intObsTime <= 10) obs.Time = DateTime.UtcNow; var intMessageTime = new DateTimeOffset(obs.MessageTime).ToUnixTimeSeconds(); if (obs.MessageTime == DateTime.MinValue || intMessageTime <= 10) obs.MessageTime = DateTime.UtcNow; if (obs.Value.ToString() == "System.Object") { obs.Value = obs.Event?.ToString()??string.Empty; } _logger.LogDebug( "Patient: {patientId} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING ALARM OBSERAVTION {obs}", patient.Id, patient.PointOfCare, patient.Bed, messageTime, obs); listToInsert.Add(obs); } //listToInsert.ForEach(async obs => await InsertObservation(obs)); foreach (var alarmToInsert in listToInsert) { var alarmData = observations.FirstOrDefault(obs => obs.Value.ToString() == alarmToInsert.Value.ToString()); if (alarmData != null) { alarmToInsert.Code = alarmData.Code; alarmToInsert.Name = alarmData.Code; alarmToInsert.CodingSystem = alarmData.CodingSystem; } else { alarmToInsert.CodingSystem = parentData?.CodingSystem?? "MDIL-ALARM"; alarmToInsert.Code = alarmToInsert.Priority.ToString(); alarmToInsert.Name = alarmToInsert.Priority switch { AlarmEnum.ObservationAlarmPriority.Ph => "RedAlarm_Ph", AlarmEnum.ObservationAlarmPriority.Pm => "YellowAlarm_Pm", AlarmEnum.ObservationAlarmPriority.Pl => "BlueAlarm_Pl", _ => "", }; } if (!alarmToInsert.Sources.IsNullOrEmpty()) { var apiRequestObs = new ApiRequest { Type = "ORU_R01", MessageTime = messageTime, ObservationData = observationData }; var obsToInsert = new List(); alarmToInsert.Sources?.ForEach(async void (c) => { try { var obs = new PatientObservation { Code = c.Code, Name = c.OriginalName, CodingSystem = c.CodeSystem, Units = c.Units, Value = c.Value?.ToString() ?? "No value", Time = alarmToInsert.Time, Result = c.Result }; var obs2 = await _calculatedObservationsService.Value .MapSourceAlarm(obs, alarmToInsert); obsToInsert.Add(obs2); } catch (Exception e) { _logger.LogError( "Error processing source observation {source} for alarm {alarm}. Exception: {ex}", c, alarmToInsert, e); } }); apiRequestObs.Observations = obsToInsert; apiRequestObs.Location = patient.Location; apiRequestObs.Patient = patient.Person; apiRequestObs.PatientNumber = patient.PatientNumber; await _observationService.Value.SaveRequestAsync(apiRequestObs); } await InsertObservation(alarmToInsert); } } /// /// Inserts a patient observation alarm, optionally mapping it first. If mapping returns null, the observation is ignored. Persistence is controlled by the flag but is overridden to false when the observation's own Persist property is false. Any exception raised during processing is caught and logged without being rethrown. /// /// The patient observation alarm to insert. /// Indicates whether the observation should be persisted to the repository. Defaults to true and is forced to false if the observation's Persist property is false. /// Indicates whether the observation should be mapped before being processed. Set to false only when the observation originates from the inner refactor job. /// private async Task InsertObservation(PatientObservationAlarm obs, bool persistObs = true, bool mapObs = true) { try { var obs2 = obs; //only will be false if the obs comes from the inner refactor job if (mapObs) obs2 = await MapObservation(obs2, onlyByName: true); if (obs2 == null) { _logger.LogDebug("Mapped observation returns null. Ignored {obs}", obs); } else { if (obs2.Persist.HasValue && !obs2.Persist.Value) persistObs = false; if (persistObs) { _logger.LogDebug("Mapped {obs2}", obs2); await _alarmRepository.InsertOneAsync(obs2); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2); } _logger.LogDebug("Inserted {obs2}", obs2); await SendObsBroadcast(obs2); } } catch (Exception ex) { _logger.LogError("Error Inserting observation {obs}. Excepcion; {ex} ", obs, ex); } } /// /// Broadcasts a patient observation to all subscribers whose configured locations match the patient's point of care. /// Skips observations without a name and returns silently when the associated patient cannot be resolved from the observation or the patient service. /// /// The patient observation to be sent to matching subscribers as an alarm operation. /// private async Task SendObsBroadcast(BasePatientObservation obs) { if (obs.Name == null) return; const OperationType type = OperationType.Alarm; var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId); if (patient == null) { _logger.LogDebug("Not patient on bd to sendOnBroadcastObs: {obspatientid}", obs.PatientId); return; } _logger.LogTrace( "sending obs name: {obsname} to patient id: {patientid}, PointOfCare: {patientpointOfCare} {patientbed}", obs.Name, patient.Id, patient.UnitId, patient.Bed); var subscribers = _subscribersService.GetSubscribers().Where(s => !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => c == patient.PointOfCareId )).ToList(); foreach (var subscriber in subscribers) { _logger.LogTrace("sending obs name: {obsname} to subscriber id: {subscriberId}", obs.Name, subscriber.Id); await _clientMessageService.SendAsync(subscriber.Id, type, obs); } } #region activación de alarmas con balizas, relé y grabaciones /// /// Evaluates alarm configuration rules for a patient observation and, when matching conditions and preconditions are satisfied, generates a new alert observation, logs it, and triggers the corresponding alarm notification. /// /// The patient observation whose value is evaluated against configured alert rules and preconditions. /// public async Task CheckObservationAlarm(PatientObservation obs) { //ConfigObservations var configs = await _configObservationService.Get(new PatientObservation { Name = obs.Name, PatientId = obs.PatientId } ); if (configs?.CreateObservation == null) return; var obsValue = obs.Value.ToString() ?? string.Empty; var observationsToCreate = configs.CreateObservation .Where(c => obsValue.ToUpper().Contains(c.RequiredValue?.ToString()?.ToUpper() ?? string.Empty)) .ToList(); foreach (var obsConfig in observationsToCreate) { var create = false; if (obsConfig.Preconditions == null) create = true; else foreach (var preCondition in obsConfig.Preconditions) { if (preCondition.Name == null) continue; var obsWithConditions = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [preCondition.Name]); if (!obsWithConditions.Any()) continue; var foundObs = obsWithConditions.FirstOrDefault(); //Descartamos la observación si ha expirado if (foundObs == null || (obsConfig.Expires.HasValue && foundObs.Time.AddSeconds(obsConfig.Expires.Value) < DateTime.UtcNow)) continue; var foundObsStr = foundObs.Value.ToString(); var requiredValueStr = preCondition.RequiredValue?.ToString(); if (string.IsNullOrEmpty(requiredValueStr) || (foundObsStr != null && foundObsStr.Contains(requiredValueStr))) { create = true; break; } } if (create) { var newObservation = CreateNewObservation(obs, obsConfig, StatusEnum.Type.Alert); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, newObservation); newObservation = await CheckAlarmConfig(newObservation); var obsName = newObservation.Name ?? string.Empty; _ = SendAlarm(newObservation, obsName, null, AlarmEnum.Severity.None, AlarmEnum.Type.Auto); _ = _observationService.Value.InsertObservation(newObservation); } } } /// /// Creates a new by combining observation values from the provided source observation and configuration, applying the specified status type. /// /// The source patient observation providing the value, patient identifier, and time. /// The configuration observation providing the coding system, code, name, and alarm settings. /// The status type to assign to the new observation. /// A new populated with the merged values from the source observation and configuration. /// private static PatientObservation CreateNewObservation(PatientObservation obs, ConfigObservation config, StatusEnum.Type type) { return new PatientObservation { CodingSystem = config.CodingSystem, Code = config.Code, Name = config.Name, Value = obs.Value, PatientId = obs.PatientId, Time = obs.Time, Alarm = config.Alarm, Status = type }; } /// /// Retrieves the alarm configuration for the specified patient observation and applies it to the observation. If no matching configuration is found, the alarm is set to null. /// /// The patient observation whose alarm configuration will be checked and updated. /// The patient observation with the configured alarm applied, or with a null alarm if no configuration was found. /// private async Task CheckAlarmConfig(PatientObservation pobs) { var configObs = await _configObservationService.Get(new PatientObservation { Name = pobs.Name, PatientId = pobs.PatientId }); pobs.Alarm = configObs?.Alarm ?? null; return pobs; } /// /// Sends a new alarm /// /// Observation to generate the alarm /// Name of the alarm /// Code of the alarm for the recording /// Severity of the alarm for the recording /// /// /// New alarm created /// /// public async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Severity severity, AlarmEnum.Type type) { try { var poc = await _pocService.FindPoCByPatientId(obs.PatientId); switch (obs.Time.Kind) { // Convert obs.Time to UTC if it's not already case DateTimeKind.Local: obs.Time = obs.Time.ToUniversalTime(); break; case DateTimeKind.Unspecified: _logger.LogWarning("obs.Time has unspecified kind. Assuming it to be UTC."); obs.Time = DateTime.SpecifyKind(obs.Time, DateTimeKind.Utc); break; case DateTimeKind.Utc: break; default: throw new ArgumentOutOfRangeException(); } //ConfigObservations var configObs = await _configObservationService.Get(new PatientObservation { Name = obs.Name, PatientId = obs.PatientId } ); if (configObs == null) return; if (configObs is { Alarm.Enabled: true }) { obs.Alarm = configObs.Alarm; var now = DateTime.UtcNow; if (obs.Expired || (obs.Expires.HasValue && obs.Time.AddSeconds(obs.Expires.Value) < now)) return; //En la prioridad de las alarmas 1 máxima prioridad if (configObs.Alarm.Beacon is { Enabled: true }) try { if (obs.Time.AddSeconds(configObs.Alarm.Beacon.EndAfter) >= now) { var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId); if (patient != null) { _logger.LogDebug("PatientId: {nObsPatientid}. Send Beacon alarmName {beaconColor}", obs.PatientId, configObs.Alarm.Beacon.BeaconColor); if (!_beaconAlarmList.Any(o => o.PatientId == obs.PatientId && o.Alarm?.Priority < obs.Alarm.Priority)) { _ = SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient); lock (_beaconAlarmList) { _beaconAlarmList.Add(obs); } } } } else { _logger.LogDebug( "PatientId: {nObsPatientid}.Beacon is Expired. EndAfter {endAfter} Time: {obsTime}", obs.PatientId, configObs.Alarm.Beacon.EndAfter, obs.Time); } } catch (Exception ex) { _logger.LogDebug("Exception sending beacon code for patient {patientId}. Exception: {ex}", obs.PatientId, ex); throw; } if (configObs.Alarm.Recording is { Enabled: true }) try { if (obs.Time.AddSeconds(configObs.Alarm.Recording.EndAfter) >= now) { //if(severity == AlarmSeverity.NONE) severity = configObs.Alarm.Recording.Severity; var strValue = obs.Value.ToString(); if (strValue == null) { _logger.LogError("Observation value to string is null observation:{nObs}", obs); return; } _logger.LogDebug("PatientId: {nObsPatientid}. Send Recording {nObsName}", obs.PatientId, obs.Name); if (code == null && Enum.TryParse(configObs.Alarm.Name, out var result)) code = result; _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, code, severity, strValue, configObs.Alarm.Recording.EndAfter, type); } else { _logger.LogDebug( "PatientId: {nObsPatientid}.Recording is Expired. EndAfter {endAfter} Time: {obsTime}", obs.PatientId, configObs.Alarm.Recording.EndAfter, obs.Time); } } catch (Exception ex) { _logger.LogDebug( "Exception sending Alarm Recording for patient {patientId}. Exception: {ex}", obs.PatientId, ex); throw; } if (configObs.Alarm.OpenDoor is { Enabled: true }) try { if (obs.Time.AddSeconds(configObs.Alarm.OpenDoor.EndAfter) >= now) { _logger.LogDebug("PatientId: {nObsPatientid}. Open door observation {obsName}", obs.PatientId, obs.Name); lock (_relayAlarmList) { if (!_relayAlarmList.Any(o => o.PatientId == obs.PatientId && o.Alarm?.Priority < obs.Alarm.Priority)) { PointOfCareConfiguration? poCSettings = null; if (poc is { Configuration.RelayIdList: not null }) poCSettings = poc.Configuration; var status = _relayService.Value.GetRelayByTypeInList(poCSettings?.RelayIdList, RelayEnum.Type.Door).FirstOrDefault()?.ManualRelayStatus; if (status is RelayEnum.Status.Off) _ = RelayPowerOn(obs.PatientId, RelayEnum.Type.Door); lock (_relayAlarmList) { _relayAlarmList.Add(obs); } } } } else { _logger.LogDebug( "PatientId: {nObsPatientid}.Open door is Expired. EndAfter {endAfter} Time: {obsTime}", obs.PatientId, configObs.Alarm.OpenDoor.EndAfter, obs.Time); } } catch (Exception ex) { _logger.LogDebug("Exception opening door for patient {patientId}. Exception: {ex}", obs.PatientId, ex); throw; } } } catch (Exception ex) { Log.Error("Exception sending alarm: {exMessage}", ex.Message); } } /// /// Calculates and dispatches test alarms for a patient observation based on its configuration. Supports beacon alerts, recording, and door control when the corresponding alarm components are enabled; if the source is not a , the method returns without action, and recordings fall back to a default severity when no severity string is provided. /// /// The patient observation value used to look up the alarm configuration and identify the target patient. /// The name associated with the observation, used for logging and alarm context. /// public async Task CalculateAlarmTest(BasePatientObservationValue source, string name) { if (source is not PatientObservation obs) return; //ConfigObservations var configObs = await _configObservationService.Get(source, true); if (configObs is { Alarm.Enabled: true }) { obs.Alarm = configObs.Alarm; string? alarmSeverityStr = null; if (configObs.Alarm.Beacon is { Enabled: true }) { var patient = await _patientService.FindById(obs.PatientId); if (patient == null) return; _logger.LogDebug("PatientId: {nObsPatientid}. Send Beacon alarmName {beaconColor}", obs.PatientId, configObs.Alarm.Beacon.BeaconColor); _ = SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient); } if (configObs.Alarm.Recording is { Enabled: true }) { _logger.LogDebug("PatientId: {nObsPatientid}. Send Recording {nObsName}", obs.PatientId, obs.Name); if (!string.IsNullOrEmpty(alarmSeverityStr) && Enum.TryParse(alarmSeverityStr, out AlarmEnum.Severity alarmSeverity)) _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, AlarmEnum.Name.Test, alarmSeverity, "test description", configObs.Alarm.Recording.EndAfter); else _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, AlarmEnum.Name.Test, AlarmEnum.Severity.Yellow, "test description", configObs.Alarm.Recording.EndAfter); } if (configObs.Alarm.OpenDoor is { Enabled: true }) { _logger.LogDebug("PatientId: {nObsPatientid}. Open door", obs.PatientId); _ = RelayPowerOn(obs.PatientId, RelayEnum.Type.Door); } } } /// /// Sends a beacon color signal to the light beacon service associated with the patient's point of care. If the patient has no PointOfCareId, the operation is skipped and an error is logged. Maps values (Blue, Yellow, Red, None) to the corresponding signals (Blue, Yellow, Red, Off). /// /// The beacon color to transmit to the light beacon service. /// The patient whose associated point of care device should display the beacon color. /// private Task SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient) { if (!patient.PointOfCareId.HasValue) { _logger.LogError("Try to sen beacon code, but no PointOfCareId id is present in the Patient {Patient}", patient.ToString()); return Task.CompletedTask; } 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; } return Task.CompletedTask; } /// /// /// /// Hora de la observación /// /// /// /// /// /// /// /// /// private async Task StartRecording(ObjectId patientId, DateTime eventTime, AlarmItem? recording, AlarmEnum.Name? alarmName, AlarmEnum.Severity severity, string? alarmDescription, int? endAfter, AlarmEnum.Type type = AlarmEnum.Type.Manual) { try { var patient = await _patientService.FindById(patientId); if (patient is not { PointOfCareId: not null }) return; var poc = await _pocService.FindByIdAllConfig(patient.PointOfCareId.Value); if (poc == null) return; //30 minutos antes y después de la fecha de la observación var startTime = recording != null ? eventTime.AddSeconds(-recording.StartBefore) : eventTime; var endDate = endAfter.HasValue ? eventTime.AddSeconds(endAfter.Value) : (DateTime?)null; await _recordingService.Value.SendRecordingDataToQueue(patient, poc, startTime, endDate, eventTime, alarmName, severity, alarmDescription, true, type); } catch (Exception ex) { _logger.LogError( "Error Starting recording from patientId: {patientId}. eventTime: {eventTime}. AlarmItem: {recording}. alarmSeverity: {alarmSeverity} Error: {ex}", patientId, eventTime, recording, severity, ex); } } /// /// Powers on the relay associated with a patient's point of care configuration matching the specified relay type. /// Exits silently when the patient, point of care, or matching relay configuration cannot be found, and logs any errors encountered during execution. /// /// The identifier of the patient whose relay should be powered on. /// The type of relay to power on, used to locate the matching configuration in the point of care's relay list. /// private async Task RelayPowerOn(ObjectId patientId, RelayEnum.Type type) { try { var patient = await _patientService.FindById(patientId); if (patient is not { PointOfCareId: not null }) { Log.Error("can not power on relay because patient: {patientId} not found", patientId); return; } var poc = await _pocService.FindById(patient.PointOfCareId.Value); if (poc?.Configuration == null) return; var relayConfig = _relayService.Value.GetRelayByTypeInList(poc.Configuration.RelayIdList, type).FirstOrDefault(); if (relayConfig != null) await _relayService.Value.PowerOn(relayConfig); } catch (Exception ex) { _logger.LogError("Error Relay Power On from patientId: {patientId}. Error: {ex}", patientId, ex); } } /// /// Starts a periodic background timer that retrieves the list of locations and periodically checks for expired alarms, using a semaphore to ensure thread-safe execution. /// Logs any errors encountered during timer initialization or execution without rethrowing them. /// /// private async void StartTimer() { try { var pocList = await _pocService.GetAllLocationInfo(); _interval = TimeSpan.FromSeconds(_apiSettings.Value.ExpireAlertIntervalSeconds); _ = new Timer(async void (_) => { try { await _semaphore.WaitAsync(); // Esperar a adquirir el semáforo antes de ejecutar el temporizador await CheckExpiredAlarms(pocList); } catch (Exception e) { _logger.LogError("Error in timer execution: {message}", e.Message); //throw new Exception("Error in timer execution", e); } finally { _semaphore.Release(); // Liberar el semáforo después de ejecutar el temporizador } }, null, TimeSpan.Zero, _interval); } catch (Exception e) { _logger.LogError("Error starting timer: {message}", e.Message); //throw new Exception("Error starting timer", e); } } /// /// Asynchronously checks for expired alarms by running the beacon and relay expiration checks in parallel. /// /// The list of points of care to be evaluated for expired beacon alarms. /// private async Task CheckExpiredAlarms(List pocList) { _logger.LogTrace("Checking Expired Alarms Started"); // Iniciar ambas tareas de forma asincrónica var checkBeaconsTask = CheckExpiredBeaconsAsync(pocList); var checkRelayTask = CheckExpiredRelayAsync(); // Esperar a que ambas tareas completen await Task.WhenAll(checkBeaconsTask, checkRelayTask); _logger.LogTrace("Checking Expired Alarms Finished"); } private readonly SemaphoreSlim _beaconListSemaphore = new(1, 1); /// /// Asynchronously checks for expired beacon alarms and turns off the corresponding lights. /// If no alarms are active, all beacons in the supplied list are turned off; otherwise, beacons /// whose alarm end time has passed are turned off and the alarm list is pruned to retain only /// the still-valid observations. /// /// The list of points of care whose beacons should be turned off when no alarms are active. /// private async Task CheckExpiredBeaconsAsync(List pocList) { await _beaconListSemaphore.WaitAsync(); try { if (!_beaconAlarmList.Any()) //var pocList = await _pocService.GetAllLocationInfo(); if (pocList.Any()) { var tasks = pocList.Select(async poc => { await _lightBeaconService.Value.SendColor(poc, LightBeaconColor.Off); }); await Task.WhenAll(tasks); return; } } finally { _beaconListSemaphore.Release(); } var now = DateTime.UtcNow; List updatedList = []; List ledTasks = []; await _beaconListSemaphore.WaitAsync(); try { foreach (var obsGroup in _beaconAlarmList.GroupBy(o => o.PatientId)) { var nonExpiredObs = obsGroup.Where(obs => obs.Alarm is { Beacon: not null } && (obs.Time.Kind == DateTimeKind.Utc ? obs.Time : obs.Time.ToUniversalTime()) .AddSeconds(obs.Alarm.Beacon.EndAfter) >= now ).ToList(); if (!nonExpiredObs.Any()) { var patient = obsGroup.FirstOrDefault()?.Patient; if (patient is { PointOfCareId: not null }) ledTasks.Add(_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off)); } else { updatedList.AddRange(nonExpiredObs); } } _beaconAlarmList = updatedList; } finally { _beaconListSemaphore.Release(); } await Task.WhenAll(ledTasks); } /// /// Asynchronously checks for expired relay alarms grouped by patient, removes observations whose /// OpenDoor.EndAfter duration has elapsed, and powers off the corresponding relays when no /// non-expired observations remain, provided the manual relay status is not forced On. Only /// patients without an assigned PointOfCareId are processed, and the relay alarm list is /// synchronized under a lock with the filtered non-expired observations. /// /// private async Task CheckExpiredRelayAsync() { var now = DateTime.UtcNow; IEnumerable> groupedByPatientId; lock (_relayAlarmList) { groupedByPatientId = _relayAlarmList.GroupBy(o => o.PatientId); } foreach (var obsGroup in groupedByPatientId) { var patient = await _patientService.FindById(obsGroup.Key); if (patient is not { PointOfCareId: null }) continue; var nonExpiredObs = obsGroup.Where(obs => obs.Alarm is { OpenDoor: not null } && (obs.Time.Kind == DateTimeKind.Utc ? obs.Time : obs.Time.ToUniversalTime()).AddSeconds( obs.Alarm.OpenDoor.EndAfter) >= now ).ToList(); // Apagar el LED si no hay observaciones no expiradas if (!nonExpiredObs.Any()) { //buscamos en PoCSettings si está activado de forma manual var pocSettings = await _pocService.FindById(patient.PointOfCareId!.Value); var relays = _relayService.Value.GetRelayInList(pocSettings?.Configuration?.RelayIdList); foreach (var relay in relays) { // Verificamos si NO tiene un estado manual activo (On) // Si el estado es null o es diferente de On, lo apagamos if (relay.ManualRelayStatus != null && relay.ManualRelayStatus != RelayEnum.Status.On) { await _relayService.Value.PowerOff(relay); } } } // Reemplazar la lista original con las observaciones no expiradas lock (_relayAlarmList) { _relayAlarmList.RemoveAll(obs => obs.PatientId == obsGroup.Key); _relayAlarmList.AddRange(nonExpiredObs); } } } } #endregion