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.DTO; using adas_core.Domain.Models.MongoModels; using Microsoft.Extensions.Logging; using MongoDB.Bson; namespace adas_core.Application.Services; /// /// Provides the concrete implementation of the contract, /// handling device-related service operations defined by the interface. /// public class DeviceService : IDeviceService { private readonly IDeviceRepository _deviceRepository; private readonly IObservationService _observationService; private readonly IAlarmService _alarmService; private readonly IConfigObservationService _configObservationService; private readonly IPointOfCareService _pointOfCareService; private readonly ILogger _logger; public DeviceService( IDeviceRepository deviceRepository, IPointOfCareService pointOfCareService, ILogger logger, IObservationService observationService, IConfigObservationService configObservationService, IAlarmService alarmService) { _deviceRepository = deviceRepository; _pointOfCareService = pointOfCareService; _logger = logger; _observationService = observationService; _configObservationService = configObservationService; _alarmService = alarmService; } /// /// Converts a into a entity by mapping its properties. Applies a fallback to an empty list when PointOfCareIds is null, and to a new instance when Settings is null. /// /// The data transfer object containing the device information to convert. /// A new entity populated with the values from the supplied DTO. public Device ToEntity(DeviceDto dto) { return new Device() { DeviceType = dto.DeviceType, MacAddr = dto.MacAddr, SerialNumber = dto.SerialNumber, Name = dto.Name, Battery = dto.Battery, Color = dto.Color, Connected = dto.Connected, Ready = dto.Ready, Uuid = dto.Uuid, Key = dto.Key, CreatedAt = dto.CreatedAt, UpdatedAt = dto.UpdatedAt, PointOfCareIds = dto.PointOfCareIds ?? new List(), Settings = dto.Settings ?? new DeviceSettings() }; } /// /// Creates a new device entity from the provided DTO, sets the creation and update timestamps to the current UTC time, and persists it via the device repository. /// /// The data transfer object containing the device information to be mapped and stored. /// The created entity after successful insertion, or if the device could not be created. public async Task Create(DeviceDto deviceDto) { var device = ToEntity(deviceDto); device.CreatedAt = DateTime.UtcNow; device.UpdatedAt = DateTime.UtcNow; await _deviceRepository.InsertOneAsync(device); return device; } /// /// Deletes an object identified by the specified by delegating to the device repository. /// Returns true when the repository's delete operation yields a non-null result, and false when the result is null (e.g., the object was not found or could not be deleted). /// /// The unique identifier of the object to delete. /// A task that resolves to true if the object was deleted; otherwise, false. public async Task Delete(ObjectId objectId) { return await _deviceRepository.DeleteAsync(objectId) != null; } /// /// Updates an existing device by mapping the provided DTO to a device entity, setting the update timestamp, and persisting the changes through the repository. /// /// The data transfer object containing the updated device information. /// The updated entity, or null if no device is returned. public async Task Update(DeviceDto deviceDto) { var device = ToEntity(deviceDto); device.UpdatedAt = DateTime.UtcNow; await _deviceRepository.UpdateOneAsync(device.Id, device); return device; } /// /// Processes an incoming device event by locating an existing device or creating a new one, then handling device-type-specific logic. Looks up the device using the MAC address, serial number, UUID, or key in that order, falling back to creating a new record when no match is found. When the device is a button, additional button management logic is invoked. /// /// The data transfer object containing the device information from the event, used for lookup and creation. /// The existing or newly created associated with the event. public async Task ReceiveEvent(DeviceDto deviceDto) { Device? deviceExist = null; if (!string.IsNullOrEmpty(deviceDto.MacAddr)) { deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr); } if (deviceExist == null && deviceDto.SerialNumber != null) { deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber); } if (deviceExist == null && deviceDto.Uuid != null) { deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid); } if (deviceExist == null && deviceDto.Key != null) { deviceExist = await _deviceRepository.FindByKey(deviceDto.Key); } if (deviceExist == null) { deviceExist = ToEntity(deviceDto); deviceExist.CreatedAt = DateTime.UtcNow; deviceExist.UpdatedAt = DateTime.UtcNow; await _deviceRepository.InsertOneAsync(deviceExist); } else { await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto); } switch (deviceDto.DeviceType) { case DeviceType.Unknown: break; case DeviceType.Button: await ManageDeviceButton(deviceExist, deviceDto); break; } return deviceExist; } /// /// Handles device button actions by dispatching the configured action type (sending an observation or alarm) when both the device's configured action and the received click event are present. /// /// The existing device whose configured action settings determine which action to execute. /// The incoming device event payload providing the click type that triggers the action. private async Task ManageDeviceButton(Device deviceExist, DeviceDto deviceDto) { if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null) { switch (deviceExist.Settings.Action.Type) { case DeviceActionType.SendObs: await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds); break; case DeviceActionType.SendAlarm: await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds); break; } } } /// /// Sends a patient observation alarm based on a device action and the type of click event that triggered it. /// The alarm value is selected from the device action's configuration according to the click type /// (SingleClick, DoubleClick, or Hold) and is dispatched for each point of care that has an associated patient. /// The method exits early when the configuration observation cannot be found or when the action has no value defined for the current click type. /// /// The device action containing the configuration observation and the per-click-type alarm values to use. /// The click event that triggered the action, which determines which value from the device action is sent. /// The list of point of care identifiers whose patients should receive the alarm. private async Task SendAlarmOnAction( DeviceAction settingsAction, ClickType eventClickType, List deviceExistPointOfCareIds) { var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId); if(configObs == null) return; var obsData = new ObservationData { Code = configObs.Code, CodingSystem = configObs.CodingSystem, Time = DateTime.UtcNow, Text = configObs.Name, }; var obs = new PatientObservationAlarm { InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled}, MessageTime = DateTime.UtcNow, Persist = true, Code = obsData.Code, CodingSystem = obsData.CodingSystem, Name = configObs.Name, Time = DateTime.UtcNow }; foreach (var pocId in deviceExistPointOfCareIds) { var data = await _pointOfCareService.GetInfo(pocId, null, true); if (data != null && data.Patient?.Id != null) { obs.PatientId = data.Patient.Id; obs.Patient = data.Patient; switch (eventClickType) { case ClickType.SingleClick: if(settingsAction.ValueOnSingleClick == null) return; obs.Value = settingsAction.ValueOnSingleClick; break; case ClickType.DoubleClick: if(settingsAction.ValueOnDoubleClick == null) return; obs.Value = settingsAction.ValueOnDoubleClick; break; case ClickType.Hold: if(settingsAction.ValueOnHoldClick == null) return; obs.Value = settingsAction.ValueOnHoldClick; break; } // Process Obs on service await _alarmService.ProcessAlarmObservations(new List{obs},new List(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData); } } } /// /// Sends a patient observation derived from the configuration linked to a device action, applying the value /// associated with the specified click type (single, double, or hold) for each point of care patient found. /// The method short-circuits when the configuration observation is missing or when no value is defined for /// the given click type. /// /// The device action whose associated configuration observation and per-click values drive the observation payload. /// The type of click event that triggered the action; selects which value (single, double, or hold) is assigned to the observation. /// The list of point of care identifiers whose resolved patients will receive the generated observation. /// A task that represents the asynchronous send operation; no meaningful business result is returned. private async Task SendObservationOnAction( DeviceAction settingsAction, ClickType eventClickType, List deviceExistPointOfCareIds) { var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId); if(configObs == null) return; var obsData = new ObservationData { Code = configObs.Code, CodingSystem = configObs.CodingSystem, Time = DateTime.UtcNow, Text = configObs.Name, }; var obs = new PatientObservation { MessageTime = DateTime.UtcNow, Persist = true, Code = obsData.Code, CodingSystem = obsData.CodingSystem, Name = configObs.Name, Time = DateTime.UtcNow }; foreach (var pocId in deviceExistPointOfCareIds) { var data = await _pointOfCareService.GetInfo(pocId, null, true); if (data != null && data.Patient?.Id != null) { obs.PatientId = data.Patient.Id; obs.Patient = data.Patient; switch (eventClickType) { case ClickType.SingleClick: if(settingsAction.ValueOnSingleClick == null) return; obs.Value = settingsAction.ValueOnSingleClick; break; case ClickType.DoubleClick: if(settingsAction.ValueOnDoubleClick == null) return; obs.Value = settingsAction.ValueOnDoubleClick; break; case ClickType.Hold: if(settingsAction.ValueOnHoldClick == null) return; obs.Value = settingsAction.ValueOnHoldClick; break; } // Process Obs on service _observationService.ProcessObservations(new List{obs}, data.Patient, DateTime.UtcNow, obsData); } } } }