Table of Contents

Работа с Telegram API внутри OpenSCADA.

Для чего это надо? Я бы использовал только для уведомлений. Некоторые, возможно, захотят и управление реализовывать через команды боту. Минимально для работы нужно реализовать два метода: отправка сообщения и прием.

Всю базовую информацию по созданию ботов в Telegram можно прочитать в тысяче статей по всему интернету, но если парой слов, то для создания бота вам нужно написать @BotFather и получить ключ API.

Подготовка к работе

Требуется создать транспорт для общения с серверами Telegram. Для этого в дереве проекта переходим в раздел Транспорты > SSL и создаем выходной транспорт. Имя можно выбрать любое, но в библиотеке, приложенной к статье, используется имя по умолчанию Telegram. Требуется именно SSL, а не просто сокет.

В транспорте нельзя указывать протокол HTTPS, но известно, что для обмена через HTTPS используется порт 443. Адрес у транспорта указываем следующий:

api.telegram.org:443

Отправка сообщения

Для отправки используется метод sendMessage Telegram API. В качестве аргументов ему требуется передать chat_id и текст. Аргументов может быть и больше, но в базовом варианте, когда передаем только текст, этого достаточно.

В коде ниже есть 4 переменных.

ИмяТипОписание
TransportСтрокаИмя транспорта в “Транспорты > SSL”
APIKeyСтрокаКлюч, полученный от @BotFather
ChatIDЦелочисленныйУникальный идентификатор переписки
MessageСтрокаТекст сообщения
tr = SYS.Transport.SSL.nodeAt("out_" + Transport);
 
req = SYS.XMLNode("POST");
req.setAttr("URI", "/bot" + APIKey + "/sendMessage");
req.childAdd("cnt").setAttr("name", "chat_id").setText(ChatID.toString());
req.childAdd("cnt").setAttr("name", "text").setText(Message);
req.childAdd("cnt").setAttr("name", "disable_notification").setText("true");
tr.messIO(req,"HTTP");

Получение сообщений

Для получения сообщений используется метод getUpdates Telegram API. Ответы от сервера приходят в формате JSON. Данный формат не поддерживается “из коробки”, однако сообществом разработана библиотека, которая преобразует JSON строку в объект, с которым удобно работать в JavaLikeCalc.

В коде ниже используются 3 переменных.

ИмяТипОписание
TransportСтрокаИмя транспорта в “Транспорты > SSL”
APIKeyСтрокаКлюч, полученный от @BotFather
updateIdЦелочисленныйID последнего полученного сообщения, при первом запуске значение 0
jsonLib = SYS.DAQ.JavaLikeCalc.lib_Json;
 
tr = SYS.Transport.SSL.nodeAt("out_" + Transport);
 
messagesCount = 1;
 
while(messagesCount){
    updateId += 1;
    req = SYS.XMLNode("POST");
    req.setAttr("URI", "/bot" + APIKey + "/getUpdates");
    req.childAdd("cnt").setAttr("name", "offset").setText(updateId.toString());
    tr.messIO(req,"HTTP");
 
    deser_err = "";
    outputObject = jsonLib.deserialize(req.text(), deser_err);
 
    messagesCount = outputObject.result.length;
    for (var i = 0; i < outputObject.result.length; i++){
        item = outputObject.result[i];
        updateId = max(updateId, item.update_id);
        username = item.message.from.username;
        chatID = item.message.from.id;
        messageText = item.message.text;
    }
}

Данный код будет загружать все доступные сообщения. Между вызовами требуется хранить последнее значение updateId, так как этот параметр позволяет пропускать уже обработанные сообщения

Библиотека для работы

Данные примеры кода стоит оформить в виде библиотеки в Сбор данных → Вычислитель на Java-подобном языке → Библиотека. Так будет гораздо удобнее использовать код повторно.

Отправка оповещений группе пользователей

Обычно телеграм бот нужен для оповещений об аварийных ситуациях. И оповещать приходится группу сотрудников.

Где брать список абонентов, которым рассылать оповещения? Логично было бы использовать список пользователей OpenSCADA и добавлять их в дополнительную группу. Так будет проще управлять списком получателей.

Где хранить ID пользователя Telegram? Логично, что в описании пользователя OpenSCADA. В поле “Описание” можно хранить все, что необходимо. Если таких параметров конфигурации требуется больше чем один, то не проблема. По одному в строку в формате <TYPE>:<VALUE>. В моем случае в данном поле хранятся телефон, адрес почты и ID пользователя Telegram.

В коде ниже есть 4 переменных.

ИмяТипОписание
TransportСтрокаИмя транспорта в “Транспорты > SSL”
GroupСтрокаИмя группы пользователей для оповещения
APIKeyСтрокаКлюч, полученный от @BotFather
MessageСтрокаСообщение
TelegramLib = SYS.DAQ.JavaLikeCalc.lib_Telegram;
 
users = SYS.Security.nodeList("usr_");
 
for (var j = 0; j < users.length; j++){
    user = SYS.Security.nodeAt(users[j]);
 
    // Пропуск пользователей, которым не разрешены уведомления
    if (user.groups().indexOf(Group) == -1) continue;
 
    // Обработка сохраненных транспортов для уведомлений
    transports = user.cfg("LONGDESCR");
    for(offset = 0; (transport = transports.parse(0, "\n", offset)).length; ){
        tmp = transport.split(":");
        TransportType = tmp[0];
        TransportValue = tmp[1];
 
        if (TransportType == "telegram"){
            TelegramLib.SendMessage(Transport, APIKey, TransportValue, Message);
            break;
        }
    }
}

Отправка журнала ошибок

Выше есть код для массовой отправки сообщений, но добавлять в каждый обработчик датчика код для отправки - плохая идея. Обработчик должен формировать сообщение в журнал, а отдельный виртуальный контроллер отправлять сообщения из журнала по какому-то каналу связи.

При помощи функции SYS.Archive.messGet запрашиваем все сообщения за определенный период, которые подходят под указанную категорию. Категорией может быть просто звездочка для всех сообщений или немного сложнее. Можно и регулярным выражением воспользоваться при необходимости.

В коде ниже есть 7 переменных.

ИмяТипОписание
TelegramTransportСтрокаИмя транспорта в “Транспорты > SSL”
TelegramAPIKeyСтрокаКлюч, полученный от @BotFather
TelegramGroupСтрокаИмя группы пользователей для оповещения через Telegram
MessagesLevelСтрокаМинимальный уровень сообщений из журнала
MessagesCatСтрокаКатегория сообщений из журнала
TimeBeginЦелыйВремя начала
utmLastЦелыйМикросекунды последнего сообщения
using Special.FLibSYS;
TelegramLib = SYS.DAQ.JavaLikeCalc.lib_Telegram;
 
if (f_start){	
    TimeBegin = SYS.time();
}
 
// Запрос сообщений по шаблону
TimeFinish = SYS.time();
messages = SYS.Archive.messGet(TimeBegin, TimeFinish, MessagesCat, MessagesLevel);
 
// Обновляем время для следующей итерации
TimeBegin = TimeFinish;
utmLast = -1;
for (var i = 0; i < messages.length; i++){
    message = messages[i];
    tm = message.tm;
    if ((TimeBegin == tm) && (utmLast > message.utm)) continue;
    utmLast = message.utm; // Сохраняем микросекунды для следующей итерации
    TimeStr = tmFStr(tm,"%d.%m.%Y %H:%M:%S");
    messageText = TimeStr + ": " + message.mess;
 
    TelegramLib.SendAll(TelegramTransport, TelegramGroup, TelegramAPIKey, messageText);
}