PI_Assync_SCDE/Application/ProcessarMedicoesUseCase.cs
Adriano Serighelli 460598c6b5 Código funcional.
Separação em camadas.
Utilização de estimativa para horas faltantes (entre 9 e 11 registros).
2025-10-02 17:46:48 -03:00

367 lines
17 KiB
C#
Raw Blame History

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<string>();
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<72><61>o)");
errosPersistentes.Add($"{perfil.Codigo5Minutos};{perfil.CodigoSCDE};Fora da data de migra<72><61>o {perfil.DataDeMigracao} x {dia}");
break; // n<>o tentar antes da data de migra<72><61>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<73><69>es
{
var now = DateTime.UtcNow;
var delay = 60000 - (now.Second * 1000 + now.Millisecond);
Console.WriteLine($"!! Limite de requisi<73><69>es atingido. Aguardando at<61> {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<73><69>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<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<70>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<70><65>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<Medicao>();
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<6C><61>o
var reais = lista.ToDictionary(m => m.Minuto, m => m);
// Identifique sequ<71>ncias de minutos faltantes consecutivos
var sequencias = new List<List<int>>();
List<int> atual = null;
int? ultimo = null;
foreach (var min in faltantes)
{
if (atual == null || ultimo == null || min != ultimo + 5)
{
atual = new List<int>();
sequencias.Add(atual);
}
atual.Add(min);
ultimo = min;
}
var estimadas = new List<Medicao>();
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<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 ||
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<72><61>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<69>o por zero).");
double yProcurado = yAnterior.Value +
((yPosterior.Value - yAnterior.Value) / (xPosterior.Value - xAnterior.Value)) *
(xProcurado - xAnterior.Value);
return yProcurado;
}
return null;
}
}
}