Построение графиков ChartJS на сервере и отправка картинки в телеграм

Вопросы и информация о скриптах для ioBroker
Antworten
goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 16.10.2018, 14:30

Расскажу, как можно получить график в виде картинки, например для отправки в 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.
Снимок экрана от 2018-10-16 16-08-30.png
graph - строит график на данных из истории состояний. Вызываются функции sendGraph1, prepareDraw1, doDraw.
Снимок экрана от 2018-10-16 16-47-02.png

Code: Alles auswählen


'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);
    }
});

Du hast keine ausreichende Berechtigung, um die Dateianhänge dieses Beitrags anzusehen.
Zuletzt geändert von goofyk am 13.11.2018, 09:34, insgesamt 3-mal geändert.

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 16.10.2018, 22:11

Не могу понять, где засада... Команда - демо, отправляет файл в адаптер телеграм... А команда - граф, отправляет что-то в адаптер хистори, в телеграм при этом пусто 8-)

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 06:22

Всё верно, команда graph сперва достает из истории данные (отправляет сообщение в History и получает ответ), а затем строит график.
Вам нужно поменять скрипт и указать свои состояния, по которым надо получить данные. Ну и удалить ненужные запросы данных, лишние ряды на графике. Т.е. нужно разобраться со скриптом и поменять его под себя :)

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 07:49

Code: Alles auswählen

/**
 * функция 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] = extraArgs; } } else { // we return the raw array ret = extraArgs; } return resolve(ret); } }, cmd=undefined, msg=undefined)

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 08:09

Ошибки быть не должно - это оборачивание функции sendTo в промис.
Выше должны быть 2 вспомогательные функции для этого (promisifyNoError и sliceArgs).

Можно переписать скрипт, чтобы использовалась обычная функция sendTo, но тогда надо в callback передавать следующую функцию и т.д.

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 08:13

Покажите какая у вас получилась функция prepareDraw?

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 08:33

Code: Alles auswählen

/**
 * функция подготовки параметров для 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;
        });
}
Я только подставил пять своих состояний, больше ничего не менял

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 08:45

хм. по всем этим состояниям ведется история и она есть на запрашиваемый период?

тогда надо начать с простого варианта. переделываем пример prepareDraw0 и добавляем туда получение истории одного состояния, вместо заданных значений. сможете это сделать? это позволит понять работает ли получение истории.

может быть дело в NodeJS или версии контроллера или еще в чем-то...

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 08:51

По всем есть история, буду пробовать переделывать демо...

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 09:47

Б-р-р-р... а в логах что?
Попробуем получить историю без промиса:

Code: Alles auswählen


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(()=>{
        

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 10:51

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] = 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}


Отвлекли, вот лог, это ещё с промисом. Что из него можно узнать?

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 11:00

в нем нет ошибок :( странно, что он текст функции вываливает в лог... у меня только sendTo
Снимок экрана от 2018-10-17 12-57-12.png
может инстанс history не 0?
Du hast keine ausreichende Berechtigung, um die Dateianhänge dieses Beitrags anzusehen.

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 11:03

Какая версия Nodejs?

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 11:06

Хистори.0... Не знаю, как вставить картинку :( Ноде 8.12.0
Щас код такой:

Code: Alles auswählen

'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);
    }
});

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 11:09

Попробуй вот такую функцию, вместо той, что в скрипте

Code: Alles auswählen

function sendToPromise(adaper, cmd, params) {
    return new Promise((resolve, reject)=>{
        sendTo(adaper, cmd, params, (result) => {
            resolve(result);
        });
    });
}

Обновил скрипт в первом посте, убрал оттуда вспомогательные функции и вставил эту функцию.

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 11:51

Так сработало, но до этого на месте этой функции было:

Code: Alles auswählen

/**
 * функция sendTo как Promise, чтобы удобно было строить цепочки
*/
var sendToPromise = promisifyNoError(sendTo);
Я думал, что тут что-то криво скопировалось...

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 12:02

Уфф, разобрались.

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 12:58

Я, честно говоря, пока не разобрался 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

TechElCo
starter
Beiträge: 10
Registriert: 24.04.2017, 09:17

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von TechElCo » 17.10.2018, 13:16

goofyk hat geschrieben:
17.10.2018, 11:09
Попробуй вот такую функцию, вместо той, что в скрипте

Code: Alles auswählen

function sendToPromise(adaper, cmd, params) {
    return new Promise((resolve, reject)=>{
        sendTo(adaper, cmd, params, (result) => {
            resolve(result);
        });
    });
}

Обновил скрипт в первом посте, убрал оттуда вспомогательные функции и вставил эту функцию.
Поменял adaper на adapter, стало лучше )

goofyk
starter
Beiträge: 23
Registriert: 24.08.2017, 14:54

Re: Построение графиков ChartJS на сервере и отправка картинки в телеграм

Beitrag von goofyk » 17.10.2018, 13:34

TechElCo hat geschrieben:
17.10.2018, 13:16
Поменял adaper на adapter, стало лучше )
:)) Спасибо

Antworten