Files
aqhomecontrol/apps/aqhome-cgi/modules/devices/mdevices_page2_graph.c
2026-08-24 23:35:54 +02:00

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:"");
}