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; public class Turktbens2LightBeacon : LightBeacon { private readonly HttpClient _client; private readonly int _code = 75; private readonly bool _emulate; private readonly ILogger _logger; private readonly string _password = "password"; private readonly int _port = 1; private readonly Dictionary _portMappings = new() { { 1, [0, 1] }, { 2, [2, 3] }, { 3, [4, 5] }, { 4, [6, 7] } }; private readonly Dictionary _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"] } }; private readonly int _timeout = 30; private readonly string _url; public Turktbens2LightBeacon(EquatableDictionary options) : base(options) { _logger = Log.ForContext(); 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) }; } /// /// 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 /// /// public override async Task 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(); } } 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; } } 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; } } 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; } } 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; } } public override Task GenerateColorAlert(PatientObservation obs) { throw new NotImplementedException(); } private async Task Send(ColorsCodes colorCodes) { var clr = ((int)colorCodes).ToString().PadLeft(3, '0'); var passwordEncoded = Uri.EscapeDataString(_password); var body = new Dictionary { { "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; } } protected async Task SendMessage(Dictionary 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 } } private enum ColorsCodes { Off = 000, Red = 001, Yellow = 011, Green = 010, Cyan = 110, Blue = 100, Magenta = 101, White = 111 } }