/**************************************************************************** * This file is part of the project AqHome. * AqHome (c) by 2026 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 #endif #include "./mdevices_page2_graph.h" #include #include #include #include #include #include /* ------------------------------------------------------------------------------------------------ * defs and enums * ------------------------------------------------------------------------------------------------ */ #define GBAS GWEN_Buffer_AppendString #define GBAA GWEN_Buffer_AppendArgs #define DEFAULT_GRAPH_WIDTH 640 #define DEFAULT_GRAPH_HEIGHT 480 #define DEFAULT_GRAPH_PRECISION 2 #define DEFAULT_GRAPH_REFRESHTIME 120 #define DEFAULT_GRAPHCONF_REFRESH 60 #define DOC_PAGE2_GRAPH "pgraph.html" #define FOLDER_CACHE "pages2" /* ------------------------------------------------------------------------------------------------ * forward declarations * ------------------------------------------------------------------------------------------------ */ static int _isGraphConfFileCurrent(AQH_MD_PAGECTX *ctx, const char *sPageId, const char *sGraphId, int refreshTime); static void _mkGraphConfigForEveryValue(AQH_MD_PAGECTX *ctx, GWEN_DB_NODE *dbGraph, GWEN_XMLNODE *n); static void _mkGraphConfigCurve(AQH_MD_PAGECTX *ctx, GWEN_DB_NODE *dbGraph, GWEN_XMLNODE *nElement); static GWEN_DB_NODE *_dbGraphFromXml(GWEN_XMLNODE *nElement, GWEN_DB_NODE *dbCtx); static GWEN_DB_NODE *_dbCurveFromXml(GWEN_XMLNODE *nElement, GWEN_DB_NODE *dbCtx); static void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, const char *lnk, GWEN_BUFFER *dbuf); static void _genPageGraph(AQH_DATACLIENT *dc, GWEN_DB_NODE *dbGraph, const char *sFilename); static int _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_DB_NODE *dbGraph, uint64_t tsBegin, uint64_t tsEnd); static void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, const char *ext, GWEN_BUFFER *buf); /* ------------------------------------------------------------------------------------------------ * code * ------------------------------------------------------------------------------------------------ */ void AQH_ModDevices_PageWriteGraph(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf) { const char *sGraphId; const char *sLink; GWEN_BUFFER *bufGraphId=NULL; int w; int h; sLink=GWEN_XMLNode_GetProperty(nElement, "link", NULL); w=GWEN_XMLNode_GetIntProperty(nElement, "width", DEFAULT_GRAPH_WIDTH); h=GWEN_XMLNode_GetIntProperty(nElement, "height", DEFAULT_GRAPH_HEIGHT); sGraphId=GWEN_XMLNode_GetProperty(nElement, "id", NULL); if (sGraphId) { GWEN_BUFFER *tbuf;; tbuf=GWEN_Buffer_new(0, 256, 0, 1); bufGraphId=GWEN_Buffer_new(0, 256, 0, 1); GWEN_DB_ReplaceVars(AQH_MDPageCtx_GetDbVars(ctx), sGraphId, tbuf); AQH_ModService_EscapeToBuffer(GWEN_Buffer_GetStart(tbuf), bufGraphId); GWEN_Buffer_free(tbuf); sGraphId=GWEN_Buffer_GetStart(bufGraphId); } DBG_ERROR(NULL, "GraphId: %s", sGraphId); if (!_isGraphConfFileCurrent(ctx, sPageId, sGraphId, DEFAULT_GRAPHCONF_REFRESH)) { GWEN_DB_NODE *dbGraph; GWEN_XMLNODE *n; GWEN_BUFFER *nbuf; int rv; dbGraph=_dbGraphFromXml(nElement, AQH_MDPageCtx_GetDbVars(ctx)); /* write real element */ n=GWEN_XMLNode_GetFirstTag(nElement); while(n) { const char *sName; sName=GWEN_XMLNode_GetData(n); if (sName && *sName) { if (strcasecmp(sName, "ForEveryValue")==0) _mkGraphConfigForEveryValue(ctx, dbGraph, n); else if (strcasecmp(sName, "curve")==0) _mkGraphConfigCurve(ctx, dbGraph, n); else { /* unhandled */ } } n=GWEN_XMLNode_GetNextTag(n); } /* write config file */ nbuf=GWEN_Buffer_new(0, 256, 0, 1); _mkPathForGraph(AQH_MDPageCtx_GetModule(ctx), sPageId, sGraphId, "conf", nbuf); rv=AQH_ModService_WriteDbFileSafely(dbGraph, GWEN_Buffer_GetStart(nbuf)); if (rv<0) { DBG_ERROR(NULL, "Error writing graph config to \"%s\": %d", GWEN_Buffer_GetStart(nbuf), rv); } GWEN_Buffer_free(nbuf); GWEN_DB_Group_free(dbGraph); } /* write graph link */ _addGraphLink(sPageId, sGraphId, w, h, sLink, dbuf); GWEN_Buffer_free(bufGraphId); } int _isGraphConfFileCurrent(AQH_MD_PAGECTX *ctx, const char *sPageId, const char *sGraphId, int refreshTime) { int rv; GWEN_BUFFER *nbuf; nbuf=GWEN_Buffer_new(0, 256, 0, 1); _mkPathForGraph(AQH_MDPageCtx_GetModule(ctx), sPageId, sGraphId, "conf", nbuf); rv=AQH_ModService_FileIsCurrent(GWEN_Buffer_GetStart(nbuf), refreshTime); GWEN_Buffer_free(nbuf); return rv; } void _mkGraphConfigForEveryValue(AQH_MD_PAGECTX *ctx, GWEN_DB_NODE *dbGraph, GWEN_XMLNODE *nElement) { AQH_VALUE_LIST *valueList; valueList=AQH_MDPageCtx_GetValues(ctx); if (valueList) { const AQH_VALUE *v; GWEN_DB_NODE *dbCtx; dbCtx=AQH_MDPageCtx_GetDbVars(ctx); v=AQH_Value_List_First(valueList); while(v) { GWEN_XMLNODE *n; AQH_ModDevices_SetupDbForDeviceAndValue(v, dbCtx); n=GWEN_XMLNode_GetFirstTag(nElement); while(n) { const char *sName; sName=GWEN_XMLNode_GetData(n); if (sName && *sName) { if (strcasecmp(sName, "curve")==0) _mkGraphConfigCurve(ctx, dbGraph, n); else { /* unhandled */ } } n=GWEN_XMLNode_GetNextTag(n); } v=AQH_Value_List_Next(v); } } else { DBG_ERROR(NULL, "No values"); } } void _mkGraphConfigCurve(AQH_MD_PAGECTX *ctx, GWEN_DB_NODE *dbGraph, GWEN_XMLNODE *nElement) { GWEN_DB_NODE *dbCurve; dbCurve=_dbCurveFromXml(nElement, AQH_MDPageCtx_GetDbVars(ctx)); GWEN_DB_AddGroup(dbGraph, dbCurve); } GWEN_DB_NODE *_dbGraphFromXml(GWEN_XMLNODE *nElement, GWEN_DB_NODE *dbCtx) { GWEN_DB_NODE *dbGraph; dbGraph=GWEN_DB_Group_new("graph"); AQH_ModDevices_SetCharValue(dbGraph, 0, "id", GWEN_XMLNode_GetProperty(nElement, "id", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbGraph, 0, "title", GWEN_XMLNode_GetProperty(nElement, "title", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbGraph, 0, "begin", GWEN_XMLNode_GetProperty(nElement, "begin", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbGraph, 0, "end", GWEN_XMLNode_GetProperty(nElement, "end", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbGraph, 0, "lowerLimit", GWEN_XMLNode_GetProperty(nElement, "lowerLimit", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbGraph, 0, "upperLimit", GWEN_XMLNode_GetProperty(nElement, "upperLimit", NULL), dbCtx); GWEN_DB_SetIntValue(dbGraph, 0, "width", GWEN_XMLNode_GetIntProperty(nElement, "width", DEFAULT_GRAPH_WIDTH)); GWEN_DB_SetIntValue(dbGraph, 0, "height", GWEN_XMLNode_GetIntProperty(nElement, "height", DEFAULT_GRAPH_HEIGHT)); GWEN_DB_SetIntValue(dbGraph, 0, "precision", GWEN_XMLNode_GetIntProperty(nElement, "precision", DEFAULT_GRAPH_PRECISION)); GWEN_DB_SetIntValue(dbGraph, 0, "refreshTime", GWEN_XMLNode_GetIntProperty(nElement, "refreshTime", DEFAULT_GRAPH_REFRESHTIME)); return dbGraph; } GWEN_DB_NODE *_dbCurveFromXml(GWEN_XMLNODE *nElement, GWEN_DB_NODE *dbCtx) { GWEN_DB_NODE *dbCurve; dbCurve=GWEN_DB_Group_new("curve"); AQH_ModDevices_SetCharValue(dbCurve, 0, "title", GWEN_XMLNode_GetProperty(nElement, "title", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbCurve, 0, "value", GWEN_XMLNode_GetProperty(nElement, "value", NULL), dbCtx); AQH_ModDevices_SetCharValue(dbCurve, 0, "modifier", GWEN_XMLNode_GetProperty(nElement, "modifier", NULL), dbCtx); return dbCurve; } void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, const char *lnk, GWEN_BUFFER *dbuf) { if (lnk && *lnk) GBAA(dbuf, "", lnk); GBAS(dbuf, "\"%s\""); if (lnk && *lnk) GBAS(dbuf, ""); } void AQH_ModDevices_HandleGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sPageId, const char *sGraphId, GWEN_BUFFER *dbuf) { GWEN_BUFFER *nbuf; GWEN_DB_NODE *dbGraph; int rv; nbuf=GWEN_Buffer_new(0, 256, 0, 1); _mkPathForGraph(m, sPageId, sGraphId, "conf", nbuf); dbGraph=GWEN_DB_Group_new("graph"); rv=GWEN_DB_ReadFile(dbGraph, GWEN_Buffer_GetStart(nbuf), GWEN_DB_FLAGS_DEFAULT | GWEN_PATH_FLAGS_CREATE_GROUP | GWEN_DB_FLAGS_LOCKFILE); if (rv<0) { DBG_ERROR(NULL, "Error reading graph config \"%s\": %d", GWEN_Buffer_GetStart(nbuf), rv); } else { int refreshTime; refreshTime=GWEN_DB_GetIntValue(dbGraph, "refreshTime", 0, DEFAULT_GRAPH_REFRESHTIME); GWEN_Buffer_Reset(nbuf); _mkPathForGraph(m, sPageId, sGraphId, "png", nbuf); if (!AQH_ModService_FileIsCurrent(GWEN_Buffer_GetStart(nbuf), refreshTime)) { DBG_INFO(NULL, "Generating graph \"%s\"", GWEN_Buffer_GetStart(nbuf)); _genPageGraph(dc, dbGraph, GWEN_Buffer_GetStart(nbuf)); } AQCGI_Request_AddResponseHeaderData(rq, "Refresh: 120"); AQH_ModService_RespondWithMimeFile(rq, GWEN_Buffer_GetStart(nbuf), "image/png", dbuf); } GWEN_DB_Group_free(dbGraph); GWEN_Buffer_free(nbuf); } void _genPageGraph(AQH_DATACLIENT *dc, GWEN_DB_NODE *dbGraph, const char *sFilename) { const char *s; const char *sTitle; int w; int h; int precision; uint64_t tsBegin; uint64_t tsEnd; AQDG_GRAPH *g; AQDG_DRAW_CONTEXT *drawContext; AQDG_OBJECT *graphObject; uint32_t tickFlags=0; double upperLimit; double lowerLimit; sTitle=GWEN_DB_GetCharValue(dbGraph, "title", 0, "untitled"); w=GWEN_DB_GetIntValue(dbGraph, "width", 0, DEFAULT_GRAPH_WIDTH); h=GWEN_DB_GetIntValue(dbGraph, "height", 0, DEFAULT_GRAPH_HEIGHT); precision=GWEN_DB_GetIntValue(dbGraph, "precision", 0, DEFAULT_GRAPH_PRECISION); tsBegin=AQH_ParseTime(GWEN_DB_GetCharValue(dbGraph, "begin", 0, "-4h")); tsEnd=AQH_ParseTime(GWEN_DB_GetCharValue(dbGraph, "end", 0, "0")); s=GWEN_DB_GetCharValue(dbGraph, "lowerLimit", 0, NULL); if (s && *s) { if (1==sscanf(s, "%lf", &lowerLimit)) tickFlags|=AQDG_TIMEGRAPH_SETUPTICKS_FLAGS_MINY; else { DBG_ERROR(NULL, "Ignoring invalid lowerLimit (%s)", s); } } s=GWEN_DB_GetCharValue(dbGraph, "upperLimit", 0, NULL); if (s && *s) { if (1==sscanf(s, "%lf", &upperLimit)) tickFlags|=AQDG_TIMEGRAPH_SETUPTICKS_FLAGS_MAXY; else { DBG_ERROR(NULL, "Ignoring invalid upperLimit (%s)", s); } } g=AQDG_TimeGraph_new(sTitle, NULL, "Value", NULL, precision); if (_addCurves(dc, g, dbGraph, tsBegin, tsEnd)) { AQDG_TimeGraph_SetupTicks(g, tickFlags, lowerLimit, upperLimit); DBG_INFO(NULL, "Draw graph %s", sFilename); drawContext=AQDG_Draw_ContextCairo_Png_new(sFilename, w, h); graphObject=AQDG_GraphWidget_new(NULL, AQDG_OBJECT_OPTIONS_STRETCHX | AQDG_OBJECT_OPTIONS_STRETCHY, drawContext); AQDG_Object_SetWidth(graphObject, w); AQDG_Object_SetHeight(graphObject, h); AQDG_GraphWidget_SetupDefaultPens(graphObject); AQDG_GraphWidget_SetupDefaultFonts(graphObject); AQDG_GraphWidget_FinishWithGraph(graphObject, g); AQDG_Object_free(graphObject); } else { DBG_ERROR(NULL, "No curves added"); AQDG_Graph_free(g); } } int _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_DB_NODE *dbGraph, uint64_t tsBegin, uint64_t tsEnd) { GWEN_DB_NODE *dbCurve; int curvesAdded=0; dbCurve=GWEN_DB_FindFirstGroup(dbGraph, "curve"); if (dbCurve==NULL) { DBG_ERROR(NULL, "No curve found"); } while(dbCurve) { const char *sCurveLabel; const char *sModifier; const char *sValueName; sCurveLabel=GWEN_DB_GetCharValue(dbCurve, "title", 0, NULL); sValueName=GWEN_DB_GetCharValue(dbCurve, "value", 0, NULL); sModifier=GWEN_DB_GetCharValue(dbCurve, "modifier", 0, NULL); DBG_INFO(NULL, "Handling curve %s", sCurveLabel?sCurveLabel:""); if (sValueName && *sValueName && sModifier && *sModifier) { AQDG_GRAPH_DATAPAIR_LIST *dpList; dpList=AQH_ModDevices_RequestDataPairList(dc, sValueName, tsBegin, tsEnd, 10000000); if (dpList) { DBG_INFO(NULL, "Adding data for %s", sCurveLabel?sCurveLabel:""); AQDG_TimeGraph_ModifyDataAndAddCurve(g, sCurveLabel?sCurveLabel:"", sModifier, dpList); curvesAdded++; } } else { DBG_ERROR(NULL, "Incomplete curve data"); } dbCurve=GWEN_DB_FindNextGroup(dbCurve, "curve"); } return curvesAdded; } void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, const char *ext, GWEN_BUFFER *buf) { AQCGI_SERVICE *sv; const char *s; int rv; GWEN_BUFFER *tbuf; sv=AQH_ModService_GetService(m); /* make sure folder exists */ tbuf=GWEN_Buffer_new(0, 256, 0, 1); s=AQCGI_Service_GetCacheFolder(sv); GBAA(tbuf, "%s%s"FOLDER_CACHE, s, GWEN_DIR_SEPARATOR_S); rv=GWEN_Directory_GetPath(GWEN_Buffer_GetStart(tbuf), 0); if (rv<0) { DBG_ERROR(NULL, "Error getting path \"%s\"", GWEN_Buffer_GetStart(tbuf)); } GWEN_Buffer_free(tbuf); GBAA(buf, "%s%s"FOLDER_CACHE"%s%s-%s.%s", s, GWEN_DIR_SEPARATOR_S, GWEN_DIR_SEPARATOR_S, sPageId, sGraphId, ext?ext:""); }