using System.Collections.Concurrent;
using System.Net;
using System.Text;
using System.Web;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Exceptions;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Infrastructure.Utils;
using adas_core.module.Relays.Devices;
using adas_core.module.Relays.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Quartz.Util;
namespace adas_core.Infrastructure.Services;
///
/// Provides a concrete implementation of the contract,
/// delivering relay-related functionality to consumers of the service.
///
///
/// This class is the default implementation of ,
/// and can be substituted via dependency injection where the interface is required.
///
public class RelayService : IRelayService
{
private readonly List _devices = [];
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly IPointOfCareService _pointOfCareService;
private readonly RelaySettings _relaySettings;
private readonly IRelayRepository _relayRepository;
private readonly ConcurrentDictionary, RelayEnum.Status> _relayWithStatus = new();
private readonly string _url;
private UriBuilder? _builder;
public RelayService(
ILogger logger,
IHttpClientFactory httpClientFactory,
IOptions relaySettings,
IPointOfCareService pointOfCareService,
IRelayRepository relayRepository)
{
_httpClientFactory = httpClientFactory;
_pointOfCareService = pointOfCareService;
_relayRepository = relayRepository;
_logger = logger;
_relaySettings = relaySettings.Value;
_url = _relaySettings.RecordingOrApiUrl ?? string.Empty;
Task.Run(async () => await InitRelayWithStatus());
}
///
/// Asynchronously determines the current status of a relay, preferring a cached value when available and falling back to a direct device read or an external endpoint when the device is unreachable.
/// Returns the cached status if caching is enabled, the cached value is present and not ; otherwise queries the relay device, or invokes the configured URL endpoint if the device cannot be obtained.
/// On failure, logs the error and resolves the status to .
///
/// The relay to check, identified by its IP, port, and relay number used both as the cache key and as the target of the status request.
/// A task that yields the resolved of the relay, or when the status cannot be determined.
public async Task CheckRelayStatus(Relay relay)
{
var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
try
{
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
if (_relayWithStatus.TryGetValue(cacheKey, out var status) &&
status != RelayEnum.Status.NotInitialized)
return status;
_logger.LogDebug("Relay status not found in cache: {Dct}",
DictionaryToString(_relayWithStatus));
}
RelayDevice? relayDevice = null;
try
{
relayDevice = GetRelayDevice(relay);
}
catch (Exception e)
{
_logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
}
if (relayDevice == null && !string.IsNullOrEmpty(_url)) return await GetRelayStatusByOr(relay);
if (relayDevice != null) return relayDevice.GetStatusRelay(relay.RelayNumber);
}
catch (Exception e)
{
_logger.LogError("Error CheckRelayStatus relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message,
e.StackTrace);
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = RelayEnum.Status.Unknown;
}
}
return RelayEnum.Status.Unknown;
}
///
/// Checks the status of a relay identified by its unique ID. Returns when the relay cannot be found, otherwise delegates to the relay-based overload to resolve the current status.
///
/// The unique identifier of the relay whose status should be checked.
/// The current of the relay, or if no relay with the given ID exists.
public async Task CheckRelayStatus(ObjectId relayId)
{
var relay = await _relayRepository.GetById(relayId);
if(relay == null) return RelayEnum.Status.Unknown;
return await CheckRelayStatus(relay);
}
///
/// Asynchronously powers off the specified relay. Uses a cache to avoid redundant operations when the relay is already off, falls back to an HTTP request when no local relay device is available, and updates the cache after a successful power off.
///
/// The relay to power off, including its network address and relay number.
public async Task PowerOff(Relay relay)
{
var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
if (_relayWithStatus.Any() &&
_relayWithStatus[cacheKey] == RelayEnum.Status.Off)
return;
}
RelayDevice? relayDevice = null;
try
{
relayDevice = GetRelayDevice(relay);
}
catch (Exception e)
{
_logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
}
if (relayDevice == null && !string.IsNullOrEmpty(_url))
{
_builder = new UriBuilder(_url)
{
Path = $"Relay/{relay.RelayNumber}/powerOff"
};
_logger.LogDebug("Send PowerOff relay {Relay}", relay);
await PowerRelay(relay, _builder);
}
relayDevice?.PowerOffRelay(relay.RelayNumber);
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = RelayEnum.Status.Off;
_logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
}
}
///
/// Powers on the specified relay. If the relay is configured with a URL, sends a remote power-on request; otherwise, drives the relay device directly. Honors a caching layer to avoid redundant power-on commands when the relay is already reported as on.
///
/// The relay to power on, including driver, IP, port, relay number, and cache settings.
public async Task PowerOn(Relay relay)
{
if (string.IsNullOrEmpty(relay.Driver) || string.IsNullOrEmpty(relay.Ip)) return;
var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
if (_relaySettings.Cache && relay.Cache)
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
if (_relayWithStatus.Any() &&
_relayWithStatus[cacheKey] == RelayEnum.Status.On)
return;
}
RelayDevice? relayDevice = null;
try
{
relayDevice = GetRelayDevice(relay);
}
catch (Exception e)
{
_logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
}
if (relayDevice == null && !string.IsNullOrEmpty(_url))
{
_builder = new UriBuilder(_url)
{
Path = $"Relay/{relay.RelayNumber}/powerOn"
};
_logger.LogDebug("Send PowerOn relay {Relay}", relay);
await PowerRelay(relay, _builder);
}
relayDevice?.PowerOnRelay(relay.RelayNumber);
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = RelayEnum.Status.On;
_logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
}
}
///
/// Sets the manual relay status for a point of care device. If the point of care exists and has a configured relay list, the relay matching the specified type is updated with the new status; otherwise, the operation is silently skipped.
///
/// The manual relay status to apply.
/// The identifier of the point of care device whose relay configuration should be updated.
/// The relay type used to look up the target relay within the point of care's relay configuration.
public async Task SetManualRelay(RelayEnum.Status status, ObjectId pocId, RelayEnum.Type type)
{
try
{
var poc = await _pointOfCareService.FindById(pocId);
if (poc is { Configuration.RelayIdList: not null })
{
var relay = _relayRepository.GetRelayByTypeInList(poc.Configuration.RelayIdList, type).FirstOrDefault();
if (relay != null) relay.ManualRelayStatus = status;
await _pointOfCareService.UpdateRelayConfig(poc);
_logger.LogDebug("SetManualRelay: {Status}, pocId: {Location}, bed: {Bed}", status, poc.Id, poc.Bed);
}
}
catch (Exception ex)
{
_logger.LogError("Error settings manual relay. Exception: {ex}", ex);
throw;
}
}
///
/// Retrieves a relay entity by its unique identifier from the repository.
/// Returns null when no matching relay is found.
///
/// The unique identifier of the relay to look up.
/// A task that resolves to the if found, or null if no relay matches the supplied identifier.
public Task GetById(ObjectId relay)
{
return _relayRepository.GetById(relay);
}
///
/// Retrieves a list of relays matching the specified list of relay identifiers. Returns an empty list when the provided identifier list is null.
///
/// The list of relay identifiers to look up. Can be null.
/// A list of objects corresponding to the provided identifiers, or an empty list if the input is null.
public List GetRelayInList(List? relayList)
{
if(relayList == null) return new List();
return _relayRepository.GetRelayInList(relayList);
}
///
/// Retrieves the relays of a specified type from the provided list of configuration relay identifiers. Returns an empty list when the input list is null, otherwise delegates the lookup to the relay repository.
///
/// The optional list of values identifying the configuration relays to filter; when null, the method short-circuits and returns an empty list.
/// The relay type used to filter the matching relays within the supplied list.
/// A containing the relays matching the specified , or an empty list if is null.
public List GetRelayByTypeInList(List? configurationRelayList, RelayEnum.Type type)
{
if(configurationRelayList == null) return new List();
return _relayRepository.GetRelayByTypeInList(configurationRelayList, type);
}
public async Task> GetPaginatedRelays(PaginationFilter filter)
{
var usedRelayIds = await _pointOfCareService.FindAllIdRelaysInUse();
var fluentQuery = _relayRepository.GetPaginatedRelays(filter);
if (filter.FilteredRequest?.InUse != null)
{
bool filterInUse = filter.FilteredRequest.InUse.Value;
var filterBuilder = Builders.Filter;
var idFilter = filterInUse
? filterBuilder.In(c => c.Id, usedRelayIds)
: filterBuilder.Not(filterBuilder.In(c => c.Id, usedRelayIds));
fluentQuery.Filter = filterBuilder.And(fluentQuery.Filter, idFilter);
}
var count = await fluentQuery.CountDocumentsAsync();
var data = await fluentQuery
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToListAsync();
if (data == null) return new PaginationResponse([], filter.PageNumber, filter.PageSize, count);
foreach (var camera in data)
{
if (camera == null) continue;
bool isInUse = usedRelayIds.Contains(camera.Id);
// Asignación mediante reflexión para el private set
camera.GetType().GetProperty(nameof(Relay.InUse))
?.SetValue(camera, isInUse);
}
return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count);
}
///
/// Inserts a new relay into the repository after verifying that no relay with the same name already exists.
///
/// The relay entity to insert, whose name is checked for duplicates prior to persistence.
/// The inserted entity returned by the repository, or null if the repository yields no result.
/// Thrown when a relay with the same name as already exists in the repository.
public async Task InsertRelay(Relay request)
{
var relayExist = await _relayRepository.GetByName(request.RelayName);
if(relayExist != null) throw new Exception($"Relay with name {request.RelayName} already exists");
return await _relayRepository.InsertOneRelayAsync(request);
}
///
/// Updates an existing relay identified by its unique ObjectId by delegating to the relay repository.
///
/// The unique identifier of the relay to update.
/// The relay object containing the updated information.
/// A task representing the asynchronous operation, containing the updated , or null if no relay with the specified identifier was found.
public Task UpdateRelayById(ObjectId objectId, Relay relay)
{
return _relayRepository.UpdateRelayAsync(objectId, relay);
}
public event EventHandler>? RelayStatusChanged;
private async Task InitRelayWithStatus()
{
try
{
var pocConfigList = await _pointOfCareService.GetAllConfigs();
var relayTasks = new List();
pocConfigList.ForEach(poc =>
{
if (poc.Configuration?.RelayList == null) return;
relayTasks.AddRange(from relayConf in poc.Configuration.RelayList
let statusTask = CheckRelayStatus(relayConf)
select statusTask.ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var cackeKey = Tuple.Create(relayConf.Ip, relayConf.Port, relayConf.RelayNumber);
if (!relayConf.Cache || !relayConf.Cache) return;
lock (_relayWithStatus)
{
_relayWithStatus[cackeKey] = task.Result;
}
}
else if (task.IsFaulted)
{
// Manejar la excepción si la llamada asincrónica falla
_logger.LogError("Error al obtener el estado del relé {RelayParsedIp}: {TaskException}",
relayConf.Ip, task.Exception.Message);
}
}));
});
await Task.WhenAll(relayTasks);
lock (_relayWithStatus)
{
_logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
}
}
catch (Exception ex)
{
Console.WriteLine("Exception setting relay with status: " + ex.Message);
}
}
///
/// Retrieves a for the given relay, returning a cached instance when available
/// or dynamically creating and registering a new one based on the relay's driver type. Returns null
/// when the relay has no IP address or its port is zero.
///
/// The relay configuration used to locate an existing device or instantiate a new one.
/// The existing or newly created , or null if the relay is missing a valid IP or port.
/// Thrown when no constructor matching and is found on the resolved driver type.
/// Thrown when the resolved driver type cannot be instantiated into a .
private RelayDevice? GetRelayDevice(Relay relay)
{
if(relay.Ip.IsNullOrWhiteSpace() || relay.Port == 0) return null;
var device = _devices.FirstOrDefault(d => d.Relay.Ip == relay.Ip && d.Relay.Port == relay.Port);
if (device != null) return device;
var type = TypesUtils.GetDriver("Relay", relay.Driver ?? string.Empty);
var constructor = type.GetConstructor([typeof(Relay), typeof(RelaySettings)]);
if (constructor == null) throw new AdasException("Constructor not found for relay device");
var relayDevice = (RelayDevice)constructor.Invoke([relay, _relaySettings]);
if (relayDevice == null) throw new AdasException("Relay device not found");
relayDevice.RelayStatusChanged += (_, outletId) =>
{
var cacheKey = Tuple.Create(relay.Ip, relay.Port, outletId);
var value = relayDevice.GetStatusRelay(outletId);
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = value;
}
_logger.LogDebug("Relay status changed for {Key} with value {Value}", cacheKey, value);
RelayStatusChanged?.Invoke(this, Tuple.Create(relayDevice, outletId));
};
_devices.Add(relayDevice);
return relayDevice;
}
///
/// Sends a power relay request by appending relay configuration details (driver, host, port, name, total count, and credentials) as query parameters to the provided URI and issuing an HTTP POST. Logs a debug message when the response status is not OK, and logs and swallows any exception encountered during the request.
///
/// The relay whose driver, IP, port, name, total count, username, and password are included in the request query string.
/// The URI builder whose query is updated with the relay parameters and whose resulting URI is used as the request target.
private async Task PowerRelay(Relay relay, UriBuilder builder)
{
var query = HttpUtility.ParseQueryString(builder.Query);
query["driver"] = relay.Driver;
query["host"] = relay.Ip;
query["port"] = relay.Port.ToString();
query["relayName"] = relay.RelayName;
query["relays"] = relay.Total.ToString();
query["username"] = relay.Username;
query["password"] = relay.Password;
builder.Query = query.ToString();
HttpRequestMessage request = new()
{
RequestUri = builder.Uri,
Method = HttpMethod.Post
};
try
{
using var client = _httpClientFactory.CreateClient();
var httpResponse = await client.SendAsync(request);
if (!httpResponse.StatusCode.Equals(HttpStatusCode.OK))
_logger.LogDebug("Send PowerRelay relay {Relay}", relay);
}
catch (Exception e)
{
_logger.LogError("Error PowerRelay relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message,
e.StackTrace);
}
}
///
/// Converts a concurrent dictionary of relay statuses into a human-readable string representation, formatting each entry as a line containing the device ID, port, and current status.
///
/// The concurrent dictionary containing relay status entries keyed by a tuple of device ID, port, and an additional integer value.
/// A string containing one formatted line per dictionary entry describing the device ID, port, and status.
private static string DictionaryToString(ConcurrentDictionary, RelayEnum.Status> dictionary)
{
var builder = new StringBuilder();
foreach (var pair in dictionary)
builder.AppendLine($"Device ID: {pair.Key.Item1}, Port: {pair.Key.Item2}, Status: {pair.Value}");
return builder.ToString();
}
///
/// Retrieves the current status of the specified relay by querying its external service endpoint, and caches the result (success or failure) when caching is enabled for both the application and the relay.
/// Falls back to when the HTTP response is not successful.
///
/// The relay instance whose status should be queried; its IP, port, relay number, driver, credentials, and mode are used to build the request.
/// The parsed returned by the remote relay service, or if the request fails.
private async Task GetRelayStatusByOr(Relay relay)
{
var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
_builder = new UriBuilder(_url)
{
Path = $"Relay/{relay.RelayNumber}/status"
};
var query = HttpUtility.ParseQueryString(_builder.Query);
query["driver"] = relay.Driver;
query["host"] = relay.Ip;
query["port"] = relay.Port.ToString();
query["relayName"] = relay.RelayName;
query["relays"] = relay.Total.ToString();
query["username"] = relay.Username;
query["password"] = relay.Password;
query["mode"] = relay.Mode.ToString();
query["refreshTime"] = "0";
_builder.Query = query.ToString();
HttpRequestMessage request = new()
{
RequestUri = _builder.Uri,
Method = HttpMethod.Get
};
using var client = _httpClientFactory.CreateClient();
client.Timeout = new TimeSpan(0, 0, 3);
var httpResponse = await client.SendAsync(request);
if (httpResponse.StatusCode.Equals(HttpStatusCode.OK))
{
var responseContent = httpResponse.Content;
var response = await responseContent.ReadAsStringAsync();
response = response.Replace("\"", "");
var relayStatusParsed = (RelayEnum.Status)Enum.Parse(typeof(RelayEnum.Status), response);
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = relayStatusParsed;
_logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
}
return relayStatusParsed;
}
if (_relaySettings.Cache && relay.Cache)
lock (_relayWithStatus)
{
_relayWithStatus[cacheKey] = RelayEnum.Status.Unknown;
_logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
}
return RelayEnum.Status.Unknown;
}
}