using System.Collections.Concurrent; using System.Globalization; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Xml.Linq; using Domain; using Infrastructure; namespace Application { public class ProcessarMedicoesUseCase { private readonly IPostgresRepository _postgresRepository; private readonly IAccessRepository _accessRepository; public ProcessarMedicoesUseCase( IPostgresRepository postgresRepository, IAccessRepository accessRepository) { _postgresRepository = postgresRepository; _accessRepository = accessRepository; } public async Task ExecuteAsync(DateTime dataIni, DateTime dataFim, string caminhoLog, CancellationToken ct) { var errosPersistentes = new ConcurrentBag(); var operacoesLog = new ConcurrentBag(); var perfis = (await _accessRepository.ObterPerfisAsync(ct)).ToList(); var endpoint = new Uri("https://servicos.ccee.org.br/ws/v2/MedidaCincoMinutosBSv2"); var datas = Enumerable.Range(0, (dataFim - dataIni).Days).Select(i => dataIni.AddDays(i)); await Parallel.ForEachAsync(perfis, async (perfil, ctPerfil) => { Console.WriteLine($"{DateTime.Now}: Iniciado ponto {perfil.CodigoSCDE}"); if (perfil.Codigo5Minutos == "0" || perfil.Codigo5Minutos == string.Empty || perfil.Codigo5Minutos == null) { Console.WriteLine($"Pular {perfil.CodigoSCDE} - (cod 5 min pendente)"); errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};{dataIni.ToOADate()};ERRO;cod_5min pendente"); return; } var existentes = await ObterMedicoesComRetry(perfil.CodigoSCDE, dataIni, dataFim, ctPerfil, errosPersistentes); // Paraleliza os dias deste perfil; o semáforo limita as requisições simultâneas await Parallel.ForEachAsync(datas, ctPerfil, async (dia, ctDia) => { try { await ProcessarDiaAsync(perfil, dia, existentes, endpoint, errosPersistentes, operacoesLog, ctDia); } catch (Exception ex) { operacoesLog.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};{dia.ToOADate()};ERRO;{ex.Message.Replace("\n", "-n-")}"); } }); Console.WriteLine($"{DateTime.Now}: Finalizado ponto {perfil.CodigoSCDE}"); }); // Cabeçalho do log var linhasLog = new List { "Perfil;Ponto;DiaNum;Status;Mensagem;Inseridos;Atualizados" }; linhasLog.AddRange(operacoesLog); linhasLog.AddRange(errosPersistentes); File.WriteAllLines(caminhoLog, linhasLog); } private async Task> ObterMedicoesComRetry( string codigoSCDE, DateTime dataIni, DateTime dataFim, CancellationToken ct, ConcurrentBag errosPersistentes) { int tentativas = 0; while (tentativas < 3) { try { return await _postgresRepository.ObterMedicoesAsync(codigoSCDE, dataIni, dataFim, ct); } catch (Exception ex) { tentativas++; if (tentativas >= 3) { errosPersistentes.Add($";{codigoSCDE};{dataIni.ToOADate()};Erro;{ex.Message.Replace("\n", "-n-")}"); throw; } int backoff = (int)Math.Pow(2, tentativas) * 1000; Console.WriteLine($"Erro ao acessar Postgres ({ex.Message}), tentativa {tentativas}. Aguardando {backoff / 1000}s..."); await Task.Delay(backoff, ct); } } return new Dictionary<(string, double, int), Medicao>(); } private async Task ProcessarDiaAsync( Perfil perfil, DateTime dia, IDictionary<(string, double, int), Medicao> existentes, Uri endpoint, ConcurrentBag errosPersistentes, ConcurrentBag operacoesLog, CancellationToken ct) { if (perfil.DataDeMigracao > dia) { Console.WriteLine($"Pular {perfil.CodigoSCDE} - {dia.ToShortDateString()} (antes da migração)"); errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};Fora da data de migração {perfil.DataDeMigracao} x {dia}"); return; } int tentativas = 0; bool sucesso = false; while (tentativas < 5 && !sucesso) { try { string payload = Xml_requisicao(dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, 1); var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml"); HttpResponseMessage response; string resposta; using (var client = CreateHttpClient()) { response = await client.PostAsync(endpoint, conteudo, ct); resposta = await response.Content.ReadAsStringAsync(); } if ((int)response.StatusCode >= 400) { try { SoapHelper.VerificarRespostaSOAP(resposta); } catch (SoapFaultException ex) { if (ex.ErrorCode == "2003") { // Aguarda o tick de janela SEM estar segurando o semáforo var now = DateTime.UtcNow; var delay = 60000 - (now.Second * 1000 + now.Millisecond); Console.WriteLine($"!! Limite de requisições atingido. Aguardando até {DateTime.Now.AddMilliseconds(delay)}"); await Task.Delay(delay, ct); continue; } if (ex.ErrorCode == "4001" || ex.ErrorCode == "2001") { errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};{dia.ToOADate()};SOAP Fault: {ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}"); break; } throw; } } await ProcessarXMLAsync(resposta, dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, existentes, ct, endpoint, 1, null, 1, operacoesLog); sucesso = true; } catch (Exception ex) { tentativas++; if (tentativas >= 5) { errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};{dia.ToOADate()};Erro;{ex.Message.Replace("\n", "-n-")}"); } else { int backoff = (int)Math.Pow(2.4, tentativas) * 1000; Console.WriteLine($"Erro na requisição ({ex.Message}), tentativa {tentativas}. Aguardando {backoff / 1000}s..."); await Task.Delay(backoff, ct); } } } } private async Task ProcessarXMLAsync( string xml, DateTime dia, string perfil, string ponto, IDictionary<(string, double, int), Medicao> existentes, CancellationToken ct, Uri endpoint, int paginaAtual = 1, List? acumulador = null, int totalPaginas = 1, ConcurrentBag? operacoesLog = null) { var doc = XDocument.Parse(xml); XNamespace ns = "http://xmlns.energia.org.br/BO/v2"; int.TryParse(doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "totalPaginas")?.Value, out totalPaginas); int.TryParse(doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "numero")?.Value, out paginaAtual); var medidas = doc.Descendants(ns + "medida") .Where(x => (string)x.Element(ns + "tipoEnergia") == "L"); acumulador ??= new List(); acumulador.AddRange(medidas); if (paginaAtual < totalPaginas) { // Próxima página: adquire o semáforo apenas para o HTTP e libera logo após string payload = Xml_requisicao(dia, perfil, ponto, paginaAtual + 1); var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml"); string proxXml; using (var client = CreateHttpClient()) { using var resp = await client.PostAsync(endpoint, conteudo, ct); proxXml = await resp.Content.ReadAsStringAsync(); } await ProcessarXMLAsync(proxXml, dia, perfil, ponto, existentes, ct, endpoint, paginaAtual + 1, acumulador, totalPaginas, operacoesLog); return; } var medidasProcessadas = acumulador .Select(m => { string origem = m.Element(ns + "coletaMedicao")?.Element(ns + "tipo")?.Element(ns + "nome")?.Value ?? ""; string pontoMed = m.Element(ns + "medidor")?.Element(ns + "codigo")?.Value ?? ""; DateTime data = DateTime.Parse(m.Element(ns + "data")?.Value ?? ""); double diaNum = (data.ToOADate() - data.ToOADate() % 1); int minuto = data.Hour * 60 + data.Minute; if (minuto == 0) { minuto = 1440; diaNum--; } double.TryParse(m.Element(ns + "energiaAtiva")?.Element(ns + "consumo")?.Element(ns + "valor")?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double ativa_c); double.TryParse(m.Element(ns + "energiaAtiva")?.Element(ns + "geracao")?.Element(ns + "valor")?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double ativa_g); double.TryParse(m.Element(ns + "energiaReativa")?.Element(ns + "consumo")?.Element(ns + "valor")?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double reat_c); double.TryParse(m.Element(ns + "energiaReativa")?.Element(ns + "geracao")?.Element(ns + "valor")?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double reat_g); return new Medicao( pontoMed, diaNum, minuto, origem, ativa_c, ativa_g, reat_c, reat_g ); }) .ToList(); var minutosEsperados = new[] { 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 }; var medidasPorHora = medidasProcessadas .GroupBy(m => new { m.Ponto, m.DiaNum, Hora = (m.Minuto - 5) / 60 }) .ToList(); var medidasComEstimativa = new List(); for (int hora = 0; hora < 24; hora++) { var grupoHora = medidasPorHora.Where(h => h.Key.Hora == hora).ToList(); var lista = grupoHora.SelectMany(g => g).OrderBy(m => m.Minuto).ToList(); // Separar por origem var logicas = lista.Where(m => m.Origem == "Inspeção Lógica").ToList(); var diarias = lista.Where(m => m.Origem == "Coleta Diária").ToList(); // Regra de prioridade List selecionados; if (logicas.Count == 12 || logicas.Count > diarias.Count) { selecionados = logicas; } else { selecionados = diarias; } var minutosPresentes = selecionados.Select(m => m.Minuto).ToHashSet(); var minutosEsperadosAbsolutos = minutosEsperados.Select(m => m + (60 * hora)).ToList(); var faltantes = minutosEsperadosAbsolutos.Except(minutosPresentes).OrderBy(m => m).ToList(); var estimadas = new List(); foreach (var faltante in faltantes) { if (faltantes.Count > 3) { // Se mais de 3 faltantes na hora, não faz estimativa var estimada = new Medicao( ponto + "P", (dia.ToOADate() - dia.ToOADate() % 1), faltante, "Faltante", null, null, null, null ); estimadas.Add(estimada); continue; } else { var ativaConsumo = selecionados.Average(m => m.AtivaConsumo); var ativaGeracao = selecionados.Average(m => m.AtivaGeracao); var reativaConsumo = selecionados.Average(m => m.ReativaConsumo); var reativaGeracao = selecionados.Average(m => m.ReativaGeracao); var estimada = new Medicao( grupoHora.First().Key.Ponto, grupoHora.First().Key.DiaNum, faltante, "Estimado", ativaConsumo, ativaGeracao, reativaConsumo, reativaGeracao ); estimadas.Add(estimada); } } // Adiciona todos (originais + estimados) ao resultado final medidasComEstimativa.AddRange(selecionados); medidasComEstimativa.AddRange(estimadas); } var novos = new List(); var alterados = new List(); foreach (var m in medidasComEstimativa) { var chave = (m.Ponto, m.DiaNum, m.Minuto); if (!existentes.TryGetValue(chave, out var existente)) { novos.Add(m); } else { if (existente.Origem != m.Origem || Math.Round(existente.AtivaConsumo ?? 0, 10) != Math.Round(m.AtivaConsumo ?? 0, 10) || Math.Round(existente.AtivaGeracao ?? 0, 10) != Math.Round(m.AtivaGeracao ?? 0, 10) || Math.Round(existente.ReativaConsumo ?? 0, 10) != Math.Round(m.ReativaConsumo ?? 0, 10) || Math.Round(existente.ReativaGeracao ?? 0, 10) != Math.Round(m.ReativaGeracao ?? 0, 10)) { alterados.Add(m); } } } if (novos.Any()) { await _postgresRepository.InserirMedicoesAsync(novos, ct); Console.WriteLine($"Inserido {novos.Count} registros. Ponto {ponto}. Dia {dia}"); operacoesLog?.Add($"{perfil};{ponto};{dia.ToOADate()};OK;Novos;{novos.Count};0"); } if (alterados.Any()) { await _postgresRepository.AtualizarMedicoesAsync(alterados, ct); Console.WriteLine($"Atualizado {alterados.Count} registros. Ponto {ponto}. Dia {dia}"); operacoesLog?.Add($"{perfil};{ponto};{dia.ToOADate()};OK;Atualizados;0;{alterados.Count}"); } if (!novos.Any() && !alterados.Any()) { Console.WriteLine($"Nenhuma alteração. Ponto {ponto}. Dia {dia}"); operacoesLog?.Add($"{perfil};{ponto};{dia.ToOADate()};OK;Sem alterações;0;0"); } } private static string Xml_requisicao(DateTime data_req, string perfil, string cod_ponto, int pagina) { string cam_ent, tex_req, sdat_req; cam_ent = @"X:\Back\Plataforma de Integração CCEE\RequestPaginate.txt"; cod_ponto += "P"; sdat_req = data_req.ToString("yyyy-MM-ddT00:00:00"); tex_req = File.ReadAllText(cam_ent); tex_req = tex_req.Replace("DATAALTERADA", sdat_req); tex_req = tex_req.Replace("PONTOMEDICAO", cod_ponto); tex_req = tex_req.Replace("CODPERFIL", perfil); tex_req = tex_req.Replace("PAGNUM", pagina.ToString()); return tex_req; } private static HttpClient CreateHttpClient() { // Configura o HttpClientHandler para ignorar erros SSL var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true }; // Adiciona o certificado SSL handler.ClientCertificates.Add(new X509Certificate2(@"X:\Back\APP Smart\Certificado\cert_ssl.pfx", "appsmart")); return new HttpClient(handler); } } }