using System.Collections.Concurrent; using System.Data; using System.Data.OleDb; using System.Globalization; using System.Text; using System.Xml.Linq; using Npgsql; internal class Plat_integ { static string PG_CONN_STRING_PROD = "Server = smart-energia-dev-pgsql.cykff7tj7mik.us-east-1.rds.amazonaws.com; Port = 5432; Database = smartenergiaprod; Username = postgres; Password = VfHml#Z78!%kvvNM; Timeout = 60; CommandTimeout = 60; ApplicationName = new_med_5_min; Connection Lifetime = 120; Minimum Pool Size = 2; Maximum Pool Size = 2;"; static readonly HttpClient client = new HttpClient(new HttpClientHandler { ClientCertificateOptions = ClientCertificateOption.Automatic, //Proxy = new WebProxy("127.0.0.1", 8888), //UseProxy = true, //ServerCertificateCustomValidationCallback = (HttpRequestMessage req, X509Certificate2? cert, X509Chain? chain, SslPolicyErrors errors) => true }); static async Task Main() { Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); DateTime inicio = DateTime.Now; string caminhoBD = @"\\srv-dados\documentos\Middle\Informativo Setorial\Modelo Word\BD1_dados cadastrais e faturas.accdb"; string caminhoLog = $@"\\srv-dados\documentos\Back\Carteira x.x\Codigo\Erros\log_erros_{DateTime.Now:MM_dd_HH_mm}.csv"; DateTime now = DateTime.Now; //DateTime dataIni = new DateTime(now.Year, now.Month, 1); //DateTime dataFim = new DateTime(now.Year, now.Month, now.Day); DateTime dataIni = new DateTime(now.Year, 7, 1); DateTime dataFim = new DateTime(now.Year, 8, 1); var perfis = Busca_dad_BD(caminhoBD, dataIni); await ProcessarMedicoesAsync(dataIni, dataFim, perfis, caminhoLog); Console.WriteLine($"Concluído. Tempo total: {DateTime.Now - inicio}"); Console.ReadKey(); } public static List Busca_dad_BD(string caminho_BD, DateTime dataIni) { var lista = new List(); string query = $"SELECT Cod_5min, Codigo_SCDE, Data_de_Migracao FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 AND Unidade_gerenciada ORDER BY cod_smart_unidade"; //string query = "SELECT Cod_5min, Codigo_SCDE FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 and (Cliente = 'RMC ALIMENTOS' OR Cliente = 'FERREIRA SUPERMERCADO' OR Cliente = 'VANGUARDA ALIMENTOS') AND Unidade_gerenciada ORDER BY PerfilCCEE"; //string query = "SELECT Cod_5min, Codigo_SCDE FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 and Cliente = 'ALMAVIVA' and Unidade = 'GUARULHOS' AND unidadade_gerenciada ORDER BY PerfilCCEE"; //string query = "SELECT Cod_5min, Codigo_SCDE FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 and Cliente = 'bramagran' AND Unidade_gerenciada ORDER BY PerfilCCEE"; using (var connection = new OleDbConnection($"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={caminho_BD};Jet OLEDB:Database Password=gds21")) { using var cmd = new OleDbCommand(query, connection); connection.Open(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { lista.Add( new perfil( reader["Cod_5min"].ToString(), reader["Codigo_SCDE"].ToString(), DateTime.Parse(reader["Data_de_Migracao"].ToString()) ) ); } } return lista; } private static async Task ProcessarMedicoesAsync(DateTime dataIni, DateTime dataFim, List perfis, string caminhoLog) { var limiter = new RateLimiter(400, TimeSpan.FromMinutes(1)); var errosPersistentes = new ConcurrentBag(); await using var dataSource = NpgsqlDataSource.Create(PG_CONN_STRING_PROD); client.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._Codigo_SCDE}"); if (perfil._Cod_5min == "0" || perfil._Cod_5min == string.Empty) { Console.WriteLine($"Pular {perfil._Codigo_SCDE} - (cod 5 min pendente)"); errosPersistentes.Add($"{perfil._Cod_5min};{perfil._Codigo_SCDE}; cod_5min pendente"); return; } string sqlSelect = @" SELECT ponto, dia_num, minuto, origem, ativa_consumo, ativa_geracao, reativa_consumo, reativa_geracao FROM med_5min WHERE ponto = @ponto AND dia_num >= @data_ini AND dia_num < @data_fim; "; var existentes = new Dictionary<(string, double, int), dynamic>(); await using (var command = dataSource.CreateCommand(sqlSelect)) { command.Parameters.AddWithValue("ponto", perfil._Codigo_SCDE + "P"); command.Parameters.AddWithValue("data_ini", dataIni.ToOADate()); command.Parameters.AddWithValue("data_fim", dataFim.ToOADate()); await using (var reader = await command.ExecuteReaderAsync(ct)) { while (await reader.ReadAsync()) { existentes[(reader.GetString(0), reader.GetDouble(1), reader.GetInt32(2))] = new { Origem = reader.GetString(3), AtivaC = reader.GetDouble(4), AtivaG = reader.GetDouble(5), ReatC = reader.GetDouble(6), ReatG = reader.GetDouble(7) }; } } } foreach (DateTime dia in datas) { int tentativas = 0; bool sucesso = false; while (tentativas < 5 && !sucesso) { if (perfil._Data_de_Migracao > dia) { Console.WriteLine($"Pular {perfil._Codigo_SCDE} - {dia.ToShortDateString()} (antes da migração)"); errosPersistentes.Add($"{perfil._Cod_5min};{perfil._Codigo_SCDE};Fora da data de migração {perfil._Data_de_Migracao} x {dia}"); break; // não tentar antes da data de migração } try { string payload = Xml_requisicao(dia, perfil._Cod_5min, perfil._Codigo_SCDE, 1); var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml"); await limiter.WaitAsync(ct); using var response = await client.PostAsync(endpoint, conteudo, ct); string resposta = await response.Content.ReadAsStringAsync(); if ((int)response.StatusCode >= 400) { try { 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._Cod_5min};{perfil._Codigo_SCDE};SOAP Fault: {ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}"); break; } if (ex.ErrorCode == "2001") // Sem acesso { errosPersistentes.Add($"{perfil._Cod_5min};{perfil._Codigo_SCDE};SOAP Fault: {ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}"); break; } else { throw; } } } await ProcessarXMLAsync(resposta, dataSource, dia, perfil._Cod_5min, perfil._Codigo_SCDE, existentes, limiter, ct, 1); sucesso = true; } catch (Exception ex) { tentativas++; if (tentativas >= 5) { errosPersistentes.Add($"{perfil._Cod_5min};{perfil._Codigo_SCDE};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._Codigo_SCDE}"); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } }); if (errosPersistentes.Count > 0) { File.WriteAllLines(caminhoLog, new[] { "Perfil;Ponto;Status;Message" }.Concat(errosPersistentes)); } } public 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 async Task ProcessarXMLAsync(string xml, NpgsqlDataSource dataSource, DateTime dia, string perfil, string ponto, Dictionary<(string, double, int), dynamic> existentes, RateLimiter limiter, 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 limiter.WaitAsync(ct); using var resp = await client.PostAsync("https://servicos.ccee.org.br/ws/v2/MedidaCincoMinutosBSv2", conteudo); string proxXml = await resp.Content.ReadAsStringAsync(); await ProcessarXMLAsync(proxXml, dataSource, dia, perfil, ponto, existentes, limiter, 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 ponto = 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, out double ativa_c); double.TryParse(m.Element(ns + "energiaAtiva")?.Element(ns + "geracao")?.Element(ns + "valor")?.Value, out double ativa_g); double.TryParse(m.Element(ns + "energiaReativa")?.Element(ns + "consumo")?.Element(ns + "valor")?.Value, out double reat_c); double.TryParse(m.Element(ns + "energiaReativa")?.Element(ns + "geracao")?.Element(ns + "valor")?.Value, out double reat_g); return new { Origem = origem, Ponto = ponto, DiaNum = diaNum, Minuto = minuto, AtivaC = ativa_c, AtivaG = ativa_g, ReatC = reat_c, ReatG = reat_g }; }) .GroupBy(x => new { x.Ponto, x.DiaNum, x.Minuto }) .Select(g => { // Se houver alguma Inspeção Lógica → prioriza var logica = g.FirstOrDefault(x => x.Origem == "Inspeção Lógica"); return logica ?? g.First(); // se não tiver lógica, pega qualquer (coleta diária) }); var novos = new List(); var alterados = new List(); foreach (var m in medidasProcessadas) { var chave = (m.Ponto, m.DiaNum, m.Minuto); if (!existentes.TryGetValue(chave, out var existente)) { // não existe → inserir novos.Add(m); } else { // existe mas mudou → atualizar if (existente.Origem != m.Origem || existente.AtivaC != m.AtivaC || existente.AtivaG != m.AtivaG || existente.ReatC != m.ReatC || existente.ReatG != m.ReatG) { alterados.Add(m); } } } if (novos.Any()) { await using (var connection = await dataSource.OpenConnectionAsync(ct)) { using var writer = connection.BeginBinaryImport("COPY med_5min (origem, dia_num, minuto, ativa_consumo, ativa_geracao, reativa_consumo, reativa_geracao, ponto) FROM STDIN (FORMAT BINARY)"); foreach (var m in novos) { writer.StartRow(); writer.Write(m.Origem); writer.Write(m.DiaNum, NpgsqlTypes.NpgsqlDbType.Numeric); writer.Write(m.Minuto, NpgsqlTypes.NpgsqlDbType.Integer); writer.Write(m.AtivaC, NpgsqlTypes.NpgsqlDbType.Numeric); writer.Write(m.AtivaG, NpgsqlTypes.NpgsqlDbType.Numeric); writer.Write(m.ReatC, NpgsqlTypes.NpgsqlDbType.Numeric); writer.Write(m.ReatG, NpgsqlTypes.NpgsqlDbType.Numeric); writer.Write(m.Ponto); } await writer.CompleteAsync(); } Console.WriteLine($"Inserido {novos.Count} registros. Ponto {ponto}. Dia {dia}"); } if (alterados.Any()) { await using (var connection = await dataSource.OpenConnectionAsync(ct)) { using (var batch = new NpgsqlBatch(connection)) { foreach (var m in alterados) { var cmd = new NpgsqlBatchCommand(@" UPDATE med_5min SET origem = @origem, ativa_consumo = @ativa_consumo, ativa_geracao = @ativa_geracao, reativa_consumo = @reativa_consumo, reativa_geracao = @reativa_geracao WHERE ponto = @ponto AND dia_num = @dia_num AND minuto = @minuto;"); // Adiciona os parâmetros de forma segura cmd.Parameters.AddWithValue("origem", m.Origem); cmd.Parameters.AddWithValue("ativa_consumo", m.AtivaC); cmd.Parameters.AddWithValue("ativa_geracao", m.AtivaG); cmd.Parameters.AddWithValue("reativa_consumo", m.ReatC); cmd.Parameters.AddWithValue("reativa_geracao", m.ReatG); cmd.Parameters.AddWithValue("ponto", m.Ponto); cmd.Parameters.AddWithValue("dia_num", m.DiaNum); cmd.Parameters.AddWithValue("minuto", m.Minuto); batch.BatchCommands.Add(cmd); } // Executa o lote de comandos de uma vez await batch.ExecuteNonQueryAsync(); Console.WriteLine($"Atualizado {alterados.Count} registros. Ponto {ponto}. Dia {dia}"); } } } return; } public class SoapFaultException : Exception { cmdUpdate.Parameters.AddWithValue("origem", m.Origem); cmdUpdate.Parameters.AddWithValue("ativa_c", m.AtivaC); cmdUpdate.Parameters.AddWithValue("ativa_g", m.AtivaG); cmdUpdate.Parameters.AddWithValue("reat_c", m.ReatC); cmdUpdate.Parameters.AddWithValue("reat_g", m.ReatG); cmdUpdate.Parameters.AddWithValue("ponto", m.Ponto); cmdUpdate.Parameters.AddWithValue("dia_num", m.DiaNum); cmdUpdate.Parameters.AddWithValue("minuto", m.Minuto); int rowsAffected = await cmdUpdate.ExecuteNonQueryAsync(); if (rowsAffected == 1) { Console.WriteLine("atualizado"); } public class RateLimiter { private readonly int _maxRequests; private readonly TimeSpan _interval; private int _requestCount; private DateTime _windowStart; private readonly object _lock = new(); public RateLimiter(int maxRequests, TimeSpan interval) { _maxRequests = maxRequests; _interval = interval; var now = DateTime.Now; _windowStart = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute - (now.Minute % interval.Minutes), 0); _requestCount = 0; } public async Task WaitAsync(CancellationToken ct) { while (true) { lock (_lock) { if ((DateTime.Now - _windowStart) > _interval) { // reset janela _windowStart = DateTime.Now; _requestCount = 0; } if (_requestCount < _maxRequests) { _requestCount++; return; // pode prosseguir } } // já bateu o limite → espera até reset await Task.Delay(200, ct); // aguarda 200ms e tenta de novo } } } } public class perfil { public string _Cod_5min { get; set; } public string _Codigo_SCDE { get; set; } public DateTime _Data_de_Migracao { get; set; } public perfil(string cod_5min, string codigo_scde, DateTime data_de_migracao) { _Cod_5min = cod_5min; _Codigo_SCDE = codigo_scde; _Data_de_Migracao = data_de_migracao; } }