From 460598c6b531f6adfb83b28eb71e5cb80b3a5bcf Mon Sep 17 00:00:00 2001 From: Adriano Serighelli Date: Thu, 2 Oct 2025 17:46:48 -0300 Subject: [PATCH] =?UTF-8?q?C=C3=B3digo=20funcional.=20Separa=C3=A7=C3=A3o?= =?UTF-8?q?=20em=20camadas.=20Utiliza=C3=A7=C3=A3o=20de=20estimativa=20par?= =?UTF-8?q?a=20horas=20faltantes=20(entre=209=20e=2011=20registros).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/Program.cs | 10 - .../Application.csproj | 5 + Application/ProcessarMedicoesUseCase.cs | 367 +++++++++++++++++ Data/AccessRepository.cs | 12 - Data/Data.csproj | 9 - Data/PostgresRepository.cs | 12 - Domain/IAccessRepository.cs | 7 + Domain/IPostgresRepository.cs | 12 + Domain/Medicao.cs | 21 +- Domain/Perfil.cs | 14 +- Infrastructure/AccessRepository.cs | 41 ++ Infrastructure/Infrastructure.csproj | 18 + Infrastructure/PostgresRepository.cs | 105 +++++ Infrastructure/RateLimiter.cs | 8 +- Infrastructure/SoapFaultException.cs | 9 +- Infrastructure/SoapHelper.cs | 9 +- PI_Assync_SCDE.sln | 28 +- .../Presentation.csproj | 7 +- Presentation/Program.cs | 36 ++ Program.cs | 385 ------------------ Services/MedicaoService.cs | 12 - Services/Services.csproj | 9 - Services/SoapService.cs | 12 - 23 files changed, 626 insertions(+), 522 deletions(-) delete mode 100644 App/Program.cs rename Infrastructure/Infra.csproj => Application/Application.csproj (56%) create mode 100644 Application/ProcessarMedicoesUseCase.cs delete mode 100644 Data/AccessRepository.cs delete mode 100644 Data/Data.csproj delete mode 100644 Data/PostgresRepository.cs create mode 100644 Domain/IAccessRepository.cs create mode 100644 Domain/IPostgresRepository.cs create mode 100644 Infrastructure/AccessRepository.cs create mode 100644 Infrastructure/Infrastructure.csproj create mode 100644 Infrastructure/PostgresRepository.cs rename App/App.csproj => Presentation/Presentation.csproj (58%) create mode 100644 Presentation/Program.cs delete mode 100644 Services/MedicaoService.cs delete mode 100644 Services/Services.csproj delete mode 100644 Services/SoapService.cs diff --git a/App/Program.cs b/App/Program.cs deleted file mode 100644 index 6949f03..0000000 --- a/App/Program.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace App -{ - internal class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello, World!"); - } - } -} diff --git a/Infrastructure/Infra.csproj b/Application/Application.csproj similarity index 56% rename from Infrastructure/Infra.csproj rename to Application/Application.csproj index 125f4c9..a26b76f 100644 --- a/Infrastructure/Infra.csproj +++ b/Application/Application.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/Application/ProcessarMedicoesUseCase.cs b/Application/ProcessarMedicoesUseCase.cs new file mode 100644 index 0000000..2fbddc7 --- /dev/null +++ b/Application/ProcessarMedicoesUseCase.cs @@ -0,0 +1,367 @@ +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; + } + } +} \ No newline at end of file diff --git a/Data/AccessRepository.cs b/Data/AccessRepository.cs deleted file mode 100644 index fe82719..0000000 --- a/Data/AccessRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Data -{ - internal class AccessRepository - { - } -} diff --git a/Data/Data.csproj b/Data/Data.csproj deleted file mode 100644 index 125f4c9..0000000 --- a/Data/Data.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net9.0 - enable - enable - - - diff --git a/Data/PostgresRepository.cs b/Data/PostgresRepository.cs deleted file mode 100644 index 9019556..0000000 --- a/Data/PostgresRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Data -{ - internal class PostgresRepository - { - } -} diff --git a/Domain/IAccessRepository.cs b/Domain/IAccessRepository.cs new file mode 100644 index 0000000..f367dd0 --- /dev/null +++ b/Domain/IAccessRepository.cs @@ -0,0 +1,7 @@ +namespace Domain +{ + public interface IAccessRepository + { + Task> ObterPerfisAsync(CancellationToken ct); + } +} diff --git a/Domain/IPostgresRepository.cs b/Domain/IPostgresRepository.cs new file mode 100644 index 0000000..e06cf4a --- /dev/null +++ b/Domain/IPostgresRepository.cs @@ -0,0 +1,12 @@ +namespace Domain +{ + public interface IPostgresRepository + { + Task> + ObterMedicoesAsync(string codigoSCDE, DateTime dataIni, DateTime dataFim, CancellationToken ct); + + Task InserirMedicoesAsync(IEnumerable medicoes, CancellationToken ct); + + Task AtualizarMedicoesAsync(IEnumerable medicoes, CancellationToken ct); + } +} diff --git a/Domain/Medicao.cs b/Domain/Medicao.cs index 03b1133..1a78824 100644 --- a/Domain/Medicao.cs +++ b/Domain/Medicao.cs @@ -1,12 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Domain +namespace Domain { - internal class Medicao - { - } + public record Medicao( + string Ponto, + double DiaNum, + int Minuto, + string Origem, + double AtivaConsumo, + double AtivaGeracao, + double ReativaConsumo, + double ReativaGeracao + ); } diff --git a/Domain/Perfil.cs b/Domain/Perfil.cs index 9400048..75775ac 100644 --- a/Domain/Perfil.cs +++ b/Domain/Perfil.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Domain +namespace Domain { - internal class Perfil - { - } -} + public record Perfil(string? Codigo5Minutos, string? CodigoSCDE, DateTime? DataDeMigracao); +} \ No newline at end of file diff --git a/Infrastructure/AccessRepository.cs b/Infrastructure/AccessRepository.cs new file mode 100644 index 0000000..1cad825 --- /dev/null +++ b/Infrastructure/AccessRepository.cs @@ -0,0 +1,41 @@ +using System.Data.OleDb; +using Domain; + +namespace Infrastructure +{ + public class AccessRepository : IAccessRepository + { + private readonly string _connectionString; + + public AccessRepository(string connectionString) + { + _connectionString = connectionString; + } + + public async Task> ObterPerfisAsync(CancellationToken ct) + { + var perfis = new List(); + + using var connection = new OleDbConnection(_connectionString); + await connection.OpenAsync(ct); + + string sql = $"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 sql = "SELECT Cod_5min, Codigo_SCDE, Data_de_Migracao 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 sql = "SELECT Cod_5min, Codigo_SCDE, Data_de_Migracao FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 and Cliente = 'ALMAVIVA' and Unidade = 'GUARULHOS' AND unidadade_gerenciada ORDER BY PerfilCCEE"; + //string sql = "SELECT Cod_5min, Codigo_SCDE, Data_de_Migracao FROM Dados_cadastrais WHERE LEN(Codigo_SCDE) > 5 and Cliente = 'aeroflex' AND Unidade_gerenciada ORDER BY PerfilCCEE"; + + using var command = new OleDbCommand(sql, connection); + using var reader = await command.ExecuteReaderAsync(ct); + + while (await reader.ReadAsync(ct)) + { + var cod5min = reader.IsDBNull(0) ? null : reader.GetString(0); + var codigoSCDE = reader.IsDBNull(1) ? null : reader.GetString(1); + var dataMigracao = reader.IsDBNull(2) ? (DateTime?)null : reader.GetDateTime(2); + perfis.Add(new Perfil(cod5min, codigoSCDE, dataMigracao)); + } + + return perfis; + } + } +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj new file mode 100644 index 0000000..d0bae70 --- /dev/null +++ b/Infrastructure/Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + diff --git a/Infrastructure/PostgresRepository.cs b/Infrastructure/PostgresRepository.cs new file mode 100644 index 0000000..43fedbd --- /dev/null +++ b/Infrastructure/PostgresRepository.cs @@ -0,0 +1,105 @@ +using Domain; +using Npgsql; +using NpgsqlTypes; + +namespace Infrastructure +{ + public class PostgresRepository : IPostgresRepository + { + private readonly NpgsqlDataSource _dataSource; + + public PostgresRepository(string connectionString) + { + _dataSource = NpgsqlDataSource.Create(connectionString); + } + + public async Task> + ObterMedicoesAsync(string codigoSCDE, DateTime dataIni, DateTime dataFim, CancellationToken ct) + { + var existentes = new Dictionary<(string, double, int), Medicao>(); + + string sql = @" + 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"; + + await using var command = _dataSource.CreateCommand(sql); + command.Parameters.AddWithValue("ponto", codigoSCDE + "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(ct)) + { + var medicao = new Medicao( + reader.GetString(0), + reader.GetDouble(1), + reader.GetInt32(2), + reader.GetString(3), + reader.GetDouble(4), + reader.GetDouble(5), + reader.GetDouble(6), + reader.GetDouble(7) + ); + + existentes[(medicao.Ponto, medicao.DiaNum, medicao.Minuto)] = medicao; + } + + return existentes; + } + + public async Task InserirMedicoesAsync(IEnumerable medicoes, CancellationToken ct) + { + 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 medicoes) + { + writer.StartRow(); + writer.Write(m.Origem); + writer.Write(m.DiaNum, NpgsqlDbType.Numeric); + writer.Write(m.Minuto, NpgsqlDbType.Integer); + writer.Write(m.AtivaConsumo, NpgsqlDbType.Numeric); + writer.Write(m.AtivaGeracao, NpgsqlDbType.Numeric); + writer.Write(m.ReativaConsumo, NpgsqlDbType.Numeric); + writer.Write(m.ReativaGeracao, NpgsqlDbType.Numeric); + writer.Write(m.Ponto); + } + + await writer.CompleteAsync(); + } + public async Task AtualizarMedicoesAsync(IEnumerable medicoes, CancellationToken ct) + { + await using var connection = await _dataSource.OpenConnectionAsync(ct); + using var batch = new NpgsqlBatch(connection); + + foreach (var m in medicoes) + { + 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;"); + + cmd.Parameters.AddWithValue("origem", m.Origem); + cmd.Parameters.AddWithValue("ativa_consumo", m.AtivaConsumo); + cmd.Parameters.AddWithValue("ativa_geracao", m.AtivaGeracao); + cmd.Parameters.AddWithValue("reativa_consumo", m.ReativaConsumo); + cmd.Parameters.AddWithValue("reativa_geracao", m.ReativaGeracao); + cmd.Parameters.AddWithValue("ponto", m.Ponto); + cmd.Parameters.AddWithValue("dia_num", m.DiaNum); + cmd.Parameters.AddWithValue("minuto", m.Minuto); + + batch.BatchCommands.Add(cmd); + } + + await batch.ExecuteNonQueryAsync(ct); + } + } +} diff --git a/Infrastructure/RateLimiter.cs b/Infrastructure/RateLimiter.cs index 9be40d3..faaa214 100644 --- a/Infrastructure/RateLimiter.cs +++ b/Infrastructure/RateLimiter.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Infra +namespace Infrastructure { public class RateLimiter { diff --git a/Infrastructure/SoapFaultException.cs b/Infrastructure/SoapFaultException.cs index 8a1b91c..37135a9 100644 --- a/Infrastructure/SoapFaultException.cs +++ b/Infrastructure/SoapFaultException.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Infra; - -namespace Infra +namespace Infrastructure { public class SoapFaultException : Exception { diff --git a/Infrastructure/SoapHelper.cs b/Infrastructure/SoapHelper.cs index f998e46..b7f5929 100644 --- a/Infrastructure/SoapHelper.cs +++ b/Infrastructure/SoapHelper.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Linq; +using System.Xml.Linq; -namespace Infra +namespace Infrastructure { public static class SoapHelper { diff --git a/PI_Assync_SCDE.sln b/PI_Assync_SCDE.sln index 9505a85..5e8f006 100644 --- a/PI_Assync_SCDE.sln +++ b/PI_Assync_SCDE.sln @@ -5,15 +5,13 @@ VisualStudioVersion = 17.1.32319.34 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App_old", "App_old.csproj", "{48DFCC70-0352-4394-9821-2B243EB389AB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infra", "Infrastructure\Infra.csproj", "{0910ED06-C154-41FF-B556-ADBA53A10427}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{4B7EE8CE-E452-4B31-9B21-64113B6C48C8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{0910ED06-C154-41FF-B556-ADBA53A10427}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{38CE64C7-8F03-4622-8DD5-5BA19F66E7AF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{2B2323E4-C409-4432-AA45-54C1642FDA88}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{8B6C1411-58F1-4701-9736-D8A87F2FBB64}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "App\App.csproj", "{2B43C133-911B-4F3D-9DAB-1DA342D3822B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation", "Presentation\Presentation.csproj", "{979738F9-0985-45D4-8E9C-2DE972084FE9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,22 +27,18 @@ Global {0910ED06-C154-41FF-B556-ADBA53A10427}.Debug|Any CPU.Build.0 = Debug|Any CPU {0910ED06-C154-41FF-B556-ADBA53A10427}.Release|Any CPU.ActiveCfg = Release|Any CPU {0910ED06-C154-41FF-B556-ADBA53A10427}.Release|Any CPU.Build.0 = Release|Any CPU - {4B7EE8CE-E452-4B31-9B21-64113B6C48C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4B7EE8CE-E452-4B31-9B21-64113B6C48C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4B7EE8CE-E452-4B31-9B21-64113B6C48C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4B7EE8CE-E452-4B31-9B21-64113B6C48C8}.Release|Any CPU.Build.0 = Release|Any CPU {38CE64C7-8F03-4622-8DD5-5BA19F66E7AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38CE64C7-8F03-4622-8DD5-5BA19F66E7AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {38CE64C7-8F03-4622-8DD5-5BA19F66E7AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {38CE64C7-8F03-4622-8DD5-5BA19F66E7AF}.Release|Any CPU.Build.0 = Release|Any CPU - {2B2323E4-C409-4432-AA45-54C1642FDA88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B2323E4-C409-4432-AA45-54C1642FDA88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B2323E4-C409-4432-AA45-54C1642FDA88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B2323E4-C409-4432-AA45-54C1642FDA88}.Release|Any CPU.Build.0 = Release|Any CPU - {2B43C133-911B-4F3D-9DAB-1DA342D3822B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B43C133-911B-4F3D-9DAB-1DA342D3822B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B43C133-911B-4F3D-9DAB-1DA342D3822B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B43C133-911B-4F3D-9DAB-1DA342D3822B}.Release|Any CPU.Build.0 = Release|Any CPU + {8B6C1411-58F1-4701-9736-D8A87F2FBB64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B6C1411-58F1-4701-9736-D8A87F2FBB64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B6C1411-58F1-4701-9736-D8A87F2FBB64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B6C1411-58F1-4701-9736-D8A87F2FBB64}.Release|Any CPU.Build.0 = Release|Any CPU + {979738F9-0985-45D4-8E9C-2DE972084FE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {979738F9-0985-45D4-8E9C-2DE972084FE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {979738F9-0985-45D4-8E9C-2DE972084FE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {979738F9-0985-45D4-8E9C-2DE972084FE9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/App/App.csproj b/Presentation/Presentation.csproj similarity index 58% rename from App/App.csproj rename to Presentation/Presentation.csproj index fd4bd08..f741727 100644 --- a/App/App.csproj +++ b/Presentation/Presentation.csproj @@ -1,10 +1,15 @@  - Exe net9.0 enable enable + Exe + + + + + diff --git a/Presentation/Program.cs b/Presentation/Program.cs new file mode 100644 index 0000000..4c6d63b --- /dev/null +++ b/Presentation/Program.cs @@ -0,0 +1,36 @@ +using System.Data.OleDb; +using Application; +using Infrastructure; + +class Program +{ + static async Task Main() + { + DateTime inicio = DateTime.Now; + 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;"; + string ACCESS_CONN_STRING = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\\srv-dados\documentos\Middle\Informativo Setorial\Modelo Word\BD1_dados cadastrais e faturas.accdb;Jet OLEDB:Database Password=gds21"; + string caminhoLog = $@"\\srv-dados\documentos\Back\Carteira x.x\Codigo\Erros\log_erros_{inicio:MM_dd_HH_mm}.csv"; + //DateTime dataIni = new DateTime(inicio.Year, inicio.Month, 1); + //DateTime dataFim = new DateTime(inicio.Year, inicio.Month, inicio.Day); + DateTime dataIni = new DateTime(inicio.Year, 1, 1); + DateTime dataFim = new DateTime(inicio.Year, 2, 1); + + // Configuração de dependências (pode usar um container DI depois) + var postgresRepo = new PostgresRepository(PG_CONN_STRING_PROD); + var accessRepo = new AccessRepository(ACCESS_CONN_STRING); + var httpClient = 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 + }); + var rateLimiter = new RateLimiter(400, TimeSpan.FromMinutes(1)); + var useCase = new ProcessarMedicoesUseCase(postgresRepo, accessRepo, httpClient, rateLimiter); + + await useCase.ExecuteAsync(dataIni, dataFim, caminhoLog, CancellationToken.None); + + Console.WriteLine($"Concluído. Tempo total: {DateTime.Now - inicio}"); + Console.ReadKey(); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index bacc53f..67debea 100644 --- a/Program.cs +++ b/Program.cs @@ -8,393 +8,8 @@ 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(); - var atualizados = new ConcurrentBag(); - var inseridos = 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 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; } } \ No newline at end of file diff --git a/Services/MedicaoService.cs b/Services/MedicaoService.cs deleted file mode 100644 index 4a0c013..0000000 --- a/Services/MedicaoService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Services -{ - internal class MedicaoService - { - } -} diff --git a/Services/Services.csproj b/Services/Services.csproj deleted file mode 100644 index 125f4c9..0000000 --- a/Services/Services.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net9.0 - enable - enable - - - diff --git a/Services/SoapService.cs b/Services/SoapService.cs deleted file mode 100644 index 5e63ae0..0000000 --- a/Services/SoapService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Services -{ - internal class SoapService - { - } -}