Files
2026-06-26 10:29:23 +02:00

365 lines
16 KiB
C#

using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Utils;
using HtmlAgilityPack;
using Serilog;
namespace adas_core.module.LightBeacons.Devices;
/// <summary>
/// This class represents a light beacon device that can be controlled via HTTP requests. It retrieves the current color of the beacon by parsing HTML pages and sends commands to change the beacon's color.
/// The class includes error handling and retry logic for network operations, and it can be configured to emulate the beacon for testing purposes.
/// </summary>
public class Turktbens2LightBeacon : LightBeacon
{
/// <summary>
/// The _client field is an instance of HttpClient that is used to send HTTP requests to the beacon's web interface. It is initialized with a timeout value specified in the options, and it is disposed of after use to free up resources.
/// </summary>
private readonly HttpClient _client;
/// <summary>
/// The _code field is a constant integer that represents a base code used in the construction of the HTTP request body when sending commands to the beacon.
/// It is combined with specific port and color information to form the complete command sent to the beacon's web interface.
/// </summary>
private readonly int _code = 75;
/// <summary>
/// The _emulate field is a boolean that indicates whether the beacon should operate in emulation mode.
/// When set to true, the GetBeaconColor method will return Off without attempting to access the web interface, allowing for testing and development without requiring a physical beacon device.
/// </summary>
private readonly bool _emulate;
/// <summary>
/// The _logger field is an instance of ILogger from the Serilog library, used for logging information, warnings, and errors throughout the class.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// The _password field is a string that holds the password used for authentication when sending commands to the beacon's web interface.
/// </summary>
private readonly string _password = "password";
/// <summary>
/// The _port field is an integer that specifies which port of the beacon is being controlled. The beacon has multiple ports, and this field determines which one is targeted for color changes and status retrieval.
/// </summary>
private readonly int _port = 1;
/// <summary>
/// The _portMappings dictionary maps each port number to the corresponding indices of the checkbox inputs in the HTML page that represent the output states for that port.
/// </summary>
private readonly Dictionary<int, int[]> _portMappings = new()
{
{ 1, [0, 1] },
{ 2, [2, 3] },
{ 3, [4, 5] },
{ 4, [6, 7] }
};
/// <summary>
/// The _ports dictionary maps each port number to an array of strings that represent the specific parameters used in the HTTP request body for controlling the beacon's color.
/// </summary>
private readonly Dictionary<int, string[]> _ports = new()
{
{ 1, ["|1|1|1|2", "|12|0|1|0", "|1|0|1|2"] },
{ 2, ["|1|3|1|2", "|12|1|1|0", "|1|2|1|2"] },
{ 3, ["|1|5|1|2", "|12|2|1|0", "|1|4|1|2"] },
{ 4, ["|1|7|1|2", "|12|3|1|0", "|1|6|1|2"] }
};
/// <summary>
/// The _timeout field is an integer that specifies the timeout duration in seconds for HTTP requests made to the beacon's web interface. If a request takes longer than this duration, it will be aborted and an error will be logged.
/// </summary>
private readonly int _timeout = 30;
/// <summary>
/// The _url field is a string that holds the base URL of the beacon's web interface. This URL is used to construct the full endpoints for retrieving the beacon's status and sending commands to change its color.
/// </summary>
private readonly string _url;
/// <summary>
/// The constructor of the Turktbens2LightBeacon class initializes a new instance of the class with the specified options.
/// </summary>
/// <param name="options">A dictionary containing the configuration options for the beacon.</param>
/// <exception cref="ArgumentException">Thrown when required options are missing or invalid.</exception>
public Turktbens2LightBeacon(EquatableDictionary<string, object> options) : base(options)
{
_logger = Log.ForContext<Turktbens2LightBeacon>();
if (options == null) throw new ArgumentException("TURKTBENS2Beacon has no options");
if (!options.TryGetValue("url", out var url))
throw new ArgumentException("TURKTBENS2Beacon has no valid url option");
if (url == null) throw new ArgumentException("TURKTBENS2Beacon has no valid url option");
_url = url.ToString()!;
if (options.TryGetValue("password", out var password)) _password = password.ToString() ?? "password";
if (options.TryGetValue("port", out var port)) _port = Convert.ToInt32(port);
if (options.TryGetValue("timeout", out var timeout)) _timeout = Convert.ToInt32(timeout);
if (options.TryGetValue("code", out var code)) _code = Convert.ToInt32(code);
if (options.TryGetValue("emulate", out var emulate)) _emulate = Convert.ToBoolean(emulate);
_client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(_timeout)
};
}
/// <summary>
/// GetByCodeSysAndCode beacon color downloading the html of the web. and using AgilityPackHtml to access.
/// The web is bugged and sometimes you are logged or not. The information is in two urls, the color is formed by 3
/// bits, Vaux html changes if logged/not logged
/// returns OFF when some error appears or try to parse the bits to BalizaColors enum
/// </summary>
/// <returns>The current color of the beacon as a LightBeaconColor enum value.</returns>
public override async Task<LightBeaconColor> GetBeaconColor()
{
if (_emulate)
return LightBeaconColor.Off;
var httpClient = new HttpClient();
try
{
//we dont need to login to check status
var vauxValue = 0;
var output1Value = 0;
var output2Value = 0;
var outputUri = new Uri($"{_url}/IO01_03.html");
var vauxUri = new Uri($"{_url}/IO12_03.html");
var outputs = await httpClient.GetAsync(outputUri);
var vauxs = await httpClient.GetAsync(vauxUri);
if (!outputs.IsSuccessStatusCode || !vauxs.IsSuccessStatusCode)
return LightBeaconColor.Off;
var outputsHtml = new HtmlDocument();
outputsHtml.LoadHtml(await outputs.Content.ReadAsStringAsync());
var vauxHtml = new HtmlDocument();
vauxHtml.LoadHtml(await vauxs.Content.ReadAsStringAsync());
//GetByCodeSysAndCode Vaux info from vauxUri can be on off and we translate it to 0,1
var trElements = vauxHtml.DocumentNode.SelectNodes("//tr");
if (trElements == null || trElements.Count < _port)
return LightBeaconColor.Off;
var portNode = trElements[_port - 1];
var tdNodes = portNode.SelectNodes(".//td");
var vauxResult = tdNodes[1];
switch (vauxResult.InnerText)
{
case "on":
vauxValue = 1;
break;
//sometimes it display a select option when logged and other times its a label. this case is for option, take selected option
case "offon":
{
var selected = vauxResult.SelectSingleNode("select").SelectSingleNode("option[@selected]");
if (selected.InnerText.Equals("on")) vauxValue = 1;
break;
}
}
//GetByCodeSysAndCode outputs info
var checkboxElements = outputsHtml.DocumentNode.SelectNodes("//input[@type='checkbox']");
if (checkboxElements == null)
return LightBeaconColor.Off;
var portsParsed = _portMappings.TryGetValue(_port, out var mappedValues);
if (!portsParsed) return LightBeaconColor.Off;
var output1 = checkboxElements[mappedValues![0]];
var output2 = checkboxElements[mappedValues[1]];
if (output1.Attributes["checked"] != null) output1Value = 1;
if (output2.Attributes["checked"] != null) output2Value = 1;
var finalValue = $"{output2Value}{vauxValue}{output1Value}";
var parsed = Enum.TryParse(finalValue, out LightBeaconColor parsedColor);
return parsed ? parsedColor : LightBeaconColor.Off;
}
catch (Exception e)
{
_logger.Error(
"Exception trying to get beacon color for beacon: {url} port: {port} ex: {eMessage}, trace: {eStackTrace}",
_url, _port, e.Message, e.StackTrace);
return LightBeaconColor.Off;
}
finally
{
httpClient.Dispose();
}
}
/// <summary>
/// The BlueCode method sends a command to the beacon's web interface to change the beacon's color to blue.
/// It constructs the appropriate HTTP request body using the predefined code and port mappings, and it includes error handling to log any exceptions that occur during the process.
/// If an error occurs, it logs the error message and rethrows the exception to be handled by the caller.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public override async Task BlueCode()
{
try
{
await Send(ColorsCodes.Blue);
}
catch (Exception e)
{
_logger.Error("An error occurred while sending the Blue code: {Message}", e.Message);
throw;
}
}
/// <summary>
/// The RedCode method sends a command to the beacon's web interface to change the beacon's color to red.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public override async Task RedCode()
{
try
{
await Send(ColorsCodes.Red);
}
catch (Exception e)
{
_logger.Error("An error occurred while sending the Red code: {Message}", e.Message);
throw;
}
}
/// <summary>
/// The YellowCode method sends a command to the beacon's web interface to change the beacon's color to yellow.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public override async Task YellowCode()
{
try
{
await Send(ColorsCodes.Yellow);
}
catch (Exception e)
{
_logger.Error("An error occurred while sending the Yellow code: {Message}", e.Message);
throw;
}
}
/// <summary>
/// The GreenCode method sends a command to the beacon's web interface to change the beacon's color to green.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public override async Task PowerOffLed()
{
try
{
await Send(ColorsCodes.Off);
}
catch (Exception e)
{
_logger.Error("An error occurred while sending the power off code: {Message}", e.Message);
throw;
}
}
/// <summary>
/// The GenerateColorAlert method is not implemented in this class. It is intended to generate a color alert based on the provided PatientObservation, but the specific implementation details are not defined in this class and will need to be implemented in a subclass or by the caller.
/// </summary>
/// <param name="obs">The patient observation based on which the color alert should be generated.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="NotImplementedException"></exception>
public override Task GenerateColorAlert(PatientObservation obs)
{
throw new NotImplementedException();
}
/// <summary>
/// The Send method is a private helper method that constructs and sends an HTTP request to the beacon's web interface to change the beacon's color based on the provided ColorsCodes enum value.
/// </summary>
/// <param name="colorCodes">The color code to be sent to the beacon.</param>
/// <returns>A task representing the asynchronous operation.</returns>
private async Task Send(ColorsCodes colorCodes)
{
var clr = ((int)colorCodes).ToString().PadLeft(3, '0');
var passwordEncoded = Uri.EscapeDataString(_password);
var body = new Dictionary<string, string>
{
{ "access_password", passwordEncoded },
{ _code + _ports[_port][0], clr[0].ToString() },
{ _code + _ports[_port][1], clr[1].ToString() },
{ _code + _ports[_port][2], clr[2].ToString() }
};
if (!_emulate)
try
{
await SendMessage(body);
}
catch (Exception e)
{
_logger.Error("Failed to send color codes: {errorMessage}", e.Message);
throw;
}
}
/// <summary>
/// The SendMessage method is a private helper method that sends an HTTP POST request to the beacon's web interface with the specified body parameters.
/// It includes retry logic to handle transient network errors, attempting to resend the request up to a maximum number of retries with exponential backoff between attempts.
/// If the request fails after all retry attempts, it logs an error message and rethrows the exception.
/// </summary>
/// <param name="body">The body parameters to be sent in the HTTP POST request.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="InvalidOperationException"></exception>
protected async Task SendMessage(Dictionary<string, string> body)
{
if (string.IsNullOrEmpty(_url)) throw new InvalidOperationException("URL is not set.");
var retryCount = 0;
const int maxRetries = 3; // Número máximo de intentos
var delayMilliseconds = 500; // Tiempo de espera inicial entre reintentos
while (retryCount < maxRetries)
try
{
var uri = new Uri(_url);
var content = new FormUrlEncodedContent(body);
var b = await content.ReadAsStringAsync();
_logger.Debug("Sending to {url} body {b}", _url, b);
var response = await _client.PostAsync(uri, content);
if (!response.IsSuccessStatusCode)
throw new Exception(
$"HTTP error: {response.StatusCode} while sending message to {_url} with body {b}");
break;
}
catch (Exception e)
{
retryCount++;
_logger.Warning(
"Error sending to url: {url}, message: {eMessage}, stack trace: {eStackTrace}, attempt: {retryCount}",
_url, e.Message, e.StackTrace, retryCount);
if (retryCount >= maxRetries)
{
_logger.Error("Max retry attempts reached. Failing operation.");
throw;
}
await Task.Delay(delayMilliseconds);
delayMilliseconds *= 2; // Incrementa el tiempo de espera para cada reintento
}
}
/// <summary>
/// The ColorsCodes enum defines the binary codes corresponding to each color that the beacon can display.
/// Each color is represented by a three-bit binary code, where each bit corresponds to a specific output state of the beacon.
/// The enum values are used in the Send method to construct the appropriate command for changing the beacon's color.
/// </summary>
private enum ColorsCodes
{
Off = 000,
Red = 001,
Yellow = 011,
Green = 010,
Cyan = 110,
Blue = 100,
Magenta = 101,
White = 111
}
}