NEWS
Построение графиков ChartJS на сервере и отправка картинки в телеграм
-
Всё верно, команда 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 передавать следующую функцию и т.д.
-
/** * функция подготовки параметров для 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>___ Отвлекли, вот лог, это ещё с промисом. Что из него можно узнать? `
-
Хистори.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); }); }); }Обновил скрипт в первом посте, убрал оттуда вспомогательные функции и вставил эту функцию.
-
Я, честно говоря, пока не разобрался 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, стало лучше )
Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.
Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.
Mit deinem Input könnte dieser Beitrag noch besser werden 💗
Registrieren Anmelden
