449 lines
14 KiB
C
449 lines
14 KiB
C
/****************************************************************************
|
|
* 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 <config.h>
|
|
#endif
|
|
|
|
|
|
#include "./mdevices_page2_graph.h"
|
|
|
|
#include <aqcgi/service/module.h>
|
|
|
|
#include <aqdiagram/graph/timegraph.h>
|
|
#include <aqdiagram/graph/w_graph.h>
|
|
#include <aqdiagram/draw/context_cairo.h>
|
|
|
|
|
|
#include <gwenhywfar/directory.h>
|
|
#include <gwenhywfar/debug.h>
|
|
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------------------------------------
|
|
* 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 _setupDbForDeviceAndValue(const AQH_VALUE *v, GWEN_DB_NODE *dbCtx);
|
|
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;
|
|
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 (!_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);
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
_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 _setupDbForDeviceAndValue(const AQH_VALUE *v, GWEN_DB_NODE *dbCtx)
|
|
{
|
|
GWEN_DB_NODE *db;
|
|
AQH_DEVICE *device;
|
|
|
|
db=GWEN_DB_GetGroup(dbCtx, GWEN_DB_FLAGS_OVERWRITE_GROUPS, "value");
|
|
AQH_Value_toDb(v, db);
|
|
|
|
device=AQH_Value_GetDevicePtr(v);
|
|
if (device) {
|
|
const char *sNameForSystem;
|
|
const char *sLocation;
|
|
const char *sDescription;
|
|
GWEN_BUFFER *tbuf;
|
|
|
|
db=GWEN_DB_GetGroup(dbCtx, GWEN_DB_FLAGS_OVERWRITE_GROUPS, "device");
|
|
AQH_Device_toDb(device, db);
|
|
/* generate descriptive name */
|
|
sNameForSystem=AQH_Device_GetNameForSystem(device);
|
|
sLocation=AQH_Device_GetLocation(device);
|
|
sDescription=AQH_Device_GetDescription(device);
|
|
tbuf=GWEN_Buffer_new(0, 256, 0, 1);
|
|
if (sLocation && sDescription)
|
|
GBAA(tbuf, "%s - %s", sDescription, sLocation);
|
|
else if (sLocation)
|
|
GBAA(tbuf, "%s", sLocation);
|
|
else if (sDescription)
|
|
GBAA(tbuf, "%s", sDescription);
|
|
else
|
|
GBAA(tbuf, "%s", sNameForSystem);
|
|
|
|
GWEN_DB_SetCharValue(db, GWEN_DB_FLAGS_OVERWRITE_VARS, "descriptiveName", GWEN_Buffer_GetStart(tbuf));
|
|
GWEN_Buffer_free(tbuf);
|
|
}
|
|
else {
|
|
DBG_ERROR(NULL, "No device pointer in device %s", AQH_Value_GetNameForSystem(v));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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, "<a href=\"%s\">", lnk);
|
|
GBAS(dbuf, "<img src=\""DOC_PAGE2_GRAPH"?page=");
|
|
AQH_ModService_EscapeToBuffer(sPageId, dbuf);
|
|
GBAS(dbuf, "&graph=");
|
|
AQH_ModService_EscapeToBuffer(sGraphId, dbuf);
|
|
GBAA(dbuf, "\" alt=\"%s\" width=\"%d\" height=\"%d\"", sGraphId, w, h);
|
|
GBAS(dbuf, "/>");
|
|
if (lnk && *lnk)
|
|
GBAS(dbuf, "</a>");
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
}
|
|
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:"<unnamed>");
|
|
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:"<no title>");
|
|
AQDG_TimeGraph_ModifyDataAndAddCurve(g, sCurveLabel?sCurveLabel:"<no title>", 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:"");
|
|
}
|
|
|
|
|
|
|