Compare commits
12 Commits
243d1551d1
...
e4c398eef0
| Author | SHA1 | Date | |
|---|---|---|---|
| e4c398eef0 | |||
| 817d542631 | |||
| 8826ba3a31 | |||
| 9a91cc456f | |||
| 0db548b273 | |||
| c98aec2c24 | |||
| 325bc76757 | |||
| 654d363d12 | |||
| a8d87addad | |||
| 460598c6b5 | |||
| 1e814e9c34 | |||
| 771de2de33 |
14
Application/Application.csproj
Normal file
14
Application/Application.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
516
Application/ProcessarMedicoesUseCase.cs
Normal file
516
Application/ProcessarMedicoesUseCase.cs
Normal file
@ -0,0 +1,516 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Xml.Linq;
|
||||
using Domain;
|
||||
using Infrastructure;
|
||||
|
||||
namespace Application
|
||||
{
|
||||
public class ProcessarMedicoesUseCase : IDisposable
|
||||
{
|
||||
private readonly IPostgresRepository _postgresRepository;
|
||||
private readonly IAccessRepository _accessRepository;
|
||||
private readonly TokenBucketRateLimiter _rateLimiter;
|
||||
|
||||
public ProcessarMedicoesUseCase(
|
||||
IPostgresRepository postgresRepository,
|
||||
IAccessRepository accessRepository)
|
||||
{
|
||||
_postgresRepository = postgresRepository;
|
||||
_accessRepository = accessRepository;
|
||||
// 400 requisições por minuto
|
||||
_rateLimiter = new TokenBucketRateLimiter(4000, capacity: 4000);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_rateLimiter?.Dispose();
|
||||
}
|
||||
|
||||
private enum LogType { Info, Error, Operation, UpdateMeasurement }
|
||||
|
||||
private record LogItem(
|
||||
LogType Tipo,
|
||||
string Perfil,
|
||||
string Ponto,
|
||||
double DiaNum,
|
||||
int Minuto,
|
||||
string Status,
|
||||
string Mensagem,
|
||||
int Inseridos,
|
||||
int Atualizados,
|
||||
Medicao? Antes,
|
||||
Medicao? Depois,
|
||||
DateTime Timestamp)
|
||||
{
|
||||
public string ToCsvLine()
|
||||
{
|
||||
static string Esc(string? s) => (s ?? "").Replace(";", ",").Replace("\r", " ").Replace("\n", " ");
|
||||
static string FMed(Medicao? m) =>
|
||||
m is null ? "" : $"{Esc(m.Ponto)}|{m.DiaNum}|{m.Minuto}|{Esc(m.Origem)}|{m.AtivaConsumo?.ToString(CultureInfo.InvariantCulture) ?? ""}|{m.AtivaGeracao?.ToString(CultureInfo.InvariantCulture) ?? ""}|{m.ReativaConsumo?.ToString(CultureInfo.InvariantCulture) ?? ""}|{m.ReativaGeracao?.ToString(CultureInfo.InvariantCulture) ?? ""}";
|
||||
|
||||
return string.Join(";", new[]
|
||||
{
|
||||
Tipo.ToString(),
|
||||
Esc(Perfil),
|
||||
Esc(Ponto),
|
||||
DiaNum.ToString(CultureInfo.InvariantCulture),
|
||||
Minuto.ToString(),
|
||||
Esc(Status),
|
||||
Esc(Mensagem),
|
||||
Inseridos.ToString(),
|
||||
Atualizados.ToString(),
|
||||
FMed(Antes),
|
||||
FMed(Depois),
|
||||
Timestamp.ToString("o")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(DateTime dataIni, DateTime dataFim, string caminhoLog, CancellationToken ct)
|
||||
{
|
||||
var logs = new ConcurrentBag<LogItem>();
|
||||
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) =>
|
||||
{
|
||||
if (perfil.Codigo5Minutos == "0" || perfil.Codigo5Minutos == string.Empty || perfil.Codigo5Minutos == null)
|
||||
{
|
||||
Console.WriteLine($"Pular {perfil.CodigoSCDE} - (cod 5 min pendente)");
|
||||
logs.Add(new LogItem(LogType.Error, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dataIni.ToOADate(), 0, "ERRO", "cod_5min pendente", 0, 0, null, null, DateTime.UtcNow));
|
||||
return;
|
||||
}
|
||||
|
||||
var existentes = await ObterMedicoesComRetry(perfil.CodigoSCDE, dataIni, dataFim, ctPerfil, logs);
|
||||
|
||||
await Parallel.ForEachAsync(datas, ctPerfil, async (dia, ctDia) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessarDiaAsync(perfil, dia, existentes, endpoint, logs, ctDia);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Add(new LogItem(LogType.Error, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dia.ToOADate(), 0, "ERRO", ex.Message.Replace("\n", "-n-"), 0, 0, null, null, DateTime.UtcNow));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var linhas = new List<string>
|
||||
{
|
||||
"Tipo;Perfil;Ponto;DiaNum;Minuto;Status;Mensagem;Inseridos;Atualizados;Antes;Depois;Timestamp"
|
||||
};
|
||||
linhas.AddRange(logs.Select(l => l.ToCsvLine()));
|
||||
|
||||
File.WriteAllLines(caminhoLog, linhas);
|
||||
}
|
||||
|
||||
private async Task<IDictionary<(string, double, int), Medicao>> ObterMedicoesComRetry(
|
||||
string codigoSCDE, DateTime dataIni, DateTime dataFim, CancellationToken ct, ConcurrentBag<LogItem> logs)
|
||||
{
|
||||
int tentativas = 0;
|
||||
while (tentativas < 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _postgresRepository.ObterMedicoesAsync(codigoSCDE, dataIni, dataFim, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tentativas++;
|
||||
if (tentativas >= 3)
|
||||
{
|
||||
logs.Add(new LogItem(LogType.Error, "", codigoSCDE, dataIni.ToOADate(), 0, "Erro", ex.Message.Replace("\n", "-n-"), 0, 0, null, null, DateTime.UtcNow));
|
||||
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<LogItem> logs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (perfil.DataDeMigracao > dia)
|
||||
{
|
||||
Console.WriteLine($"Pular {perfil.CodigoSCDE} - {dia.ToShortDateString()} (antes da migração)");
|
||||
logs.Add(new LogItem(LogType.Info, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dia.ToOADate(), 0, "Fora da data de migração", $"Data de migração {perfil.DataDeMigracao} x {dia}", 0, 0, null, null, DateTime.UtcNow));
|
||||
return;
|
||||
}
|
||||
|
||||
// Acumulador de medidas (todas as páginas)
|
||||
var acumulador = new List<XElement>();
|
||||
int paginaAtual = 1;
|
||||
int totalPaginas = 1;
|
||||
|
||||
while (paginaAtual <= totalPaginas)
|
||||
{
|
||||
int tentativas = 0;
|
||||
bool sucesso = false;
|
||||
|
||||
while (tentativas < 5 && !sucesso)
|
||||
{
|
||||
try
|
||||
{
|
||||
string payload = Xml_requisicao(dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, paginaAtual);
|
||||
var conteudo = new StringContent(payload, Encoding.UTF8, "application/xml");
|
||||
|
||||
// Aguarda token do rate limiter antes de cada requisição
|
||||
await _rateLimiter.WaitAsync(ct);
|
||||
|
||||
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")
|
||||
{
|
||||
// limite de requisições atingido -> aguardar tick de janela e tentar novamente a mesma página
|
||||
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; // não conta como tentativa extra; re-tenta a mesma página
|
||||
}
|
||||
if (ex.ErrorCode == "4001" || ex.ErrorCode == "2001")
|
||||
{
|
||||
// erro persistente, registra e interrompe processamento deste dia/ponto
|
||||
logs.Add(new LogItem(LogType.Error, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dia.ToOADate(), 0, "SOAP Fault", $"{ex.ErrorCode};{ex.ErrorMessage.Replace("\n", "-n-")}", 0, 0, null, null, DateTime.UtcNow));
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse e acumula medidas desta página
|
||||
var doc = XDocument.Parse(resposta);
|
||||
XNamespace ns = "http://xmlns.energia.org.br/BO/v2";
|
||||
|
||||
if (paginaAtual == 1)
|
||||
{
|
||||
int.TryParse(doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "totalPaginas")?.Value, out totalPaginas);
|
||||
}
|
||||
|
||||
var medidasPagina = doc.Descendants(ns + "medida")
|
||||
.Where(x => (string)x.Element(ns + "tipoEnergia") == "L")
|
||||
.ToList();
|
||||
|
||||
acumulador.AddRange(medidasPagina);
|
||||
|
||||
// página processada com sucesso
|
||||
sucesso = true;
|
||||
paginaAtual++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tentativas++;
|
||||
if (tentativas >= 5)
|
||||
{
|
||||
logs.Add(new LogItem(LogType.Error, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dia.ToOADate(), 0, "Erro", ex.Message.Replace("\n", "-n-"), 0, 0, null, null, DateTime.UtcNow));
|
||||
// aborta o processamento do dia após falhas repetidas na mesma página
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
int backoff = (int)Math.Pow(2.4, tentativas) * 1000;
|
||||
Console.WriteLine($"Erro na requisição (página {paginaAtual}) ({ex.Message}), tentativa {tentativas}. Aguardando {backoff / 1000}s...");
|
||||
await Task.Delay(backoff, ct);
|
||||
}
|
||||
}
|
||||
} // fim tentativasPagina
|
||||
} // fim while paginas
|
||||
|
||||
// ao final de todas as páginas, processa o XML acumulado
|
||||
try
|
||||
{
|
||||
await ProcessarXMLAsync(acumulador, dia, perfil.Codigo5Minutos, perfil.CodigoSCDE, existentes, ct, logs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Add(new LogItem(LogType.Error, perfil.Codigo5Minutos ?? "", perfil.CodigoSCDE ?? "", dia.ToOADate(), 0, "Erro", ex.Message.Replace("\n", "-n-"), 0, 0, null, null, DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessarXMLAsync(
|
||||
List<XElement> acumulador,
|
||||
DateTime dia,
|
||||
string perfil,
|
||||
string ponto,
|
||||
IDictionary<(string, double, int), Medicao> existentes,
|
||||
CancellationToken ct,
|
||||
ConcurrentBag<LogItem>? logs = null)
|
||||
{
|
||||
logs ??= new ConcurrentBag<LogItem>();
|
||||
|
||||
XNamespace ns = "http://xmlns.energia.org.br/BO/v2";
|
||||
|
||||
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<Medicao>();
|
||||
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<Medicao> 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<Medicao>();
|
||||
|
||||
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<Medicao>();
|
||||
var alterados = new List<Medicao>();
|
||||
|
||||
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);
|
||||
// log detalhado por medição alterada
|
||||
logs.Add(new LogItem(
|
||||
LogType.UpdateMeasurement,
|
||||
perfil,
|
||||
m.Ponto,
|
||||
m.DiaNum,
|
||||
m.Minuto,
|
||||
"ALTERADO",
|
||||
"Medição alterada (antes x depois)",
|
||||
0,
|
||||
1,
|
||||
existente,
|
||||
m,
|
||||
DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (novos.Any())
|
||||
{
|
||||
await _postgresRepository.InserirMedicoesAsync(novos, ct);
|
||||
Console.WriteLine($"Ponto {ponto}. Dia {dia:dd/MM/yyyy}. {novos.Count:D3} registros inseridos.");
|
||||
logs.Add(new LogItem(LogType.Operation, perfil, ponto, dia.ToOADate(), 0, "OK", "Novos", novos.Count, 0, null, null, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
if (alterados.Any())
|
||||
{
|
||||
await _postgresRepository.AtualizarMedicoesAsync(alterados, ct);
|
||||
Console.WriteLine($"Ponto {ponto}. Dia {dia:dd/MM/yyyy}. {alterados.Count:D3} registros atualizados.");
|
||||
// logs.Add(new LogItem(LogType.Operation, perfil, ponto, dia.ToOADate(), 0, "OK", "Atualizados", 0, alterados.Count, null, null, DateTime.UtcNow));
|
||||
}
|
||||
if (!novos.Any() && !alterados.Any())
|
||||
{
|
||||
Console.WriteLine($"Ponto {ponto}. Dia {dia:dd/MM/yyyy}. 000 registros alterados.");
|
||||
logs.Add(new LogItem(LogType.Info, perfil, ponto, dia.ToOADate(), 0, "OK", "Sem alterações", 0, 0, null, null, DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Token-bucket simples: emite tokens a cada intervalo (distribui as requisições ao longo do tempo)
|
||||
private class TokenBucketRateLimiter : IDisposable
|
||||
{
|
||||
private readonly Channel<bool> _channel;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _producer;
|
||||
|
||||
public TokenBucketRateLimiter(int tokensPerMinute, int capacity = 400, int initialTokens = 0)
|
||||
{
|
||||
if (tokensPerMinute <= 0) throw new ArgumentOutOfRangeException(nameof(tokensPerMinute));
|
||||
_channel = Channel.CreateBounded<bool>(new BoundedChannelOptions(capacity)
|
||||
{
|
||||
SingleWriter = true,
|
||||
SingleReader = false,
|
||||
FullMode = BoundedChannelFullMode.DropWrite
|
||||
});
|
||||
|
||||
// opcional: preencher tokens iniciais (0 evita burst inicial)
|
||||
for (int i = 0; i < Math.Min(initialTokens, capacity); i++)
|
||||
_channel.Writer.TryWrite(true);
|
||||
|
||||
var intervalMs = (int)Math.Ceiling(60000.0 / tokensPerMinute); // ex: 400 -> 150ms
|
||||
|
||||
_producer = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(intervalMs, _cts.Token);
|
||||
// tenta escrever; se cheio, descarta (evita acumular tokens além da capacidade)
|
||||
_channel.Writer.TryWrite(true);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
public ValueTask<bool> WaitAsync(CancellationToken ct) => _channel.Reader.ReadAsync(ct);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_channel.Writer.TryComplete();
|
||||
try { _producer?.GetAwaiter().GetResult(); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Domain/Domain.csproj
Normal file
9
Domain/Domain.csproj
Normal file
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
7
Domain/IAccessRepository.cs
Normal file
7
Domain/IAccessRepository.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Domain
|
||||
{
|
||||
public interface IAccessRepository
|
||||
{
|
||||
Task<IEnumerable<Perfil>> ObterPerfisAsync(CancellationToken ct);
|
||||
}
|
||||
}
|
||||
12
Domain/IPostgresRepository.cs
Normal file
12
Domain/IPostgresRepository.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Domain
|
||||
{
|
||||
public interface IPostgresRepository
|
||||
{
|
||||
Task<IDictionary<(string Ponto, double DiaNum, int Minuto), Medicao>>
|
||||
ObterMedicoesAsync(string codigoSCDE, DateTime dataIni, DateTime dataFim, CancellationToken ct);
|
||||
|
||||
Task InserirMedicoesAsync(IEnumerable<Medicao> medicoes, CancellationToken ct);
|
||||
|
||||
Task AtualizarMedicoesAsync(IEnumerable<Medicao> medicoes, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
13
Domain/Medicao.cs
Normal file
13
Domain/Medicao.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Domain
|
||||
{
|
||||
public record Medicao(
|
||||
string Ponto,
|
||||
double DiaNum,
|
||||
int Minuto,
|
||||
string Origem,
|
||||
double? AtivaConsumo,
|
||||
double? AtivaGeracao,
|
||||
double? ReativaConsumo,
|
||||
double? ReativaGeracao
|
||||
);
|
||||
}
|
||||
4
Domain/Perfil.cs
Normal file
4
Domain/Perfil.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Domain
|
||||
{
|
||||
public record Perfil(string? Codigo5Minutos, string? CodigoSCDE, DateTime? DataDeMigracao);
|
||||
}
|
||||
42
Infrastructure/AccessRepository.cs
Normal file
42
Infrastructure/AccessRepository.cs
Normal file
@ -0,0 +1,42 @@
|
||||
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<IEnumerable<Perfil>> ObterPerfisAsync(CancellationToken ct)
|
||||
{
|
||||
var perfis = new List<Perfil>();
|
||||
|
||||
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 = 'ABEVÊ' and Unidade = 'ABV LOJA 29 - COXIM' 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 = 'SURF CENTER' 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 Codigo_SCDE = 'PIPMTEUFCHE01'";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Infrastructure/Infrastructure.csproj
Normal file
18
Infrastructure/Infrastructure.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="System.Data.OleDb" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
110
Infrastructure/PostgresRepository.cs
Normal file
110
Infrastructure/PostgresRepository.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System.Globalization;
|
||||
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<IDictionary<(string, double, int), Medicao>>
|
||||
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.IsDBNull(4) ? (double?)null : reader.GetDouble(4),
|
||||
reader.IsDBNull(5) ? (double?)null : reader.GetDouble(5),
|
||||
reader.IsDBNull(6) ? (double?)null : reader.GetDouble(6),
|
||||
reader.IsDBNull(7) ? (double?)null : reader.GetDouble(7)
|
||||
);
|
||||
|
||||
existentes[(medicao.Ponto, medicao.DiaNum, medicao.Minuto)] = medicao;
|
||||
}
|
||||
|
||||
return existentes;
|
||||
}
|
||||
|
||||
public async Task InserirMedicoesAsync(IEnumerable<Medicao> 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<Medicao> medicoes, CancellationToken ct)
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(ct);
|
||||
using var batch = new NpgsqlBatch(connection);
|
||||
|
||||
// Criar os valores da atualização, como se fosse uma tabela temporária
|
||||
var valores = medicoes
|
||||
.Select(m => $"('{m.Ponto}', {m.DiaNum}, {m.Minuto}, '{m.Origem}', " +
|
||||
$"{(m.AtivaConsumo.HasValue ? m.AtivaConsumo.Value.ToString(CultureInfo.InvariantCulture) : "NULL")}, " +
|
||||
$"{(m.AtivaGeracao.HasValue ? m.AtivaGeracao.Value.ToString(CultureInfo.InvariantCulture) : "NULL")}, " +
|
||||
$"{(m.ReativaConsumo.HasValue ? m.ReativaConsumo.Value.ToString(CultureInfo.InvariantCulture) : "NULL")}, " +
|
||||
$"{(m.ReativaGeracao.HasValue ? m.ReativaGeracao.Value.ToString(CultureInfo.InvariantCulture) : "NULL")})")
|
||||
.ToList();
|
||||
|
||||
// Gerar a query dinâmica com os valores em formato de tabela temporária
|
||||
var query = $@"
|
||||
UPDATE med_5min
|
||||
SET origem = nv.origem,
|
||||
ativa_consumo = CAST(nv.ativa_consumo AS numeric),
|
||||
ativa_geracao = CAST(nv.ativa_geracao AS numeric),
|
||||
reativa_consumo = CAST(nv.reativa_consumo AS numeric),
|
||||
reativa_geracao = CAST(nv.reativa_geracao AS numeric)
|
||||
FROM (VALUES
|
||||
{string.Join(",", valores)}
|
||||
) AS nv (ponto, dia_num, minuto, origem, ativa_consumo, ativa_geracao, reativa_consumo, reativa_geracao)
|
||||
WHERE med_5min.ponto = nv.ponto
|
||||
AND med_5min.dia_num = nv.dia_num
|
||||
AND med_5min.minuto = nv.minuto;";
|
||||
|
||||
// Criação do comando NpgsqlBatchCommand
|
||||
var cmd = new NpgsqlBatchCommand(query);
|
||||
batch.BatchCommands.Add(cmd);
|
||||
|
||||
// Executar o comando
|
||||
await batch.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Infrastructure/RateLimiter.cs
Normal file
45
Infrastructure/RateLimiter.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace Infrastructure
|
||||
{
|
||||
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, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Infrastructure/SoapFaultException.cs
Normal file
21
Infrastructure/SoapFaultException.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace Infrastructure
|
||||
{
|
||||
public class SoapFaultException : Exception
|
||||
{
|
||||
public string FaultCode { get; }
|
||||
public string FaultString { get; }
|
||||
public string ErrorCode { get; }
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
public SoapFaultException(string faultCode, string faultString, string errorCode, string errorMessage)
|
||||
: base($"{faultString} (Code: {errorCode})")
|
||||
{
|
||||
FaultCode = faultCode;
|
||||
FaultString = faultString;
|
||||
ErrorCode = errorCode;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
Infrastructure/SoapHelper.cs
Normal file
27
Infrastructure/SoapHelper.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Infrastructure
|
||||
{
|
||||
public static class SoapHelper
|
||||
{
|
||||
public static void VerificarRespostaSOAP(string responseXml)
|
||||
{
|
||||
var doc = XDocument.Parse(responseXml);
|
||||
XNamespace env = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
XNamespace tns = "http://xmlns.energia.org.br/FM/v2";
|
||||
|
||||
var fault = doc.Descendants(env + "Fault").FirstOrDefault();
|
||||
if (fault != null)
|
||||
{
|
||||
string faultCode = fault.Element("faultcode")?.Value ?? "";
|
||||
string faultString = fault.Element("faultstring")?.Value ?? "";
|
||||
|
||||
var detail = fault.Element("detail")?.Descendants().First();
|
||||
string errorCode = detail?.Element(tns + "errorCode")?.Value ?? "";
|
||||
string message = detail?.Element(tns + "message")?.Value ?? "";
|
||||
|
||||
throw new SoapFaultException(faultCode, faultString, errorCode, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
|
||||
<StartupObject></StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<Optimize>False</Optimize>
|
||||
<DefineConstants>$(DefineConstants)TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<Optimize>False</Optimize>
|
||||
<DefineConstants>$(DefineConstants)TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<COMReference Include="WinHttp">
|
||||
<WrapperTool>tlbimp</WrapperTool>
|
||||
<VersionMinor>1</VersionMinor>
|
||||
<VersionMajor>5</VersionMajor>
|
||||
<Guid>662901fc-6951-4854-9eb2-d9a2570f2b2e</Guid>
|
||||
<Lcid>0</Lcid>
|
||||
<Isolated>false</Isolated>
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="System.Data.OleDb" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -3,7 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32319.34
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PI_Assync_SCDE", "PI_Assync_SCDE.csproj", "{48DFCC70-0352-4394-9821-2B243EB389AB}"
|
||||
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}") = "Application", "Application\Application.csproj", "{8B6C1411-58F1-4701-9736-D8A87F2FBB64}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation", "Presentation\Presentation.csproj", "{979738F9-0985-45D4-8E9C-2DE972084FE9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -11,10 +17,22 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{48DFCC70-0352-4394-9821-2B243EB389AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{48DFCC70-0352-4394-9821-2B243EB389AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{48DFCC70-0352-4394-9821-2B243EB389AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{48DFCC70-0352-4394-9821-2B243EB389AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0910ED06-C154-41FF-B556-ADBA53A10427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
{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
|
||||
{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
|
||||
|
||||
15
Presentation/Presentation.csproj
Normal file
15
Presentation/Presentation.csproj
Normal file
@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
Presentation/Program.cs
Normal file
36
Presentation/Program.cs
Normal file
@ -0,0 +1,36 @@
|
||||
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 = 4;";
|
||||
// string PG_CONN_STRING_PROD = "Server = 192.168.10.248; Port = 5432; Database = smartenergiadev; Username = postgres; Password = gds21; Timeout = 60; CommandTimeout = 60; ApplicationName = new_med_5_min; Connection Lifetime = 120; Minimum Pool Size = 2; Maximum Pool Size = 4;";
|
||||
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);
|
||||
//agosto finalizado
|
||||
DateTime dataIni = new DateTime(inicio.Year, 09, 01);
|
||||
DateTime dataFim = new DateTime(inicio.Year, 10, 01);
|
||||
|
||||
// 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 useCase = new ProcessarMedicoesUseCase(postgresRepo, accessRepo);
|
||||
|
||||
await useCase.ExecuteAsync(dataIni, dataFim, caminhoLog, CancellationToken.None);
|
||||
|
||||
Console.WriteLine($"Concluído. Tempo total: {DateTime.Now - inicio}");
|
||||
string input = string.Empty;
|
||||
|
||||
while (input.ToLower() != "fim")
|
||||
{
|
||||
Console.WriteLine("Digite 'fim' para finalizar:");
|
||||
input = Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
479
Program.cs
479
Program.cs
@ -1,479 +0,0 @@
|
||||
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<perfil> Busca_dad_BD(string caminho_BD, DateTime dataIni)
|
||||
{
|
||||
var lista = new List<perfil>();
|
||||
|
||||
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<perfil> perfis, string caminhoLog)
|
||||
{
|
||||
var limiter = new RateLimiter(400, TimeSpan.FromMinutes(1));
|
||||
|
||||
var errosPersistentes = new ConcurrentBag<string>();
|
||||
var atualizados = new ConcurrentBag<string>();
|
||||
var inseridos = new ConcurrentBag<string>();
|
||||
|
||||
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<XElement> 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<XElement>();
|
||||
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<dynamic>();
|
||||
var alterados = new List<dynamic>();
|
||||
|
||||
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
|
||||
{
|
||||
public string FaultCode { get; }
|
||||
public string FaultString { get; }
|
||||
public string ErrorCode { get; }
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
public SoapFaultException(string faultCode, string faultString, string errorCode, string errorMessage)
|
||||
: base($"{faultString} (Code: {errorCode})")
|
||||
{
|
||||
FaultCode = faultCode;
|
||||
FaultString = faultString;
|
||||
ErrorCode = errorCode;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public static void VerificarRespostaSOAP(string responseXml)
|
||||
{
|
||||
var doc = XDocument.Parse(responseXml);
|
||||
XNamespace env = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
XNamespace tns = "http://xmlns.energia.org.br/FM/v2";
|
||||
|
||||
var fault = doc.Descendants(env + "Fault").FirstOrDefault();
|
||||
if (fault != null)
|
||||
{
|
||||
string faultCode = fault.Element("faultcode")?.Value ?? "";
|
||||
string faultString = fault.Element("faultstring")?.Value ?? "";
|
||||
|
||||
var detail = fault.Element("detail")?.Descendants().First();
|
||||
string errorCode = detail?.Element(tns + "errorCode")?.Value ?? "";
|
||||
string message = detail?.Element(tns + "message")?.Value ?? "";
|
||||
|
||||
throw new SoapFaultException(faultCode, faultString, errorCode, message);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
28
Properties/Resources.Designer.cs
generated
28
Properties/Resources.Designer.cs
generated
@ -1,24 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
// O código foi gerado por uma ferramenta.
|
||||
// Versão de Tempo de Execução:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se
|
||||
// o código for gerado novamente.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace PI_Assync_SCDE.Properties {
|
||||
namespace App_old.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// Uma classe de recurso de tipo de alta segurança, para pesquisar cadeias de caracteres localizadas etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
// Essa classe foi gerada automaticamente pela classe StronglyTypedResourceBuilder
|
||||
// através de uma ferramenta como ResGen ou Visual Studio.
|
||||
// Para adicionar ou remover um associado, edite o arquivo .ResX e execute ResGen novamente
|
||||
// com a opção /str, ou recrie o projeto do VS.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
@ -33,13 +33,13 @@ namespace PI_Assync_SCDE.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// Retorna a instância de ResourceManager armazenada em cache usada por essa classe.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PI_Assync_SCDE.Properties.Resources", typeof(Resources).Assembly);
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("App_old.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
@ -47,8 +47,8 @@ namespace PI_Assync_SCDE.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// Substitui a propriedade CurrentUICulture do thread atual para todas as
|
||||
/// pesquisas de recursos que usam essa classe de recurso de tipo de alta segurança.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user