NEWS
Построение графиков ChartJS на сервере и отправка картинки в телеграм
-
Расскажу, как можно получить график в виде картинки, например для отправки в Telegram
В ioBroker есть стандартный способ построения графиков - Flot-драйвер. Этот драйвер работает в паре с Web-драйвером и отображает результат в браузере. Но для того чтобы получить созданный график на сервере (в скрипте) в виде картинки нужен дополнительный драйвер PhantomJS, который делает “скриншот” страницы (на которой у нас нарисуется Flot-график).
Но я расскажу об альтернативном способе построения графиков на сервере в скрипте.
Есть такая библиотека Chart.js http://www.chartjs.org/ которая позволяет рисовать приятные на вид графики в браузере (примеры http://www.chartjs.org/samples/latest/).
Для рисования она использует “холст” (канва, canvas) браузера. Поэтому, чтобы рисовать с помощью этой библиотеки на сервере, нужно использовать “серверный” вариант “холста” и DOM-объекты. Это и делает пакет chartjs-node (https://github.com/vmpowerio/chartjs-node).
Основной зависимостью для этого пакета является пакет canvas (https://github.com/Automattic/node-canvas), который следует установить глобально (или в папку iobroker). Важно установить все зависимости для той платформы, куда вы ставите https://github.com/Automattic/node-canvas#compiling .
После этого можно в настройках драйвера javascript добавить модули chart.js, chartjs-node. Они должны установиться корректно, без ошибок. Иначе - разбираться с ошибками и решать их.
А дальше, можно написать скрипт.
Ниже приведен скрипт для примера, т.к. в нем включено использование драйвера History и используются конкретные имена состояний.
Внимание! В скрипте есть сложные для новичков конструкции - Promise. Это удобные способ не писать функции с callback, а делать цепочки шагов. Так, например, это удобно делать для получения данных из истории состояний.
Скрипт подписывается на 2 команды, приходящие через Телеграм:
demo - строит график на готовых тестовых данных (чтобы проверить, что код вообще работает). Вызываются функции sendGraph0, prepareDraw0, doDraw.
graph - строит график на данных из истории состояний. Вызываются функции sendGraph1, prepareDraw1, doDraw.
'use strict'; const ChartjsNode = require('chartjs-node'); /** * функция sendTo как Promise, чтобы удобно было строить цепочки */ function sendToPromise(adapter, cmd, params) { return new Promise((resolve, reject) => { sendTo(adapter, cmd, params, (result) => { resolve(result); }); }); } // константы для цветов const chartColors = { black: 'rgb(0, 0, 0)', red: 'rgb(255, 99, 132)', orange: 'rgb(255, 159, 64)', yellow: 'rgb(255, 205, 86)', green: 'rgb(75, 192, 192)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', grey: 'rgb(201, 203, 207)' }; /** * функция рисования и сохранения картинки в файл * параметры: * @param config - конфигурация графика для рисования * @param filename - имя файла для сохранения * результат: * @param Promise - успешное сохранение файла */ function doDraw(config, filename) { // создадим полотно с размером 640x480 пикселей var chartNode = new ChartjsNode(640, 480); return chartNode.drawChart(config) .then(() => { // запишем результат в файл return chartNode.writeImageToFile('image/png', filename); }); } /** * функция подготовки параметров для ChartJS. * результат: * @param Promise - успешная подготовка параметров */ function prepareDraw0(){ // переменная, куда сохраним данные var пример; // создадим Promise сборки данных и конфигурации return new Promise((resolve, reject)=>{resolve()}) // здесь могут быть много шагов сбора данных, прежде чем перейти к графику .then(()=>{ // произвольные данные, похожие на те, что хранятся в истории пример = [ {"val":3,"ack":1,"ts":1539063874301}, {"val":5,"ack":1,"ts":1539063884299}, {"val":5.3,"ack":1,"ts":1539063894299}, {"val":3.39,"ack":1,"ts":1539063904301}, {"val":5.6,"ack":1,"ts":1539063914300}, {"val":-1.3,"ack":1,"ts":1539063924300}, {"val":-6.3,"ack":1,"ts":1539063934302}, {"val":1.23,"ack":1,"ts":1539063944301}, ]; }) // финальный шаг - создаем конфигурацию графиков .then(()=>{ const chartJsOptions = { // тип графика - линейный type: 'line', data: { // список наборов данных datasets: [ { // заголовок ряда label: 'тест', // цвет backgroundColor: chartColors.black, borderColor: chartColors.black, // размер точек pointRadius: 3, // ширина линии графика borderWidth: 3, // достанем данные из переменной 'пример' и оставим только значение и время изменения data: пример.map((item) => { return {y: item.val, t: new Date(item.ts)} }), // заливка графика - нет fill: false, } ] }, options: { // настройка легенды legend: { labels: { // размер шрифта fontSize: 20, }, }, // оси координат scales: { // оси X xAxes: [{ // тип - временная ось type: 'time', display: true, // метка оси scaleLabel: { display: true, labelString: 'Время' }, }], // оси Y yAxes: [{ // тип - линейная type: 'linear', display: true, // метка оси scaleLabel: { display: true, labelString: 'Температура' }, }] } } }; return chartJsOptions; }); } /** * функция подготовки параметров для ChartJS. * собирает данные из истории и складывает их в переменные, * чтобы потом включить в ряды. * * параметры: * @param hours - количество часов, за которые получить данные * результат: * @param Promise - успешная подготовка параметров */ function prepareDraw1(hours){ // вычислим интервал времени, за который надо получить данные const end = new Date().getTime(), start = end - 3600000*(hours || 1); // 1 = час назад // зададим переменные, в которые будем складывать результаты запроса // исторических данных var улица, куры2, куры1, куры2свет, куры2вент; // создадим Promise сборки данных и конфигурации return new Promise((resolve, reject)=>{resolve()}) // на этом шаге собираем историю по 'mqtt.0.ESP_Easy.Улица.Temperature' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.ESP_Easy.Улица.Temperature', options: { start: start, end: end, aggregate: 'onchange' } } ).then((result) => { // записываем результат в переменную 'улица' улица = result.result; }); }) // на этом шаге собираем историю по 'sonoff.0.chicken2.DS18B20_Temperature' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.DS18B20_Temperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ // записываем результат в переменную 'куры2' куры2 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.sonoff_chicken_vent.DS18B20_Temperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры1 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.POWER1', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры2свет = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.POWER2', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры2вент = result.result; }); }) // финальный шаг - создаем конфигурацию графиков .then(()=>{ const chartJsOptions = { // тип графика - линейный type: 'line', data: { // список наборов данных datasets: [ { // заголовок ряда с указанием последнего значения из ряда в скобках label: 'Улица ('+улица[улица.length - 1].val+')', // цвет backgroundColor: chartColors.blue, borderColor: chartColors.blue, // размер точек. 0 - нет точки pointRadius: 0, // ширина линии графика borderWidth: 3, // достанем данные из переменной 'улица' и оставим только значение и время изменения data: улица.map((item) => { return {y: item.val, t: new Date(item.ts)} }), // заливка графика - нет fill: false, // идентификатор оси Y yAxisID: 'y-axis-1', },{ label: 'Куры 1 ('+куры1[куры1.length - 1].val+')', backgroundColor: chartColors.green, borderColor: chartColors.green, pointRadius: 0, borderWidth: 3, data: куры1.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: 'Куры 2 ('+куры2[куры2.length - 1].val+')', backgroundColor: chartColors.red, borderColor: chartColors.red, pointRadius: 0, borderWidth: 3, data: куры2.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: 'Куры 2 свет ('+куры2свет[куры2свет.length - 1].val+')', backgroundColor: chartColors.yellow, borderColor: chartColors.yellow, pointRadius: 0, borderWidth: 1, data: куры2свет.map((item) => { return {y: (item.val) ? 1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', },{ label: 'Куры 2 вент ('+куры2вент[куры2вент.length - 1].val+')', backgroundColor: chartColors.grey, borderColor: chartColors.grey, pointRadius: 0, borderWidth: 1, data: куры2вент.map((item) => { return {y: (item.val) ? -1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', } ] }, options: { // настройка легенды legend: { labels: { // размер шрифта fontSize: 20, }, }, // оси координат scales: { // оси X xAxes: [{ // тип - временная ось type: 'time', display: true, // метка оси scaleLabel: { display: true, labelString: 'Время' }, // настройка формата оси (времени) time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, }], // оси Y yAxes: [{ // тип - линейная type: 'linear', display: true, // метка оси scaleLabel: { display: true, labelString: 'Температура' }, // расположение линейки - слева position: 'left', // идентификатор оси id: 'y-axis-1', },{ type: 'linear', display: true, scaleLabel: { display: true, labelString: 'Свет и вентиляция' }, ticks: { min: -4, max: 2 }, // расположение линейки - справа position: 'right', id: 'y-axis-2', }] } } }; return chartJsOptions; }); } function sendGraph0(user){ // имя файла, в который положим картинку с графиком const filename = '/tmp/graph0.png'; // выполним подготовку данных prepareDraw0() // на след шаге нарисуем .then((result) => { // рисуем картинку по полученным данным и конфигурации return doDraw(result, filename); }) .then(()=>{ // теперь отправим сообщение в телеграм sendTo('telegram.0', { user: user, text: filename, caption: 'Пример графика', }); }) .catch((err)=>{ console.error(err); }); } /** * функция отправки графика в телеграм * @param user - какому юзеру слать. если пусто - всем * @param chat_id - * @param message_id - в каком чате и какое сообщение заменить при обновлении * @param hours - количество часов, за которые получить данные */ function sendGraph1(user, chat_id, message_id, hours){ // имя файла, в который положим картинку с графиком const filename = '/tmp/graph1.png'; hours = hours || 1; // выполним подготовку данных prepareDraw1(hours) // на след шаге нарисуем .then((result) => { // рисуем картинку по полученным данным и конфигурации return doDraw(result, filename); }) .then(() => { // удалим предыдущее сообщение if (message_id && chat_id) { sendTo('telegram', { user: user, deleteMessage: { options: { chat_id: chat_id, message_id: message_id } } }); } }) .then(()=>{ // теперь отправим сообщение в телеграм sendTo('telegram.0', { user: user, text: filename, caption: 'Температура в курятниках ('+hours+'ч)', reply_markup: { inline_keyboard: [ [ { text: '🔄', callback_data: 'graph_'+hours}, { text: '1 ч', callback_data: 'graph_1' }, { text: '2 ч', callback_data: 'graph_2' }, { text: '4 ч', callback_data: 'graph_4' }, { text: '12 ч', callback_data: 'graph_12' }, { text: '24 ч', callback_data: 'graph_24' }, ] ] } }); }) .catch((err)=>{ console.error(err); }); } // будем слушать телеграм и ждать команды на построение графика on({id: "telegram.0.communicate.request", ack: false, change: 'any'}, function (obj) { var v; var msg = obj.state.val; var command = obj.state.val.substring(obj.state.val.indexOf(']')+1); var user = obj.state.val.substring(obj.state.val.indexOf('[')+1,obj.state.val.indexOf(']')); var chat_id = getState("telegram.0.communicate.requestChatId").val; var message_id = getState("telegram.0.communicate.requestMessageId").val; // команда для графика - demo if (command == 'demo') { sendGraph0(user); } // команда для графика - graph if (command.startsWith('graph')) { const hours = parseInt(command.split('_')[1]); sendGraph1(user, chat_id, message_id, hours); } });
-
Не могу понять, где засада… Команда - демо, отправляет файл в адаптер телеграм... А команда - граф, отправляет что-то в адаптер хистори, в телеграм при этом пусто 8-)
-
Всё верно, команда graph сперва достает из истории данные (отправляет сообщение в History и получает ответ), а затем строит график.
Вам нужно поменять скрипт и указать свои состояния, по которым надо получить данные. Ну и удалить ненужные запросы данных, лишние ряды на графике. Т.е. нужно разобраться со скриптом и поменять его под себя
-
/** * функция sendTo как Promise, чтобы удобно было строить цепочки */ var sendToPromise = promisifyNoError(sendTo); ````А тут никакой ошибки нет? Я ставлю свои пять состояний, которые есть в хистори, но это ничего не меняет (( Вот такое в логе скрипта после приёма команды граф: ` > javascript.1 script.js.test.Скрипт1: sendTo(adapter=function (result) { // decide on how we want to return the callback arguments switch (arguments.length) { case 0: // no arguments were given return resolve(); // Promise case 1: // a single value (result) was returned return resolve(result); default: // multiple values should be returned /** @type {{} | any[]} */ let ret; const extraArgs = sliceArgs(arguments, 0); if (returnArgNames && returnArgNames.length === extraArgs.length) { // we can build an object ret = {}; for (let i = 0; i < returnArgNames.length; i++) { ret[returnArgNames_~~[i]~~] = extraArgs__; } } else { // we return the raw array ret = extraArgs; } return resolve(ret); } }, cmd=undefined, msg=undefined)___ `
-
Ошибки быть не должно - это оборачивание функции sendTo в промис.
Выше должны быть 2 вспомогательные функции для этого (promisifyNoError и sliceArgs).
Можно переписать скрипт, чтобы использовалась обычная функция sendTo, но тогда надо в callback передавать следующую функцию и т.д.
-
Покажите какая у вас получилась функция prepareDraw?
-
/** * функция подготовки параметров для ChartJS. * собирает данные из истории и складывает их в переменные, * чтобы потом включить в ряды. * * параметры: * @param hours - количество часов, за которые получить данные * результат: * @param Promise - успешная подготовка параметров */ function prepareDraw1(hours){ // вычислим интервал времени, за который надо получить данные const end = new Date().getTime(), start = end - 3600000*(hours || 1); // 1 = час назад // зададим переменные, в которые будем складывать результаты запроса // исторических данных var улица, куры2, куры1, куры2свет, куры2вент; // создадим Promise сборки данных и конфигурации return new Promise((resolve, reject)=>{resolve()}) // на этом шаге собираем историю по 'mqtt.0.Borovoe1.bmet' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.Borovoe1.bmet', options: { start: start, end: end, aggregate: 'onchange' } }) .then((result) => { // записываем результат в переменную 'улица' улица = result.result; }); }) // на этом шаге собираем историю по 'sonoff.0.chicken2.DS18B20_Temperature' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.Sonoff.dsw1', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ // записываем результат в переменную 'куры2' куры2 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'zigbee.0.00158d0001dbd5d6.temperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры1 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.Sonoff.output12', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры2свет = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'parser.0.cpuTemperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ куры2вент = result.result; }); }) // финальный шаг - создаем конфигурацию графиков .then(()=>{ const chartJsOptions = { // тип графика - линейный type: 'line', data: { // список наборов данных datasets: [ { // заголовок ряда с указанием последнего значения из ряда в скобках label: 'Улица ('+улица[улица.length - 1].val+')', // цвет backgroundColor: chartColors.blue, borderColor: chartColors.blue, // размер точек. 0 - нет точки pointRadius: 0, // ширина линии графика borderWidth: 3, // достанем данные из переменной 'улица' и оставим только значение и время изменения data: улица.map((item) => { return {y: item.val, t: new Date(item.ts)} }), // заливка графика - нет fill: false, // идентификатор оси Y yAxisID: 'y-axis-1', },{ label: 'Куры 1 ('+куры1[куры1.length - 1].val+')', backgroundColor: chartColors.green, borderColor: chartColors.green, pointRadius: 0, borderWidth: 3, data: куры1.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: 'Куры 2 ('+куры2[куры2.length - 1].val+')', backgroundColor: chartColors.red, borderColor: chartColors.red, pointRadius: 0, borderWidth: 3, data: куры2.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: 'Куры 2 свет ('+куры2свет[куры2свет.length - 1].val+')', backgroundColor: chartColors.yellow, borderColor: chartColors.yellow, pointRadius: 0, borderWidth: 1, data: куры2свет.map((item) => { return {y: (item.val) ? 1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', },{ label: 'Куры 2 вент ('+куры2вент[куры2вент.length - 1].val+')', backgroundColor: chartColors.grey, borderColor: chartColors.grey, pointRadius: 0, borderWidth: 1, data: куры2вент.map((item) => { return {y: (item.val) ? -1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', } ] }, options: { // настройка легенды legend: { labels: { // размер шрифта fontSize: 20, }, }, // оси координат scales: { // оси X xAxes: [{ // тип - временная ось type: 'time', display: true, // метка оси scaleLabel: { display: true, labelString: 'Время' }, // настройка формата оси (времени) time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, }], // оси Y yAxes: [{ // тип - линейная type: 'linear', display: true, // метка оси scaleLabel: { display: true, labelString: 'Температура' }, // расположение линейки - слева position: 'left', // идентификатор оси id: 'y-axis-1', },{ type: 'linear', display: true, scaleLabel: { display: true, labelString: 'Свет и вентиляция' }, ticks: { min: -4, max: 2 }, // расположение линейки - справа position: 'right', id: 'y-axis-2', }] } } }; return chartJsOptions; }); }
Я только подставил пять своих состояний, больше ничего не менял
-
хм. по всем этим состояниям ведется история и она есть на запрашиваемый период?
тогда надо начать с простого варианта. переделываем пример prepareDraw0 и добавляем туда получение истории одного состояния, вместо заданных значений. сможете это сделать? это позволит понять работает ли получение истории.
может быть дело в NodeJS или версии контроллера или еще в чем-то…
-
По всем есть история, буду пробовать переделывать демо…
-
Б-р-р-р… а в логах что?
Попробуем получить историю без промиса:
function prepareDraw0(hours){ // вычислим интервал времени, за который надо получить данные const end = new Date().getTime(), start = end - 3600000*(hours || 1); // 1 = час назад // переменная, куда сохраним данные var пример; // создадим Promise сборки данных и конфигурации return new Promise((resolve, reject)=>{ sendTo('history.0', 'getHistory', { id: 'mqtt.0.Borovoe1.bmet', options: { start: start, end: end, aggregate: 'onchange' } }, (result) => { пример = result.result; resolve(); }) }) // финальный шаг - создаем конфигурацию графиков .then(()=>{
-
` > javascript.1 2018-10-17 12:47:21.360 info }, cmd=undefined, msg=undefined)
javascript.1 2018-10-17 12:47:21.360 info }
javascript.1 2018-10-17 12:47:21.360 info return resolve(ret);
javascript.1 2018-10-17 12:47:21.360 info }
javascript.1 2018-10-17 12:47:21.360 info ret = extraArgs;
javascript.1 2018-10-17 12:47:21.360 info // we return the raw array
javascript.1 2018-10-17 12:47:21.360 info } else {
javascript.1 2018-10-17 12:47:21.360 info }
javascript.1 2018-10-17 12:47:21.360 info ret[returnArgNames_
[i]] = extraArgs__;javascript.1 2018-10-17 12:47:21.360 info for (let i = 0; i < returnArgNames.length; i++) {
javascript.1 2018-10-17 12:47:21.360 info ret = {};
javascript.1 2018-10-17 12:47:21.360 info // we can build an object
javascript.1 2018-10-17 12:47:21.360 info if (returnArgNames && returnArgNames.length === extraArgs.length) {
javascript.1 2018-10-17 12:47:21.360 info const extraArgs = sliceArgs(arguments, 0);
javascript.1 2018-10-17 12:47:21.360 info let ret;
javascript.1 2018-10-17 12:47:21.360 info /** @type {{} | any[]} */
javascript.1 2018-10-17 12:47:21.360 info default: // multiple values should be returned
javascript.1 2018-10-17 12:47:21.360 info return resolve(result);
javascript.1 2018-10-17 12:47:21.360 info case 1: // a single value (result) was returned
javascript.1 2018-10-17 12:47:21.360 info return resolve(); // Promise <void>javascript.1 2018-10-17 12:47:21.360 info case 0: // no arguments were given
javascript.1 2018-10-17 12:47:21.360 info switch (arguments.length) {
javascript.1 2018-10-17 12:47:21.360 info // decide on how we want to return the callback arguments
javascript.1 2018-10-17 12:47:21.360 info script.js.test.Скрипт2: sendTo(adapter=function (result) {
javascript.1 2018-10-17 12:47:21.357 info script.js.test.Скрипт2: getState(id=telegram.0.communicate.requestMessageId, timerId=0) => {"val":19302,"ack":false,"ts":1539769641330,"q":0,"from":"system.adapter.telegram.0","lc":1539769641330}
javascript.1 2018-10-17 12:47:21.355 info script.js.test.Скрипт2: getState(id=telegram.0.communicate.requestChatId, timerId=0) => {"val":289938044,"ack":false,"ts":1539769641319,"q":0,"from":"system.adapter.telegram.0","lc":1538911049076}</void>___ Отвлекли, вот лог, это ещё с промисом. Что из него можно узнать? `
-
в нем нет ошибок странно, что он текст функции вываливает в лог… у меня только sendTo
может инстанс history не 0?
-
Какая версия Nodejs?
-
Хистори.0… Не знаю, как вставить картинку Ноде 8.12.0
Щас код такой:
`'use strict'; const ChartjsNode = require('chartjs-node'); /*** Вспомогательные функции (взяты из js-controller) ***/ /** * Puts all values from an `arguments` object into an array, starting at the given index * @param {IArguments} argsObj An `arguments` object as passed to a function * @param {number} [startIndex=0] The optional index to start taking the arguments from */ function sliceArgs(argsObj, startIndex) { if (startIndex === null) startIndex = 0; const ret = []; for (let i = startIndex; i < argsObj.length; i++) { ret.push(argsObj[i]); } return ret; } /** * Promisifies a function which does not provide an error as the first argument in its callback * @param {Function} fn The function to promisify * @param {any} [context] (optional) The context (value of `this` to bind the function to) * @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments, * you can combine them into one object by passing the names as an array. * Otherwise the Promise will resolve with an array * @returns {(...args: any[]) => Promise<any>} */ function promisifyNoError(fn, context, returnArgNames) { return function () { const args = sliceArgs(arguments); context = context || this; return new Promise(function (resolve, reject) { fn.apply(context, args.concat([ function (result) { // decide on how we want to return the callback arguments switch (arguments.length) { case 0: // no arguments were given return resolve(); // Promise <void>case 1: // a single value (result) was returned return resolve(result); default: // multiple values should be returned /** @type {{} | any[]} */ let ret; const extraArgs = sliceArgs(arguments, 0); if (returnArgNames && returnArgNames.length === extraArgs.length) { // we can build an object ret = {}; for (let i = 0; i < returnArgNames.length; i++) { ret[returnArgNames[i]] = extraArgs[i]; } } else { // we return the raw array ret = extraArgs; } return resolve(ret); } } ])); }); }; } /** * функция sendTo как Promise, чтобы удобно было строить цепочки */ var sendToPromise = promisifyNoError(sendTo); // константы для цветов const chartColors = { black: 'rgb(0, 0, 0)', red: 'rgb(255, 99, 132)', orange: 'rgb(255, 159, 64)', yellow: 'rgb(255, 205, 86)', green: 'rgb(75, 220, 150)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', grey: 'rgb(201, 203, 207)' }; /** * функция рисования и сохранения картинки в файл * параметры: * @param config - конфигурация графика для рисования * @param filename - имя файла для сохранения * результат: * @param Promise - успешное сохранение файла */ function doDraw(config, filename) { // создадим полотно с размером 640x480 пикселей var chartNode = new ChartjsNode(640, 480); return chartNode.drawChart(config) .then(() => { // запишем результат в файл return chartNode.writeImageToFile('image/png', filename); }); } /** * функция подготовки параметров для ChartJS. * результат: * @param Promise - успешная подготовка параметров */ function prepareDraw0(hours){ // вычислим интервал времени, за который надо получить данные const end = new Date().getTime(), start = end - 3600000*(hours || 1); // 1 = час назад // переменная, куда сохраним данные var пример; // создадим Promise сборки данных и конфигурации return new Promise((resolve, reject)=>{resolve()}) // здесь могут быть много шагов сбора данных, прежде чем перейти к графику .then(()=>{ return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.Borovoe1.bmet', options: { start: start, end: end, aggregate: 'onchange' } }) .then((result) => { // записываем результат в переменную 'пример' пример = result.result; }); }) // финальный шаг - создаем конфигурацию графиков .then(()=>{ const chartJsOptions = { // тип графика - линейный type: 'line', data: { // список наборов данных datasets: [ { // заголовок ряда label: 'проба', // цвет backgroundColor: chartColors.green, borderColor: chartColors.green, // размер точек pointRadius: 3, // ширина линии графика borderWidth: 3, // достанем данные из переменной 'пример' и оставим только значение и время изменения data: пример.map((item) => { return {y: item.val, t: new Date(item.ts)} }), // заливка графика - нет fill: false, } ] }, options: { // настройка легенды legend: { labels: { // размер шрифта fontSize: 24, }, }, // оси координат scales: { // оси X xAxes: [{ // тип - временная ось type: 'time', display: true, // метка оси scaleLabel: { display: true, labelString: 'Время' }, }], // оси Y yAxes: [{ // тип - линейная type: 'linear', display: true, // метка оси scaleLabel: { display: true, labelString: 'Температура' }, }] } } }; return chartJsOptions; }); } /** * функция отправки графика в телеграм * @param user - какому юзеру слать. если пусто - всем * @param chat_id - * @param message_id - в каком чате и какое сообщение заменить при обновлении * @param hours - количество часов, за которые получить данные */ //************************************************************************************** function sendGraph0(user, chat_id, message_id, hours){ // имя файла, в который положим картинку с графиком const filename = '/tmp/graph0.png'; hours = hours || 1; // выполним подготовку данных prepareDraw0(hours) // на след шаге нарисуем .then((result) => { // рисуем картинку по полученным данным и конфигурации return doDraw(result, filename); }) .then(() => { // удалим предыдущее сообщение if (message_id && chat_id) { sendTo('telegram.0', { user: user, deleteMessage: { options: { chat_id: chat_id, message_id: message_id } } }); } }) .then(()=>{ // теперь отправим сообщение в телеграм sendTo('telegram.0', { user: user, text: filename, caption: 'Температура в курятниках ('+hours+'ч)', reply_markup: { inline_keyboard: [ [ { text: '🔄', callback_data: 'graph_'+hours}, { text: '1 ч', callback_data: 'graph_1' }, { text: '2 ч', callback_data: 'graph_2' }, { text: '4 ч', callback_data: 'graph_4' }, { text: '12 ч', callback_data: 'graph_12' }, { text: '24 ч', callback_data: 'graph_24' }, ] ] } }); }); } // будем слушать телеграм и ждать команды на построение графика on({id: "telegram.0.communicate.request", ack: false, change: 'any'}, function (obj) { var v; var msg = obj.state.val; var command = obj.state.val.substring(obj.state.val.indexOf(']')+1); var user = obj.state.val.substring(obj.state.val.indexOf('[')+1,obj.state.val.indexOf(']')); var chat_id = getState("telegram.0.communicate.requestChatId").val; var message_id = getState("telegram.0.communicate.requestMessageId").val; // команда для графика - demo if (command.startsWith('demo')) { const hours = parseInt(command.split('_')[1]); sendGraph0(user, chat_id, message_id, hours); } });</void></any>`[/i][/i]
-
Попробуй вот такую функцию, вместо той, что в скрипте
function sendToPromise(adaper, cmd, params) { return new Promise((resolve, reject)=>{ sendTo(adaper, cmd, params, (result) => { resolve(result); }); }); }
Обновил скрипт в первом посте, убрал оттуда вспомогательные функции и вставил эту функцию.
-
Так сработало, но до этого на месте этой функции было:
/** * функция sendTo как Promise, чтобы удобно было строить цепочки */ var sendToPromise = promisifyNoError(sendTo);
Я думал, что тут что-то криво скопировалось…
-
Уфф, разобрались.
-
Я, честно говоря, пока не разобрался 8-) Но мне ещё надо покурить много, а сегодня я урывками, на ходу, то с работы, то из дома пробовал код менять. Вечерком по изучаю, спасибо за помощь Но вообще, у меня пока отдаёт картинку только за час, один раз, как то случайно отдал за 24 часа… Вот с таким логом : ` > telegram.0 2018-10-17 15:03:36.149 error Cannot send deleteMessage [chatId - 289938044]: Error: ETELEGRAM: 400 Bad Request: message can't be deleted
javascript.1 2018-10-17 15:03:35.561 debug sendTo "send" to system.adapter.telegram.0 from system.adapter.javascript.1
javascript.1 2018-10-17 15:03:35.560 info script.js.test.Скрипт3: sendTo(adapter=telegram.0, cmd=[object Object], msg=undefined)
javascript.1 2018-10-17 15:03:35.559 debug sendTo "send" to system.adapter.telegram.0 from system.adapter.javascript.1
javascript.1 2018-10-17 15:03:35.558 info script.js.test.Скрипт3: sendTo(adapter=telegram.0, cmd=[object Object], msg=undefined)
javascript.1 2018-10-17 15:03:35.254 debug sendTo "getHistory" to system.adapter.history.0 from system.adapter.javascript.1
javascript.1 2018-10-17 15:03:35.253 info script.js.test.Скрипт3: sendTo(adapter=history.0, cmd=getHistory, msg={"id":"mqtt.0.Borovoe1.bmet","options":{"start":1539774215252,"end":1539777815252,"aggregate":"onchange"}})
javascript.1 2018-10-17 15:03:35.251 info script.js.test.Скрипт3: getState(id=telegram.0.communicate.requestMessageId, timerId=0) => {"val":19327,"ack":false,"ts":1539777815225,"q":0,"from":"system.adapter.telegram.0","lc":1539777815225}
javascript.1 2018-10-17 15:03:35.249 info script.js.test.Скрипт3: getState(id=telegram.0.communicate.requestChatId, timerId=0) => {"val":289938044,"ack":false,"ts":1539777815213,"q":0,"from":"system.adapter.telegram.0","lc":1538911049076}
telegram.0 2018-10-17 15:03:31.405 error Cannot send deleteMessage [chatId - 289938044]: Error: ETELEGRAM: 400 Bad Request: message can't be deleted `
-
Попробуй вот такую функцию, вместо той, что в скрипте
function sendToPromise(adaper, cmd, params) { return new Promise((resolve, reject)=>{ sendTo(adaper, cmd, params, (result) => { resolve(result); }); }); }
Обновил скрипт в первом посте, убрал оттуда вспомогательные функции и вставил эту функцию. `
Поменял adaper на adapter, стало лучше )
-