No meu projeto de Supervisório, utilizando o scadaBR 1.2, contemplo um senário onde monitoro diferentes sistemas com operadores (pessoas) monitorando os respectivos sistemas.
Tenho um problema no qual a lista de alarmes apresenta todos os alarmes, independente do sistema e/ou operador, e isso não me parecia um problema, até um operador começar a reconhecer o alarme do sistema do outro operador.
A procura de uma solução, encontrei um tópico descontinuado que tratava este assunto, contudo está sem resposta: https://forum.scadabr.com.br/t/personalizacao-do-objeto-lista-de-alarmes-no-graphicview/1725
Alguns dos colegas mais avançados já trabalharam ou fazem ideia de como solucionar este “Problema”?
Pela lista de alarmes propriamente dita é mais difícil realizar personalizações. Uma alternativa é criar uma tabela de alarmes à parte com scripts para servidor.
Eu tenho um código antigo nesse sentido. Vou ver se consigo adaptá-lo para postar aqui.
Conforme prometido, segue o script de tabela de alarmes. Importante avisar que eu fiz esse script para um trabalho que tive com o Scada-LTS, então a aparência da tabela foi feita para atender às necessidades de um cliente específico, mas você pode editar o CSS e deixar o estilo da forma que preferir.
Código Javascript (insira como script para servidor)
// Configurar os títulos do cabeçalho da tabela
var titulo_nivel = "Nível de alarme";
var titulo_tempo = "Tempo de início";
var titulo_desc = "Descrição";
var titulo_status = "Status do evento";
var titulo_reconhecimento = "Reconhecimento";
var titulo_comando = "Comandos";
// Mostrar o texto do nível de alarme
var mostrar_nivel_alarme = true;
// Usar formatos mais longos de tempo (sempre exibir as datas)
var tempo_extenso = false;
// Mostrar comandos (reconhecer/silenciar)
var mostrar_comandos = true;
// MODIFICADO 03/02/2024
var uniqueClass = "evttable-" + pointComponent.id;
// FIM DA MODIFICAÇÃO
// Criação do HTML da tabela
var s = "";
s += "<div class='alarms-table " + uniqueClass + "'>";
s += "<table>";
// Cabeçalhos da tabela
s += "<thead>";
s += "<tr>";
s += "<th>" + titulo_nivel + "</th>";
s += "<th>" + titulo_tempo + "</th>";
s += "<th>" + titulo_desc + "</th>";
s += "<th>" + titulo_status + "</th>";
s += "<th>" + titulo_reconhecimento + "</th>";
// MODIFICADO 03/02/2024
if (mostrar_comandos) {
s += "<th>" + titulo_comando +
"<span style='margin-left: 4px;'><img class='cmd-btn' src='images/tick.png' title='Reconhecer todos' onclick='document.querySelectorAll(`." + uniqueClass + " td > .cmd-btn[src*=tick]:not([style*=hidden])`).forEach(elm => elm.click());'><img class='cmd-btn' src='images/sound_mute.png' title='Silenciar todos' onclick='MiscDwr.silenceAll();'>" +
"</span></th>";
}
// FIM DA MODIFICAÇÃO
s += "</tr>";
s += "</thead>";
// Corpo da tabela
s += "<tbody>";
var eventos = getActiveOrPendingEvents();
for (var i = 0; i < eventos.length; i++) {
var e = eventos[i];
var classe_linha = "";
classe_linha += e.isActive ? "evento-ativo " : "";
classe_linha += !e.isAcked ? "evento-pendente " : "";
s += "<tr class='" + classe_linha + "'>";
// Nível de alarme
var niveis = {0: "Nenhum", 1: "Informação", 2: "Urgente", 3: "Crítico", 4: "Risco de vida"};
var imagens = {
0: ['./images/flag_green_off.png', './images/flag_green.png'],
1: ['./images/flag_blue_off.png', './images/flag_blue.png'],
2: ['./images/flag_yellow_off.png', './images/flag_yellow.png'],
3: ['./images/flag_orange_off.png', './images/flag_orange.png'],
4: ['./images/flag_red_off.png', './images/flag_red.png']
};
s += "<td class='alarm-flag'>";
s += "<img src='" + imagens[e.alarmLevel][e.isActive] + "'>";
if (mostrar_nivel_alarme) {
s += niveis[e.alarmLevel];
}
"</td>";
// Tempo de início
s += "<td>" + e.activeTime + "</td>";
// Descrição e comentários
s += "<td><span>" + e.message + "</span>";
for (var j in e.comments) {
var c = e.comments[j];
s += "<span class='event-comment'>" + c.username + " - " + c.prettyTime + ": " + c.comment + "</span>";
}
s += "</td>";
// Status (tempo de inatividade)
s += "<td>" + e.statusMessage + "</td>";
// Reconhecimento (reconhecido/pendente)
if (e.isAcked) {
s += "<td> Reconhecido </td>";
} else {
s += "<td> Pendente </td>";
}
// Comandos
if (mostrar_comandos) {
s += "<td>";
var event_ref_code = com.serotonin.mango.vo.UserComment.TYPE_EVENT;
var sound_img = e.isSilenced ? "images/sound_mute.png" : "images/sound_none.png";
if (!e.isAcked) {
s += "<img class='cmd-btn' src='images/tick.png' title='Reconhecer' onclick='MiscDwr.acknowledgeEvent(" + e.id + ")'>";
} else {
s += "<img class='cmd-btn' style='visibility:hidden;' src='images/tick.png'>";
}
s += "<img class='cmd-btn' src='" + sound_img + "' title='Ativar/desativar som' onclick='MiscDwr.toggleSilence(" + e.id + ")'>";
s += "</td>";
}
s += "</tr>";
}
s += "<tbody>";
s += "</table>";
s += "</div>";
return s;
// Obter os eventos
function getActiveOrPendingEvents() {
// Carregar classes Java
var eventDao = include(org.scada_lts.mango.service.EventService, com.serotonin.mango.db.dao.EventDao);
var user = new com.serotonin.mango.Common().getUser();
// Consultar eventos pendentes no banco de dados
var pending = eventDao.getPendingEvents(user.getId());
// Consultar eventos ativos no banco de dados
var active = eventDao.getActiveEvents();
var evt1 = parseEvents(pending);
var evt2 = parseEvents(active);
if (evt1.length > 0) {
for (var i in evt2) {
var filter = evt1.filter(function(elm) { return elm.id == evt2[i].id; });
if (filter.length == 0) {
evt1.push(evt2[i]);
}
}
} else {
evt1 = evt2;
}
// Remover eventos com certas expressões
var list = ["entrou no sistema", "logged in"];
var modo_blacklist = true; // true: blacklist / false: whitelist
evt1 = evt1.filter(function(elm) {
for (var i in list) {
if (elm.message.toLowerCase().includes(list[i])) {
return !modo_blacklist;
}
}
return modo_blacklist;
});
evt1.sort(function(a, b) { return (b.id - a.id) });
return evt1;
}
function parseEvents(events) {
var eventArray = [];
var bundle = new com.serotonin.mango.Common().getBundle();
// Converter os dados para um objeto Javascript
for (var i = 0; i < events.size(); i++) {
var event = events.get(i);
var e = {};
// Dados básicos do evento
e.id = event.getId();
e.alarmLevel = event.getAlarmLevel();
e.isActive = Number(event.isActive());
e.isAcked = Number(event.isAcknowledged());
e.activeTime = tempo_extenso? event.getFullPrettyActiveTimestamp() : event.getPrettyActiveTimestamp();
e.message = sanitizeXss(event.getMessage().getLocalizedMessage(bundle)) || "";
e.comments = parseComments(event.getEventComments());
// Mensagem de status do evento (ativo/retornou/sem retorno)
e.statusMessage = "Ativo";
if (event.isRtnApplicable() && !event.isActive()) {
var rtnTime = tempo_extenso ? event.getFullPrettyRtnTimestamp() : event.getPrettyRtnTimestamp();
var rtnMessage = sanitizeXss(event.getRtnMessage().getLocalizedMessage(bundle));
e.statusMessage = rtnTime + " - " + rtnMessage;
} else if (!event.isRtnApplicable()) {
e.statusMessage = "\"Retornar ao normal\" desabilitado";
}
// Verificar se o evento está silenciado
if (event.isAlarm()) {
e.isSilenced = event.isSilenced();
}
eventArray.push(e);
}
// Retornar array com todos os eventos
return eventArray;
}
function parseComments(userComments) {
var comments = [];
if (!userComments) {
return [];
}
// Converter para um objeto Javascript
for (var i = 0; i < userComments.size(); i++) {
var uc = userComments.get(i);
var x = {};
x.comment = sanitizeXss(uc.getComment());
x.username = sanitizeXss(uc.getUsername());
x.timestamp = uc.getTs();
x.prettyTime = sanitizeXss(uc.getPrettyTime());
comments.push(x);
}
// Retornar um array com os comentários do evento
return comments;
}
function sanitizeXss(text) {
var sanitized = "";
// Prevents a loop at &'s replacement
String(text).split("&").forEach(function(value, index) {
if (index == 0) {
if (text.charAt(0) != "&") sanitized = value;
return;
}
if (value.search(";") > -1) {
sanitized += "&" + value;
} else {
sanitized += "&" + value;
}
});
return sanitized.replace(/</g, "<")
.replace(/>/g, ">").replace(/'/g, "'")
.replace(/"/g, '"').replace(/\//g, "/");
}
// Include Java classes conditionally (Scada-LTS compatibility feature)
function include(defaultClass, alternativeClass) {
try {
return defaultClass();
} catch (e) {
return alternativeClass();
}
}
Não posso prometer nada, mas tenho a intenção de em algum momento criar uma página no meu site com um repositório de modelos de scripts para servidor prontos. Se/quando eu fizer isso postarei no fórum o link.
Celso, ja testei um funcionou perfeitamente. Vou fuçar nas configurações e personalizar aqui do meu jeito. Cara, se vc conseguir um espaço no seu site dedicado a essas estripulias, com certeza será muito bem quisto, e vídeos né … na minha opinião, qualquer leigo assim como eu e com seus vídeos, consegue aprender scadaBR. Agradeço muito pelo seu empenho.
Parece que você está enfrentando um desafio interessante com o sistema SCADA, onde os alarmes não estão sendo devidamente filtrados por sistema ou operador, o que está causando confusão entre os operadores. Uma possível solução seria personalizar a visualização da lista de alarmes para permitir uma organização melhor, filtrando por sistema ou operador responsável pelo alarme. Vale a pena revisar o tópico descontinuado que você mencionou e tentar modificar as configurações ou o código para resolver esse problema.
Da mesma forma, se você precisar gerenciar as horas de trabalho dos operadores ou funcionários no seu projeto, pode ser interessante usar uma Calculadora de Horas. Essa ferramenta pode te ajudar a monitorar o tempo de cada operador em sistemas específicos, garantindo mais clareza e organização no seu projeto, assim como você gostaria de organizar os alarmes de acordo com a responsabilidade de cada operador.
Celso e meus amigos scadistas, resolvi retomar este assunto, visto que o negócio é bem complexo. Analisei os seus códigos e fui utilizando alguns trechos e avançando nos estudos, lembrando que testei os seus códigos originais e funcionam perfeitamente.
Estou aprendendo a linguagem JS e sei que a JS utilizada no scadaBR tem algumas limitações, mas acho que tem todas as classes disponíveis para a minha tentativa.
A minha idéia é conseguir gerar a lista de alarmes, personalizada como o seu código, mas com informações filtradas. Este filtro poderia ser por datasources (acho que é mais difícil) ou pela mensagem do alarme tipo “Gerador com falha de arrefecimento”. Na questão da mensagem, pensei em alterar as minha mensagens como por exemplo: “GGD: Gerador com falha de arrefecimento”, “GGD: Falha na Partida”, “TEL: Switch 2, Porta 6 down”, “DIS: No-Break em falha”, . Quero usar a sigla do sistema afetado para filtrar e listar os eventos. Podem me ajudar?
No script que eu passei existe uma variável de whitelist que permite filtrar os eventos por uma mensagem específica, dentro dessa lista você também pode passar o valor de um data point alfanumérico para alterar dinamicamente o texto do filtro.
Celso, baseado no seu código, criei esse que está ficando bom, e com as classes disponíveis no scadaBR. Neste caso estou apenas usando um script de servidor e aplicando o CSS e JS no mesmo código, acho que está ficando bom. Se der para vc olhar e dar umas dicas para melhorar, ficarei muito grato. Estou com um pouco de dificulade na parte de reconhecimento dos alarmes … mas vou atacar mais amanhã, estou aprendendo JS e CSS agora, embora eu conheça um pouco de C++, ainda falta bastante pra aprender:
var eventDao = new com.serotonin.mango.db.dao.EventDao();
var activeEvents = eventDao.getActiveEvents();
var resultado = "";
var sequencia = "GGD"; // Sequência de caracteres a ser filtrada
// Função para formatar a data e o horário
function formatarDataHora(timestamp) {
var data = new Date(timestamp);
var dia = data.getDate().toString().padStart(2, '0');
var mes = (data.getMonth() + 1).toString().padStart(2, '0'); // Meses são indexados a partir de 0
var ano = data.getFullYear();
var horas = data.getHours().toString().padStart(2, '0');
var minutos = data.getMinutes().toString().padStart(2, '0');
var segundos = data.getSeconds().toString().padStart(2, '0');
return dia + "/" + mes + "/" + ano + " " + horas + ":" + minutos + ":" + segundos;
}
// Função para obter
function obterIconeNivelAlarme(nivelAlarme) {
var icones = {
0: './images/flag_green.png',
1: './images/flag_blue.png',
2: './images/flag_yellow.png',
3: './images/flag_orange.png',
4: './images/flag_red.png'
};
return "<img src='" + icones[nivelAlarme] + "' alt='Nível de Alarme " + nivelAlarme + "' class='icone-nivel-alarme'>";
}
// Função para obter a classe CSS do nível de alarme
function obterClasseNivelAlarme(nivelAlarme) {
var classes = {
0: 'nivel-verde',
1: 'nivel-azul',
2: 'nivel-amarelo',
3: 'nivel-laranja',
4: 'nivel-vermelho'
};
return classes[nivelAlarme];
}
// Função para processar eventos e gerar linhas da tabela
function processarEventos(eventos, filtrarNaoReconhecidos) {
var linhas = "";
for (var i = 0; i < eventos.size(); i++) {
var evento = eventos.get(i);
if (!filtrarNaoReconhecidos || !evento.isAcknowledged()) {
var bundle = new com.serotonin.mango.Common().getBundle(); // Obter o bundle para tradução
var conteudoMensagem = evento.getMessage().getLocalizedMessage(bundle); // Obter o conteúdo da mensagem
if (conteudoMensagem.includes(sequencia)) { // Filtrar por sequência de caracteres
var nivelAlarme = evento.getAlarmLevel(); // Obter nível de alarme
var dataHoraFormatada = formatarDataHora(evento.getActiveTimestamp()); // Formatar data e horário
var iconeNivelAlarme = obterIconeNivelAlarme(nivelAlarme); // Obter ícone do nível de alarme
var classeNivelAlarme = obterClasseNivelAlarme(nivelAlarme); // Obter classe CSS do nível de alarme
linhas += "<tr class='" + classeNivelAlarme + "'><td>" + evento.getId() + "</td><td>" + dataHoraFormatada + "</td><td class='centralizado'>" + iconeNivelAlarme + "</td><td>" + conteudoMensagem + "</td></tr>";
}
}
}
return linhas;
}
// Gerar cabeçalho da tabela
resultado += "<table class='tabela-eventos'><thead><tr><th>ID</th><th>Data e Horário</th><th>Nível de Alarme</th><th>Mensagem</th></tr></thead><tbody>";
// Processar eventos ativos e não reconhecidos
resultado += processarEventos(activeEvents, false);
resultado += processarEventos(activeEvents, true);
// Fechar a tabela
resultado += "</tbody></table>";
// Adicionar estilo CSS com dimensões máximas e personalização de linhas
resultado += "<style>";
resultado += ".tabela-eventos { width: 100%; border-collapse: collapse; max-width: 800px; max-height: 400px; overflow: auto; }";
resultado += ".tabela-eventos th, .tabela-eventos td { border: 1px solid #ddd; padding: 8px; }";
resultado += ".tabela-eventos th { background-color: #f2f2f2; text-align: left; }";
resultado += ".tabela-eventos tr:nth-child(even) { background-color: #f9f9f9; }";
resultado += ".tabela-eventos tr:hover { background-color: #ddd; }";
resultado += ".tabela-eventos img { width: 20px; height: 20px; }"; // Ajustar tamanho dos ícones
resultado += ".centralizado { text-align: center; vertical-align: middle; }"; // Centralizar conteúdo da célula
resultado += ".icone-nivel-alarme { display: block; margin-left: auto; margin-right: auto; }"; // Centralizar imagem
resultado += ".nivel-verde { background-color: #e0f2e0; color: #006400; font-weight: bold; }"; // Personalização para nível verde
resultado += ".nivel-azul { background-color: #e0e0f2; color: #0000cd; font-weight: bold; }"; // Personalização para nível azul
resultado += ".nivel-amarelo { background-color: #f2f2e0; color: #ffd700; font-weight: bold; }"; // Personalização para nível amarelo
resultado += ".nivel-laranja { background-color: #f2e0e0; color: #ff8c00; font-weight: bold; }"; // Personalização para nível laranja
resultado += ".nivel-vermelho { background-color: #f2e0e0; color: #ff0000; font-weight: bold; }"; // Personalização para nível vermelho
resultado += "</style>";
return resultado;