using System.Collections.Concurrent; using System.Text; using System.Xml.Linq; using Domain; using Infrastructure; using System.Globalization; namespace Application { public class ProcessarMedicoesUseCase { private readonly IPostgresRepository _postgresRepository; private readonly IAccessRepository _accessRepository; private readonly HttpClient _httpClient; private readonly RateLimiter _rateLimiter; public ProcessarMedicoesUseCase( IPostgresRepository postgresRepository, IAccessRepository accessRepository, HttpClient httpClient, RateLimiter rateLimiter) { _postgresRepository = postgresRepository; _accessRepository = accessRepository; _httpClient = httpClient; _rateLimiter = rateLimiter; } public async Task ExecuteAsync(DateTime dataIni, DateTime dataFim, string caminhoLog, CancellationToken ct) { var errosPersistentes = new ConcurrentBag(); var perfis = (await _accessRepository.ObterPerfisAsync(ct)).ToList(); _httpClient.DefaultRequestHeaders.Add("SOAPAction", "listarMedidaCincoMinutos"); 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, ct) => { try { Console.WriteLine($"{DateTime.Now}: Iniciado ponto {perfil.CodigoSCDE}"); if (perfil.Codigo5Minutos == "0" || perfil.Codigo5Minutos == string.Empty) { Console.WriteLine($"Pular {perfil.CodigoSCDE} - (cod 5 min pendente)"); errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE}; cod_5min pendente"); return; } var existentes = (await _postgresRepository.ObterMedicoesAsync(perfil.CodigoSCDE, dataIni, dataFim, ct)); foreach (DateTime dia in datas) { int tentativas = 0; bool sucesso = false; while (tentativas < 5 && !sucesso) { 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}"); break; // não tentar antes da data de migração } try { string payload = Xml_requisicao(dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, 1); var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml"); await _rateLimiter.WaitAsync(ct); using var response = await _httpClient.PostAsync(endpoint, conteudo, ct); string resposta = await response.Content.ReadAsStringAsync(); if ((int)response.StatusCode >= 400) { try { SoapHelper.VerificarRespostaSOAP(resposta); } catch (SoapFaultException ex) { if (ex.ErrorCode == "2003") // limite de requisições { 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); // tentar de novo sem contar como falha continue; } if (ex.ErrorCode == "4001") // Dados não encontrados { errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};SOAP Fault: {ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}"); break; } if (ex.ErrorCode == "2001") // Sem acesso { errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};SOAP Fault: {ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}"); break; } else { throw; } } } await ProcessarXMLAsync(resposta, dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, existentes, ct, 1); sucesso = true; } catch (Exception ex) { tentativas++; if (tentativas >= 5) { errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};Erro;{ex.Message.Replace("\n", "-n-")}"); } else { int backoff = (int)Math.Pow(2.4, tentativas) * 1000; // exponencial Console.WriteLine($"Erro na requisição ({ex.Message}), tentativa {tentativas}. Aguardando {backoff / 1000}s..."); await Task.Delay(backoff); } } } } Console.WriteLine($"{DateTime.Now}: Finalizado ponto {perfil.CodigoSCDE}"); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } }); if (errosPersistentes.Count > 0) { File.WriteAllLines(caminhoLog, new[] { "Perfil;Ponto;Status;Message" }.Concat(errosPersistentes)); } } private async Task ProcessarXMLAsync( string xml, DateTime dia, string perfil, string ponto, IDictionary<(string, double, int), Medicao> existentes, CancellationToken ct, int paginaAtual = 1, List? acumulador = null, int totalPaginas = 1) { 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) { // Requisita próxima página string payload = Xml_requisicao(dia, perfil, ponto, paginaAtual + 1); var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml"); await _rateLimiter.WaitAsync(ct); using var resp = await _httpClient.PostAsync("https://servicos.ccee.org.br/ws/v2/MedidaCincoMinutosBSv2", conteudo, ct); string proxXml = await resp.Content.ReadAsStringAsync(); await ProcessarXMLAsync(proxXml, dia, perfil, ponto, existentes, ct, paginaAtual + 1, acumulador, totalPaginas); 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 ); }) .GroupBy(x => new { x.Ponto, x.DiaNum, x.Minuto }) .Select(g => { var logica = g.FirstOrDefault(x => x.Origem == "Inspeção Lógica"); return logica ?? g.First(); }); 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(); foreach (var grupoHora in medidasPorHora) { var lista = grupoHora.OrderBy(m => m.Minuto).ToList(); var minutosPresentes = lista.Select(m => m.Minuto).ToHashSet(); var minutosEsperadosAbsolutos = minutosEsperados.Select(m => m + (60 * grupoHora.Key.Hora)).ToList(); var faltantes = minutosEsperadosAbsolutos.Except(minutosPresentes).OrderBy(m => m).ToList(); // Use apenas valores reais para interpolação var reais = lista.ToDictionary(m => m.Minuto, m => m); // Identifique sequências de minutos faltantes consecutivos var sequencias = new List>(); List atual = null; int? ultimo = null; foreach (var min in faltantes) { if (atual == null || ultimo == null || min != ultimo + 5) { atual = new List(); sequencias.Add(atual); } atual.Add(min); ultimo = min; } var estimadas = new List(); foreach (var seq in sequencias) { int minIni = seq.First(); int minFim = seq.Last(); // Busca anterior real var anterior = reais.Values.Where(m => m.Minuto < minIni).OrderByDescending(m => m.Minuto).FirstOrDefault(); // Busca posterior real var posterior = reais.Values.Where(m => m.Minuto > minFim).OrderBy(m => m.Minuto).FirstOrDefault(); foreach (var minFaltante in seq) { var ativaConsumo = Interpolar(anterior?.Minuto, anterior?.AtivaConsumo, posterior?.Minuto, posterior?.AtivaConsumo, minFaltante) ?? 0; var ativaGeracao = Interpolar(anterior?.Minuto, anterior?.AtivaGeracao, posterior?.Minuto, posterior?.AtivaGeracao, minFaltante) ?? 0; var reativaConsumo = Interpolar(anterior?.Minuto, anterior?.ReativaConsumo, posterior?.Minuto, posterior?.ReativaConsumo, minFaltante) ?? 0; var reativaGeracao = Interpolar(anterior?.Minuto, anterior?.ReativaGeracao, posterior?.Minuto, posterior?.ReativaGeracao, minFaltante) ?? 0; var estimada = new Medicao( grupoHora.Key.Ponto, grupoHora.Key.DiaNum, minFaltante, "Estimado", ativaConsumo, ativaGeracao, reativaConsumo, reativaGeracao ); estimadas.Add(estimada); } } // Adiciona todos (originais + estimados) ao resultado final medidasComEstimativa.AddRange(lista); 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 || existente.AtivaConsumo != m.AtivaConsumo || existente.AtivaGeracao != m.AtivaGeracao || existente.ReativaConsumo != m.ReativaConsumo || existente.ReativaGeracao != m.ReativaGeracao) { alterados.Add(m); } } } if (novos.Any()) { await _postgresRepository.InserirMedicoesAsync(novos, ct); Console.WriteLine($"Inserido {novos.Count} registros. Ponto {ponto}. Dia {dia}"); } if (alterados.Any()) { await _postgresRepository.AtualizarMedicoesAsync(alterados, ct); Console.WriteLine($"Atualizado {alterados.Count} registros. Ponto {ponto}. Dia {dia}"); } } 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 double? Interpolar( double? xAnterior, double? yAnterior, double? xPosterior, double? yPosterior, double xProcurado) { if (xAnterior.HasValue && yAnterior.HasValue && (!xPosterior.HasValue || !yPosterior.HasValue)) { return yAnterior.Value; } if (xPosterior.HasValue && yPosterior.HasValue && (!xAnterior.HasValue || !yAnterior.HasValue)) { return yPosterior.Value; } if (xAnterior.HasValue && yAnterior.HasValue && xPosterior.HasValue && yPosterior.HasValue) { if (xPosterior.Value == xAnterior.Value) throw new ArgumentException("xAnterior e xPosterior não podem ser iguais (divisão por zero)."); double yProcurado = yAnterior.Value + ((yPosterior.Value - yAnterior.Value) / (xPosterior.Value - xAnterior.Value)) * (xProcurado - xAnterior.Value); return yProcurado; } return null; } } }