Files
aqhomecontrol/apps/aqhome-cgi/modules/devices/mdevices_vgraph.c
2025-10-23 20:58:40 +02:00

432 lines
12 KiB
C

/****************************************************************************
* This file is part of the project AqHome.
* AqHome (c) by 2025 Martin Preuss, all rights reserved.
*
* The license for this file can be found in the file COPYING which you
* should have received along with this file.
****************************************************************************/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include "./mdevices_vgraph.h"
#include "aqhome-cgi/service/module.h"
#include "aqhome-cgi/modules/mdataclient.h"
#include <aqdiagram/graph/timegraph.h>
#include <aqdiagram/graph/w_graph.h>
#include <aqdiagram/draw/context_cairo.h>
//#include <aqdiagram/data/date.h>
//#include <aqdiagram/data/floatingavg.h>
#include <gwenhywfar/debug.h>
#include <gwenhywfar/timestamp.h>
#include <gwenhywfar/text.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
/* ------------------------------------------------------------------------------------------------
* defs and enums
* ------------------------------------------------------------------------------------------------
*/
#define GBAS GWEN_Buffer_AppendString
#define GBAA GWEN_Buffer_AppendArgs
enum {
VALUEGRAPH_PERIOD_4H=1,
VALUEGRAPH_PERIOD_1D,
VALUEGRAPH_PERIOD_1W,
VALUEGRAPH_PERIOD_1M
};
/* ------------------------------------------------------------------------------------------------
* forward declarations
* ------------------------------------------------------------------------------------------------
*/
static void _runGraphValueWithArgs(AQH_MODULE *m,
AQCGI_REQUEST *rq,
AQH_SESSION *session,
AQH_DATACLIENT *dc,
const char *sDeviceName,
const char *sValueName,
GWEN_BUFFER *dbuf);
static void _createGraph(AQH_DATACLIENT *dc,
const AQH_VALUE *v,
int period,
const char *graphTitle, int precision, const char *curveLabel,
const char *sImgFile, int imgWidth, int imgHeight, uint64_t numDataPoints);
static int _fileIsCurrent(const char *sPath, int seconds);
static AQDG_GRAPH *_mkGraphObjectWithTitle(const char *graphTitle, int period, int precision);
static int _getPeriodFromString(const char *sPeriod);
static int _getAcceptedAgeForPeriod(int period);
static void _mkPathForValueAndPeriod(AQH_MODULE *m, const AQH_VALUE *v, int pPeriod, GWEN_BUFFER *dbuf);
static uint64_t _getStartTimeForPeriod(int period);
static const char *_getModifiersForPeriod(int period);
static AQDG_GRAPH_DATAPAIR_LIST *_requestDataPairList(AQH_DATACLIENT *dc, const char *valueName,
uint64_t tsBegin, uint64_t tsEnd, uint64_t num);
static AQDG_GRAPH_DATAPAIR_LIST *_createDataPairListFromDataPoints(const uint64_t *dataPoints, uint64_t numValues);
/* ------------------------------------------------------------------------------------------------
* code
* ------------------------------------------------------------------------------------------------
*/
void AQH_ModDevices_RunGraphValue(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
GWEN_DB_NODE *dbQuery;
const char *sDeviceName;
const char *sValueName;
DBG_ERROR(NULL, "GraphValue");
dbQuery=AQCGI_Request_GetDbQuery(rq);
sDeviceName=GWEN_DB_GetCharValue(dbQuery, "device", 0, NULL);
sValueName=GWEN_DB_GetCharValue(dbQuery, "value", 0, NULL);
DBG_ERROR(NULL, "Device=%s, value=%s", sDeviceName?sDeviceName:"<empty>", sValueName?sValueName:"<empty>");
if (sDeviceName && *sDeviceName && sValueName && *sValueName) {
GWEN_BUFFER *bufDeviceName;
GWEN_BUFFER *bufValueName;
bufDeviceName=GWEN_Buffer_new(0, 64, 0, 1);
GWEN_Text_UnescapeToBufferTolerant(sDeviceName, bufDeviceName);
bufValueName=GWEN_Buffer_new(0, 64, 0, 1);
GWEN_Text_UnescapeToBufferTolerant(sValueName, bufValueName);
_runGraphValueWithArgs(m, rq, session, dc,
GWEN_Buffer_GetStart(bufDeviceName),
GWEN_Buffer_GetStart(bufValueName),
dbuf);
GWEN_Buffer_free(bufValueName);
GWEN_Buffer_free(bufDeviceName);
}
}
void _runGraphValueWithArgs(AQH_MODULE *m,
AQCGI_REQUEST *rq,
AQH_SESSION *session,
AQH_DATACLIENT *dc,
const char *sDeviceName,
const char *sValueName,
GWEN_BUFFER *dbuf)
{
GWEN_DB_NODE *dbQuery;
AQH_VALUE *value;
const char *sPeriod;
int period;
DBG_ERROR(NULL, "GraphValue with args");
dbQuery=AQCGI_Request_GetDbQuery(rq);
sPeriod=GWEN_DB_GetCharValue(dbQuery, "period", 0, NULL);
period=sPeriod?_getPeriodFromString(sPeriod):VALUEGRAPH_PERIOD_1D;
DBG_ERROR(NULL, "Device=%s, value=%s, period=%s",
sDeviceName?sDeviceName:"<empty>", sValueName?sValueName:"<empty>",
sPeriod?sPeriod:"<empty>");
value=AQH_ModDevices_GetValueForDevice(dc, sDeviceName, sValueName);
if (value) {
GWEN_BUFFER *fbuf;
int rv;
fbuf=GWEN_Buffer_new(0, 256, 0, 1);
_mkPathForValueAndPeriod(m, value, period, fbuf);
if (!_fileIsCurrent(GWEN_Buffer_GetStart(fbuf), _getAcceptedAgeForPeriod(period))) {
DBG_ERROR(NULL, "Creating graph");
_createGraph(dc,
value,
period,
sValueName,
2,
AQH_ValueModality_toString(AQH_Value_GetModality(value)),
GWEN_Buffer_GetStart(fbuf),
AQH_MODDEVICES_GRAPH_WIDTH, AQH_MODDEVICES_GRAPH_HEIGHT,
100000);
}
if (1) {
GWEN_BUFFER *ibuf;
ibuf=GWEN_Buffer_new(0, 1024, 0, 1);
// return file
rv=GWEN_SyncIo_Helper_ReadFile(GWEN_Buffer_GetStart(fbuf), ibuf);
if (rv<0) {
DBG_ERROR(NULL, "here (%d)", rv);
}
else {
GWEN_Buffer_Reset(dbuf);
GWEN_Buffer_AppendBytes(dbuf, GWEN_Buffer_GetStart(ibuf), GWEN_Buffer_GetUsedBytes(ibuf));
AQCGI_Request_AddResponseHeaderData(rq, "Content-type: image/png");
AQCGI_Request_AddFlags(rq, AQH_MODSERVICE_RQFLAGS_RAWFILE);
}
GWEN_Buffer_free(ibuf);
}
GWEN_Buffer_free(fbuf);
AQH_Value_free(value);
}
else {
DBG_ERROR(NULL, "Could not get value");
}
}
int _fileIsCurrent(const char *sPath, int seconds)
{
struct stat sb;
time_t t1;
if (lstat(sPath, &sb)==-1) {
DBG_ERROR(NULL, "Error on lstat(%s): %s (%d)", sPath, strerror(errno), errno);
return 0;
}
t1=time(0);
if ((t1-sb.st_mtime)<(time_t) seconds) {
DBG_ERROR(NULL, "File %s is current", sPath);
return 1;
}
return 0;
}
void _createGraph(AQH_DATACLIENT *dc,
const AQH_VALUE *v,
int period,
const char *graphTitle, int precision,
const char *curveLabel,
const char *sImgFile, int imgWidth, int imgHeight, uint64_t numDataPoints)
{
const char *sValue;
AQDG_GRAPH *g;
AQDG_DRAW_CONTEXT *drawContext;
AQDG_OBJECT *graphObject;
uint64_t tsBegin;
uint64_t tsEnd;
const char *sModifier;
AQDG_GRAPH_DATAPAIR_LIST *dpList;
sValue=AQH_Value_GetNameForSystem(v);
tsEnd=time(0);
tsBegin=_getStartTimeForPeriod(period);
g=_mkGraphObjectWithTitle(graphTitle, period, precision);
sModifier=_getModifiersForPeriod(period);
DBG_ERROR(NULL, "Requesting data for %s", sValue);
dpList=_requestDataPairList(dc, sValue, tsBegin, tsEnd, numDataPoints);
if (dpList) {
DBG_ERROR(NULL, "Adding data for %s", sValue);
AQDG_TimeGraph_ModifyDataAndAddCurve(g, curveLabel?curveLabel:sValue, sModifier, dpList);
}
else {
DBG_ERROR(NULL, "No data");
AQDG_Graph_free(g);
return;
}
DBG_ERROR(NULL, "Setup ticks for %s", sValue);
AQDG_TimeGraph_SetupTicks(g, 0, 0.0, 0.0);
DBG_ERROR(NULL, "Draw graph for %s", sValue);
drawContext=AQDG_Draw_ContextCairo_Png_new(sImgFile, imgWidth, imgHeight);
graphObject=AQDG_GraphWidget_new(NULL, AQDG_OBJECT_OPTIONS_STRETCHX | AQDG_OBJECT_OPTIONS_STRETCHY, drawContext);
AQDG_Object_SetWidth(graphObject, imgWidth);
AQDG_Object_SetHeight(graphObject, imgHeight);
AQDG_GraphWidget_SetupDefaultPens(graphObject);
AQDG_GraphWidget_SetupDefaultFonts(graphObject);
AQDG_GraphWidget_FinishWithGraph(graphObject, g);
AQDG_Object_free(graphObject);
}
AQDG_GRAPH *_mkGraphObjectWithTitle(const char *graphTitle, int period, int precision)
{
AQDG_GRAPH *g;
GWEN_BUFFER *tbuf;
const char *s;
tbuf=GWEN_Buffer_new(0, 256, 0, 1);
switch(period) {
case VALUEGRAPH_PERIOD_4H: s="last 4 hours"; break;
case VALUEGRAPH_PERIOD_1D: s="last 24 hours"; break;
case VALUEGRAPH_PERIOD_1W: s="last 7 days"; break;
case VALUEGRAPH_PERIOD_1M: s="last 30 days"; break;
default: s="last 24 hours"; break;
}
GBAA(tbuf, "%s - %s", graphTitle, s);
g=AQDG_TimeGraph_new(GWEN_Buffer_GetStart(tbuf), NULL, "Value", NULL, precision);
GWEN_Buffer_free(tbuf);
return g;
}
int _getPeriodFromString(const char *sPeriod)
{
if (strcasecmp(sPeriod, "4h")==0)
return VALUEGRAPH_PERIOD_4H;
else if (strcasecmp(sPeriod, "1d")==0)
return VALUEGRAPH_PERIOD_1D;
else if (strcasecmp(sPeriod, "1w")==0)
return VALUEGRAPH_PERIOD_1W;
else if (strcasecmp(sPeriod, "1m")==0)
return VALUEGRAPH_PERIOD_1M;
return VALUEGRAPH_PERIOD_1D;
}
void _mkPathForValueAndPeriod(AQH_MODULE *m, const AQH_VALUE *v, int period, GWEN_BUFFER *dbuf)
{
AQH_SERVICE *sv;
const char *s;
sv=AQH_ModService_GetService(m);
/* cache folder */
s=AQH_Service_GetCacheFolder(sv);
GBAA(dbuf, "%s%s", s, GWEN_DIR_SEPARATOR_S);
/* var name */
s=AQH_Value_GetNameForSystem(v);
GWEN_Text_EscapeToBuffer(s, dbuf);
/* period */
switch(period) {
case VALUEGRAPH_PERIOD_4H: s="4h"; break;
case VALUEGRAPH_PERIOD_1D: s="1d"; break;
case VALUEGRAPH_PERIOD_1W: s="1w"; break;
case VALUEGRAPH_PERIOD_1M: s="1m"; break;
default: s="1d"; break;
}
GBAA(dbuf, "-%s.png", s);
}
uint64_t _getStartTimeForPeriod(int period)
{
time_t t;
t=time(0);
/* period */
switch(period) {
case VALUEGRAPH_PERIOD_4H: t-=4*60*60; break;
case VALUEGRAPH_PERIOD_1D: t-=24*60*60; break;
case VALUEGRAPH_PERIOD_1W: t-=7*24*60*60; break;
case VALUEGRAPH_PERIOD_1M: t-=30*24*60*60; break;
default: t-=24*60*60; break;
}
return (uint64_t) t;
}
const char *_getModifiersForPeriod(int period)
{
/* period */
switch(period) {
case VALUEGRAPH_PERIOD_4H: return "La5";
case VALUEGRAPH_PERIOD_1D: return "La30";
case VALUEGRAPH_PERIOD_1W: return "La240";
case VALUEGRAPH_PERIOD_1M: return "La480";
default: return "La15";
}
}
int _getAcceptedAgeForPeriod(int period)
{
/* period */
switch(period) {
case VALUEGRAPH_PERIOD_4H: return 2*60; /* 2m */
case VALUEGRAPH_PERIOD_1D: return 5*60; /* 5m */
case VALUEGRAPH_PERIOD_1W: return 15*60; /* 15m */
case VALUEGRAPH_PERIOD_1M: return 60*60; /* 1h */
default: return 5*60; /* 5m */
}
}
AQDG_GRAPH_DATAPAIR_LIST *_requestDataPairList(AQH_DATACLIENT *dc, const char *valueName,
uint64_t tsBegin, uint64_t tsEnd, uint64_t num)
{
uint64_t *dataPoints;
uint64_t recvdNum;
dataPoints=malloc(num*sizeof(uint64_t)*2);
recvdNum=AQH_DataClient_GetPeriodData(dc, valueName, dataPoints, num, tsBegin, tsEnd);
if (recvdNum>0) {
AQDG_GRAPH_DATAPAIR_LIST *dpList;
dpList=_createDataPairListFromDataPoints(dataPoints, recvdNum);
free(dataPoints);
return dpList;
}
else {
DBG_ERROR(NULL, "No data received");
free(dataPoints);
return NULL;
}
}
AQDG_GRAPH_DATAPAIR_LIST *_createDataPairListFromDataPoints(const uint64_t *dataPoints, uint64_t numValues)
{
AQDG_GRAPH_DATAPAIR_LIST *dpList;
uint64_t i;
DBG_ERROR(NULL, "Got %d datapoints", (int) numValues);
dpList=AQDG_Graph_DataPair_List_new();
for(i=0; i<numValues; i++) {
AQDG_GRAPH_DATAPAIR *dp;
double timestamp;
union {double f; uint64_t i;} u;
timestamp=(double)(*(dataPoints++));
u.i=*(dataPoints++);
dp=AQDG_Graph_DataPair_new();
AQDG_Graph_DataPair_SetValueX(dp, timestamp);
AQDG_Graph_DataPair_SetValueY(dp, u.f);
AQDG_Graph_DataPair_List_Add(dp, dpList);
}
AQDG_Graph_DataPair_List_SortByValueX(dpList, 1);
return dpList;
}