12 Commits

Author SHA1 Message Date
Martin Preuss
d47a496c3f aqhome-cgi: allow use of <ForEveryValue> nearly everywhere. 2026-08-25 00:42:42 +02:00
Martin Preuss
b72bc7d8b5 added some more weather api scripts. 2026-08-24 23:36:25 +02:00
Martin Preuss
8158bc28fb aqhome-cgi: rewrite of the page module. 2026-08-24 23:35:54 +02:00
Martin Preuss
5587dd5abc aqhome-cgi: added AQH_ModService_WriteDbFileSafely() 2026-08-24 23:32:45 +02:00
Martin Preuss
515829ef6e aqhome-cgi: decreased verbosity (can be increased again via env AQHOME_LOGLEVEL). 2026-08-24 23:32:25 +02:00
Martin Preuss
7a17e87541 added scripts to build file. 2026-08-16 23:44:45 +02:00
Martin Preuss
1934a40ae6 added script for www.weatherapi.com
works great so far. You will need an API key for this.
2026-08-16 23:44:30 +02:00
Martin Preuss
c9f4339fb2 added script for open-meteo.com (doesn't work reliably in my setup). 2026-08-16 23:43:48 +02:00
Martin Preuss
7a35b1e374 aqhome-cgi: fixed path of diagrams generated for pages. 2026-08-16 23:43:07 +02:00
Martin Preuss
ffe455bf6b avr: added README for app light controller. 2026-08-16 23:42:37 +02:00
Martin Preuss
9b7ad7c6e3 examples: fixed paths. 2026-08-16 23:41:16 +02:00
Martin Preuss
343da31937 LightControl: send new RGBW value also in AUTO mode. 2026-08-15 00:35:50 +02:00
39 changed files with 2523 additions and 99 deletions

View File

@@ -29,7 +29,7 @@
#define AQHOME_CGI_DEFAULT_RUNTIME_FILES AQHOME_CGI_WWWDIR"/data" #define AQHOME_CGI_DEFAULT_RUNTIME_FILES AQHOME_CGI_WWWDIR"/data"
#define AQHOME_CGI_DEFAULT_CACHE_FILES AQHOME_CGI_WWWDIR"/cache" #define AQHOME_CGI_DEFAULT_CACHE_FILES AQHOME_CGI_WWWDIR"/cache"
#define AQHOME_CGI_DEFAULT_BASE_URL "http://127.0.0.1/aqbt" #define AQHOME_CGI_DEFAULT_BASE_URL "http://127.0.0.1/aqbt"
#define AQHOME_CGI_DEFAULT_LOGLEVEL "warning"
/* ------------------------------------------------------------------------------------------------ /* ------------------------------------------------------------------------------------------------
@@ -55,6 +55,13 @@ int main(int argc, char **argv)
const char *sPathStaticFiles; const char *sPathStaticFiles;
const char *sPathRuntimeFiles; const char *sPathRuntimeFiles;
const char *sBaseUrl; const char *sBaseUrl;
const char *sLogLevel;
int logLevel;
sLogLevel=getenv("AQHOME_LOGLEVEL");
if (!(sLogLevel && *sLogLevel))
sLogLevel=AQHOME_CGI_DEFAULT_LOGLEVEL;
logLevel=GWEN_Logger_Name2Level(sLogLevel);
sPathStaticFiles=getenv("AQHOME_STATIC_FILES"); sPathStaticFiles=getenv("AQHOME_STATIC_FILES");
if (!(sPathStaticFiles && *sPathStaticFiles)) if (!(sPathStaticFiles && *sPathStaticFiles))
@@ -79,17 +86,17 @@ int main(int argc, char **argv)
GWEN_Logger_Open(AQDG_LOGDOMAIN, "aqdiagram", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon); GWEN_Logger_Open(AQDG_LOGDOMAIN, "aqdiagram", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon);
GWEN_Logger_Open(NULL, "aqhome-cgi", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon); GWEN_Logger_Open(NULL, "aqhome-cgi", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon);
GWEN_Logger_SetLevel(GWEN_LOGDOMAIN, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(GWEN_LOGDOMAIN, logLevel);
GWEN_Logger_SetLevel(AQCGI_LOGDOMAIN, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(AQCGI_LOGDOMAIN, logLevel);
GWEN_Logger_SetLevel(NULL, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(NULL, logLevel);
GWEN_Logger_Close(GWEN_LOGDOMAIN); GWEN_Logger_Close(GWEN_LOGDOMAIN);
GWEN_Logger_Open(GWEN_LOGDOMAIN, "gwenhywfar", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon); GWEN_Logger_Open(GWEN_LOGDOMAIN, "gwenhywfar", AQHOME_CGI_LOGFILE, GWEN_LoggerType_File, GWEN_LoggerFacility_Daemon);
GWEN_Logger_SetLevel(GWEN_LOGDOMAIN, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(GWEN_LOGDOMAIN, logLevel);
GWEN_Logger_SetLevel(AQCGI_LOGDOMAIN, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(AQCGI_LOGDOMAIN, logLevel);
GWEN_Logger_SetLevel(AQDG_LOGDOMAIN, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(AQDG_LOGDOMAIN, logLevel);
GWEN_Logger_SetLevel(NULL, GWEN_LoggerLevel_Debug); GWEN_Logger_SetLevel(NULL, logLevel);
if (argc>1 && argv[1]) { if (argc>1 && argv[1]) {
if (0==strcasecmp(argv[1], "init")) { if (0==strcasecmp(argv[1], "init")) {

View File

@@ -17,6 +17,7 @@
#include <gwenhywfar/debug.h> #include <gwenhywfar/debug.h>
#include <gwenhywfar/text.h> #include <gwenhywfar/text.h>
#include <gwenhywfar/fslock.h>
#include <string.h> #include <string.h>
#include <ctype.h> #include <ctype.h>
@@ -920,4 +921,61 @@ void AQH_ModService_DenyRequest(AQCGI_MODULE *m, AQCGI_REQUEST *rq, const AQCGI_
int AQH_ModService_WriteDbFileSafely(GWEN_DB_NODE *db, const char *sFilename)
{
if (db && sFilename && *sFilename) {
GWEN_BUFFER *nbuf;
GWEN_FSLOCK *lck;
GWEN_FSLOCK_RESULT res;
int rv;
nbuf=GWEN_Buffer_new(0, 256, 0, 1);
GWEN_Buffer_AppendString(nbuf, sFilename);
GWEN_Buffer_AppendString(nbuf, ".tmp");
lck=GWEN_FSLock_new(sFilename, GWEN_FSLock_TypeFile);
res=GWEN_FSLock_Lock(lck, GWEN_DB_DEFAULT_LOCK_TIMEOUT, 0);
if (res!=GWEN_FSLock_ResultOk) {
DBG_ERROR(GWEN_LOGDOMAIN, "Could not apply lock to file \"%s\" (%d)", sFilename, res);
GWEN_FSLock_free(lck);
return GWEN_ERROR_IO;
}
rv=GWEN_DB_WriteFile(db, GWEN_Buffer_GetStart(nbuf), GWEN_DB_FLAGS_DEFAULT);
if (rv<0) {
DBG_INFO(GWEN_LOGDOMAIN, "here (%d)", rv);
remove(GWEN_Buffer_GetStart(nbuf));
GWEN_FSLock_Unlock(lck);
GWEN_FSLock_free(lck);
GWEN_Buffer_free(nbuf);
return rv;
}
rv=rename(GWEN_Buffer_GetStart(nbuf), sFilename);
if (rv<0) {
DBG_INFO(GWEN_LOGDOMAIN, "here (%d: %s)", errno, strerror(errno));
remove(GWEN_Buffer_GetStart(nbuf));
GWEN_FSLock_Unlock(lck);
GWEN_FSLock_free(lck);
GWEN_Buffer_free(nbuf);
return rv;
}
res=GWEN_FSLock_Unlock(lck);
if (res!=GWEN_FSLock_ResultOk) {
DBG_WARN(GWEN_LOGDOMAIN, "Could not remove lock on file \"%s\" (%d)", sFilename, res);
}
GWEN_FSLock_free(lck);
GWEN_Buffer_free(nbuf);
return 0;
}
else {
DBG_ERROR(GWEN_LOGDOMAIN, "Empty arg(s)");
return GWEN_ERROR_INVALID;
}
}

View File

@@ -97,5 +97,7 @@ void AQH_ModService_Redirect(AQCGI_REQUEST *rq, const char *destLocation);
void AQH_ModService_RedirectWithReferer(AQCGI_REQUEST *rq, const char *destLocation); void AQH_ModService_RedirectWithReferer(AQCGI_REQUEST *rq, const char *destLocation);
void AQH_ModService_DenyRequest(AQCGI_MODULE *m, AQCGI_REQUEST *rq, const AQCGI_SESSION *session, GWEN_BUFFER *dbuf); void AQH_ModService_DenyRequest(AQCGI_MODULE *m, AQCGI_REQUEST *rq, const AQCGI_SESSION *session, GWEN_BUFFER *dbuf);
int AQH_ModService_WriteDbFileSafely(GWEN_DB_NODE *db, const char *sFilename);
#endif #endif

View File

@@ -62,6 +62,10 @@
mdevices_device.h mdevices_device.h
mdevices_setdevice.h mdevices_setdevice.h
mdevices_page.h mdevices_page.h
mdevices_page2.h
mdevices_page2_graph.h
mdevices_pagectx.h
mdevices_pagectx_p.h
</headers> </headers>
@@ -83,6 +87,9 @@
mdevices_device.c mdevices_device.c
mdevices_setdevice.c mdevices_setdevice.c
mdevices_page.c mdevices_page.c
mdevices_page2.c
mdevices_page2_graph.c
mdevices_pagectx.c
</sources> </sources>

View File

@@ -11,7 +11,7 @@
#endif #endif
#include "./mdevices.h" #include "./mdevices_p.h"
#include "aqhome-cgi/modules/devices/mdevices_index.h" #include "aqhome-cgi/modules/devices/mdevices_index.h"
#include "aqhome-cgi/modules/devices/mdevices_valuestable.h" #include "aqhome-cgi/modules/devices/mdevices_valuestable.h"
@@ -22,6 +22,7 @@
#include "aqhome-cgi/modules/devices/mdevices_device.h" #include "aqhome-cgi/modules/devices/mdevices_device.h"
#include "aqhome-cgi/modules/devices/mdevices_setdevice.h" #include "aqhome-cgi/modules/devices/mdevices_setdevice.h"
#include "aqhome-cgi/modules/devices/mdevices_page.h" #include "aqhome-cgi/modules/devices/mdevices_page.h"
#include "aqhome-cgi/modules/devices/mdevices_page2.h"
#include "aqhome-cgi/modules/mdataclient.h" #include "aqhome-cgi/modules/mdataclient.h"
#include <aqcgi/service/module.h> #include <aqcgi/service/module.h>
@@ -46,12 +47,21 @@
#define P_VALUEWRITE AQH_MODDEVICES_PERMS_VALUEWRITE #define P_VALUEWRITE AQH_MODDEVICES_PERMS_VALUEWRITE
/* ------------------------------------------------------------------------------------------------
* global vars
* ------------------------------------------------------------------------------------------------
*/
GWEN_INHERIT(AQCGI_MODULE, AQH_MOD_DEVICES)
/* ------------------------------------------------------------------------------------------------ /* ------------------------------------------------------------------------------------------------
* forward declarations * forward declarations
* ------------------------------------------------------------------------------------------------ * ------------------------------------------------------------------------------------------------
*/ */
static void GWENHYWFAR_CB _freeData(void *bp, void *p);
static AQCGI_MODULE *_loadSubModule(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sModuleName); static AQCGI_MODULE *_loadSubModule(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sModuleName);
static int _handleRequest(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sLastPathElem); static int _handleRequest(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sLastPathElem);
@@ -103,13 +113,94 @@ static AQH_MODSERVICE_HANDLER_ENTRY _requestTable[]={
void AQH_ModDevices_Extend(AQCGI_MODULE *m, AQCGI_SERVICE *sv, const char *baseFolder) void AQH_ModDevices_Extend(AQCGI_MODULE *m, AQCGI_SERVICE *sv, const char *baseFolder)
{ {
AQH_MOD_DEVICES *xm;
AQH_ModService_Extend(m, sv, baseFolder); AQH_ModService_Extend(m, sv, baseFolder);
GWEN_NEW_OBJECT(AQH_MOD_DEVICES, xm);
GWEN_INHERIT_SETDATA(AQCGI_MODULE, AQH_MOD_DEVICES, m, xm, _freeData);
AQH_ModService_SetHandleRequestFn(m, _handleRequest); AQH_ModService_SetHandleRequestFn(m, _handleRequest);
AQH_ModService_SetLoadSubModuleFn(m, _loadSubModule); AQH_ModService_SetLoadSubModuleFn(m, _loadSubModule);
} }
void _freeData(GWEN_UNUSED void *bp, void *p)
{
AQH_MOD_DEVICES *xm;
xm=(AQH_MOD_DEVICES*) p;
AQH_Value_List_free(xm->valueList);
AQH_Device_List_free(xm->deviceList);
GWEN_FREE_OBJECT(xm);
}
AQH_DEVICE_LIST *AQH_ModDevices_GetDeviceList(const AQCGI_MODULE *m)
{
if (m) {
AQH_MOD_DEVICES *xm;
xm=GWEN_INHERIT_GETDATA(AQCGI_MODULE, AQH_MOD_DEVICES, m);
if (xm) {
return xm->deviceList;
}
}
return NULL;
}
void AQH_ModDevices_SetDeviceList(AQCGI_MODULE *m, AQH_DEVICE_LIST *deviceList)
{
if (m) {
AQH_MOD_DEVICES *xm;
xm=GWEN_INHERIT_GETDATA(AQCGI_MODULE, AQH_MOD_DEVICES, m);
if (xm) {
AQH_Device_List_free(xm->deviceList);
xm->deviceList=deviceList;
}
}
}
AQH_VALUE_LIST *AQH_ModDevices_GetValueList(const AQCGI_MODULE *m)
{
if (m) {
AQH_MOD_DEVICES *xm;
xm=GWEN_INHERIT_GETDATA(AQCGI_MODULE, AQH_MOD_DEVICES, m);
if (xm) {
return xm->valueList;
}
}
return NULL;
}
void AQH_ModDevices_SetValueList(AQCGI_MODULE *m, AQH_VALUE_LIST *valueList)
{
if (m) {
AQH_MOD_DEVICES *xm;
xm=GWEN_INHERIT_GETDATA(AQCGI_MODULE, AQH_MOD_DEVICES, m);
if (xm) {
AQH_Value_List_free(xm->valueList);
xm->valueList=valueList;
}
}
}
AQCGI_MODULE *_loadSubModule(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sModuleName) AQCGI_MODULE *_loadSubModule(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, const char *sModuleName)
{ {
return NULL; return NULL;
@@ -183,7 +274,7 @@ void _handleRqDevicePost(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *sess
void _handleRqPageGet(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, GWEN_BUFFER *dbuf) void _handleRqPageGet(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, GWEN_BUFFER *dbuf)
{ {
AQH_ModDataClient_HandleRequest(m, rq, session, AQH_ModDevices_RunPageGet, dbuf); AQH_ModDataClient_HandleRequest(m, rq, session, AQH_ModDevices_RunPage2Get, dbuf);
} }
@@ -197,7 +288,7 @@ void _handleRqPagePost(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *sessio
void _handleRqPageGraphGet(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, GWEN_BUFFER *dbuf) void _handleRqPageGraphGet(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, GWEN_BUFFER *dbuf)
{ {
AQH_ModDataClient_HandleRequest(m, rq, session, AQH_ModDevices_RunPageGraph, dbuf); AQH_ModDataClient_HandleRequest(m, rq, session, AQH_ModDevices_RunPage2Graph, dbuf);
} }
@@ -583,3 +674,41 @@ AQDG_GRAPH_DATAPAIR_LIST *_createDataPairListFromDataPoints(const uint64_t *data
void AQH_ModDevices_ReadDevicesAndValues(AQCGI_MODULE *m, AQH_DATACLIENT *dc)
{
AQH_DEVICE_LIST *deviceList;
AQH_VALUE_LIST *valueList;
deviceList=AQH_DataClient_GetDevices(dc, NULL);
AQH_ModDevices_SetDeviceList(m, deviceList);
valueList=AQH_DataClient_GetValues(dc, NULL, 0);
AQH_ModDevices_SetValueList(m, valueList);
if (deviceList && valueList) {
AQH_VALUE *v;
v=AQH_Value_List_First(valueList);
while(v) {
const char *s;
s=AQH_Value_GetDeviceNameForSystem(v);
if (s && *s) {
AQH_DEVICE *device;
device=AQH_Device_List_GetByNameForSystem(deviceList, s);
if (device) {
AQH_Value_SetDevicePtr(v, device);
}
else {
DBG_ERROR(NULL, "Device %s not found in list", s);
}
}
v=AQH_Value_List_Next(v);
}
}
}

View File

@@ -42,6 +42,14 @@
void AQH_ModDevices_Extend(AQCGI_MODULE *m, AQCGI_SERVICE *sv, const char *baseFolder); void AQH_ModDevices_Extend(AQCGI_MODULE *m, AQCGI_SERVICE *sv, const char *baseFolder);
int AQH_ModDevices_Create(AQCGI_SERVICE *sv); int AQH_ModDevices_Create(AQCGI_SERVICE *sv);
AQH_DEVICE_LIST *AQH_ModDevices_GetDeviceList(const AQCGI_MODULE *m);
void AQH_ModDevices_SetDeviceList(AQCGI_MODULE *m, AQH_DEVICE_LIST *deviceList);
AQH_VALUE_LIST *AQH_ModDevices_GetValueList(const AQCGI_MODULE *m);
void AQH_ModDevices_SetValueList(AQCGI_MODULE *m, AQH_VALUE_LIST *valueList);
void AQH_ModDevices_ReadDevicesAndValues(AQCGI_MODULE *m, AQH_DATACLIENT *dc);
uint32_t AQH_ModDevices_ColorFromHexString(const char *s); uint32_t AQH_ModDevices_ColorFromHexString(const char *s);
uint32_t AQH_ModDevices_HtmlColorToValueRGBW(uint32_t colorIn); uint32_t AQH_ModDevices_HtmlColorToValueRGBW(uint32_t colorIn);

View File

@@ -0,0 +1,23 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MODULES_DEVICES_P_H
#define AQHOME_CGI_MODULES_DEVICES_P_H
#include "aqhome-cgi/modules/devices/mdevices.h"
struct AQH_MOD_DEVICES {
AQH_DEVICE_LIST *deviceList;
AQH_VALUE_LIST *valueList;
};
typedef struct AQH_MOD_DEVICES AQH_MOD_DEVICES;
#endif

View File

@@ -61,13 +61,14 @@ static void _writeActor(AQH_DATACLIENT *dc, const char *sPageId, GWEN_XMLNODE *n
static void _writeGraph(const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf); static void _writeGraph(const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf);
static void _handlePageActor(AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sActorId, GWEN_XMLNODE *nActor); static void _handlePageActor(AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sActorId, GWEN_XMLNODE *nActor);
static void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sGraphId, GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf); static void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc,
static void _genPageGraph(AQH_DATACLIENT *dc, const char *sGraphId, const char *sFilename, GWEN_XMLNODE *nGraph); const char *sPageId, const char *sGraphId,
GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf);
static void _genPageGraph(AQH_DATACLIENT *dc, const char *sFilename, GWEN_XMLNODE *nGraph);
static void _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_XMLNODE *nGraph, uint64_t tsBegin, uint64_t tsEnd); static void _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_XMLNODE *nGraph, uint64_t tsBegin, uint64_t tsEnd);
static AQDG_GRAPH_DATAPAIR_LIST *_readCurveData(AQH_DATACLIENT *dc, GWEN_XMLNODE *nCurve, uint64_t tsBegin, uint64_t tsEnd); static AQDG_GRAPH_DATAPAIR_LIST *_readCurveData(AQH_DATACLIENT *dc, GWEN_XMLNODE *nCurve, uint64_t tsBegin, uint64_t tsEnd);
static uint64_t _parseTime(const char *s);
static GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName); static GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName);
static void _mkPathForGraph(AQCGI_MODULE *m, const char *sGraphId, GWEN_BUFFER *dbuf); static void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, GWEN_BUFFER *dbuf);
static void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, GWEN_BUFFER *dbuf); static void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, GWEN_BUFFER *dbuf);
static void _writeRgbwToForm(const char *sValueName, uint32_t color, GWEN_BUFFER *dbuf); static void _writeRgbwToForm(const char *sValueName, uint32_t color, GWEN_BUFFER *dbuf);
static void _writeOnOffToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf); static void _writeOnOffToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf);
@@ -148,7 +149,7 @@ void AQH_ModDevices_RunPageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSI
nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", "id", sPageId); nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", "id", sPageId);
nGraph=_getSubItemNode(nPage, sGraphId, "graph"); nGraph=_getSubItemNode(nPage, sGraphId, "graph");
if (nPage && nGraph) if (nPage && nGraph)
_handlePageGraph(m, rq, dc, sGraphId, nGraph, dbuf); _handlePageGraph(m, rq, dc, sPageId, sGraphId, nGraph, dbuf);
else { else {
DBG_ERROR(NULL, "Graph %s/%s not found", sPageId, sGraphId); DBG_ERROR(NULL, "Graph %s/%s not found", sPageId, sGraphId);
} }
@@ -247,16 +248,16 @@ void _handlePageActor(AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sActorI
void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sGraphId, GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf) void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sPageId, const char *sGraphId, GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf)
{ {
GWEN_BUFFER *fbuf; GWEN_BUFFER *fbuf;
int refreshTime; int refreshTime;
refreshTime=GWEN_XMLNode_GetIntProperty(nGraph, "refreshTime", 120); refreshTime=GWEN_XMLNode_GetIntProperty(nGraph, "refreshTime", 120);
fbuf=GWEN_Buffer_new(0, 256, 0, 1); fbuf=GWEN_Buffer_new(0, 256, 0, 1);
_mkPathForGraph(m, sGraphId, fbuf); _mkPathForGraph(m, sPageId, sGraphId, fbuf);
if (!AQH_ModService_FileIsCurrent(GWEN_Buffer_GetStart(fbuf), refreshTime)) { if (!AQH_ModService_FileIsCurrent(GWEN_Buffer_GetStart(fbuf), refreshTime)) {
_genPageGraph(dc, sGraphId, GWEN_Buffer_GetStart(fbuf), nGraph); _genPageGraph(dc, GWEN_Buffer_GetStart(fbuf), nGraph);
} }
AQH_ModService_RespondWithMimeFile(rq, GWEN_Buffer_GetStart(fbuf), "image/png", dbuf); AQH_ModService_RespondWithMimeFile(rq, GWEN_Buffer_GetStart(fbuf), "image/png", dbuf);
GWEN_Buffer_free(fbuf); GWEN_Buffer_free(fbuf);
@@ -264,7 +265,7 @@ void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, co
void _genPageGraph(AQH_DATACLIENT *dc, const char *sGraphId, const char *sFilename, GWEN_XMLNODE *nGraph) void _genPageGraph(AQH_DATACLIENT *dc, const char *sFilename, GWEN_XMLNODE *nGraph)
{ {
const char *s; const char *s;
const char *sTitle; const char *sTitle;
@@ -284,8 +285,8 @@ void _genPageGraph(AQH_DATACLIENT *dc, const char *sGraphId, const char *sFilena
w=GWEN_XMLNode_GetIntProperty(nGraph, "width", AQH_MODDEVICES_GRAPH_WIDTH); w=GWEN_XMLNode_GetIntProperty(nGraph, "width", AQH_MODDEVICES_GRAPH_WIDTH);
h=GWEN_XMLNode_GetIntProperty(nGraph, "height", AQH_MODDEVICES_GRAPH_HEIGHT); h=GWEN_XMLNode_GetIntProperty(nGraph, "height", AQH_MODDEVICES_GRAPH_HEIGHT);
precision=GWEN_XMLNode_GetIntProperty(nGraph, "precision", 2); precision=GWEN_XMLNode_GetIntProperty(nGraph, "precision", 2);
tsBegin=_parseTime(GWEN_XMLNode_GetProperty(nGraph, "begin", "-4h")); tsBegin=AQH_ParseTime(GWEN_XMLNode_GetProperty(nGraph, "begin", "-4h"));
tsEnd=_parseTime(GWEN_XMLNode_GetProperty(nGraph, "end", "0")); tsEnd=AQH_ParseTime(GWEN_XMLNode_GetProperty(nGraph, "end", "0"));
s=GWEN_XMLNode_GetProperty(nGraph, "lowerLimit", NULL); s=GWEN_XMLNode_GetProperty(nGraph, "lowerLimit", NULL);
if (s && *s) { if (s && *s) {
@@ -309,7 +310,7 @@ void _genPageGraph(AQH_DATACLIENT *dc, const char *sGraphId, const char *sFilena
_addCurves(dc, g, nGraph, tsBegin, tsEnd); _addCurves(dc, g, nGraph, tsBegin, tsEnd);
AQDG_TimeGraph_SetupTicks(g, tickFlags, lowerLimit, upperLimit); AQDG_TimeGraph_SetupTicks(g, tickFlags, lowerLimit, upperLimit);
DBG_DEBUG(NULL, "Draw graph for %s", sGraphId); DBG_DEBUG(NULL, "Draw graph %s", sFilename);
drawContext=AQDG_Draw_ContextCairo_Png_new(sFilename, w, h); drawContext=AQDG_Draw_ContextCairo_Png_new(sFilename, w, h);
graphObject=AQDG_GraphWidget_new(NULL, AQDG_OBJECT_OPTIONS_STRETCHX | AQDG_OBJECT_OPTIONS_STRETCHY, drawContext); graphObject=AQDG_GraphWidget_new(NULL, AQDG_OBJECT_OPTIONS_STRETCHX | AQDG_OBJECT_OPTIONS_STRETCHY, drawContext);
AQDG_Object_SetWidth(graphObject, w); AQDG_Object_SetWidth(graphObject, w);
@@ -378,69 +379,6 @@ AQDG_GRAPH_DATAPAIR_LIST *_readCurveData(AQH_DATACLIENT *dc, GWEN_XMLNODE *nCurv
// TODO: move to aqhome.{c,h}
uint64_t _parseTime(const char *s)
{
if (s && *s) {
if (*s=='-') {
uint64_t x=0;
uint64_t now=time(NULL);
s++;
while(*s && isdigit(*s)) {
unsigned int i;
i=*(s++)-'0';
x*=10;
x+=i;
}
if (*s) {
switch(*s) {
case 0:
case 'm': x*=60; break;
case 'h': x*=(60*60); break;
case 'd': x*=(60*60*24); break;
case 'w': x*=(60*60*24*7); break;
case 'M': x*=(60*60*24*30); break;
case 'y': x*=(60*60*24*365); break;
default: break;
}
}
return (now-x);
}
if (*s=='@') {
int y, m, d, H, M, S;
if (6==sscanf(s+1, "%d/%d/%d-%d:%d:%d", &y, &m, &d, &H, &M, &S)) {
GWEN_TIMESTAMP *ts;
uint64_t x=0;
ts=GWEN_Timestamp_new(y, m, d, H, M, S);
x=GWEN_Timestamp_toTimeT(ts);
GWEN_Timestamp_free(ts);
return x;
}
else {
DBG_ERROR(NULL, "Invalid timespec [%s], expected: @YYYY/MM/DD-HH:MM:SS", s);
return (uint64_t) (-1);
}
}
else {
unsigned long int x;
if (1!=sscanf(s, "%lu", &x)) {
DBG_ERROR(NULL, "ERROR: Invalid timestamp");
return (uint64_t) (-1);
}
return (uint64_t) x;
}
}
return 0;
}
GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName) GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName)
{ {
GWEN_XMLNODE *nItem; GWEN_XMLNODE *nItem;
@@ -460,14 +398,14 @@ GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *
void _mkPathForGraph(AQCGI_MODULE *m, const char *sGraphId, GWEN_BUFFER *buf) void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, GWEN_BUFFER *buf)
{ {
AQCGI_SERVICE *sv; AQCGI_SERVICE *sv;
const char *s; const char *s;
sv=AQH_ModService_GetService(m); sv=AQH_ModService_GetService(m);
s=AQCGI_Service_GetCacheFolder(sv); s=AQCGI_Service_GetCacheFolder(sv);
GBAA(buf, "%s%s%s", s, GWEN_DIR_SEPARATOR_S, sGraphId); GBAA(buf, "%s%s%s-%s", s, GWEN_DIR_SEPARATOR_S, sPageId, sGraphId);
AQH_ModService_EscapeToBuffer(s, buf); AQH_ModService_EscapeToBuffer(s, buf);
GBAS(buf, ".png"); GBAS(buf, ".png");
} }

View File

@@ -0,0 +1,522 @@
/****************************************************************************
* 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.h"
#include "./mdevices_page2_graph.h"
#include "aqhome-cgi/modules/mdataclient.h"
#include "aqhome-cgi/modules/devices/mdevices_pagectx.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/debug.h>
#include <gwenhywfar/timestamp.h>
#include <gwenhywfar/text.h>
#include <gwenhywfar/directory.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
/* ------------------------------------------------------------------------------------------------
* defs and enums
* ------------------------------------------------------------------------------------------------
*/
#define GBAS GWEN_Buffer_AppendString
#define GBAA GWEN_Buffer_AppendArgs
#define DOC_PAGE2 "page.html"
#define FOLDER_PAGES "pages2"
/* ------------------------------------------------------------------------------------------------
* forward declarations
* ------------------------------------------------------------------------------------------------
*/
static void _sendPageList(AQCGI_MODULE *m, GWEN_BUFFER *dbuf);
static GWEN_STRINGLIST *_listPageFiles(AQCGI_MODULE *m);
static GWEN_XMLNODE *_readPage(AQCGI_MODULE *m, const char *sPageName);
static GWEN_XMLNODE *_readPageFile(const char *sFilename);
static void _writePage(AQCGI_MODULE *m, AQH_DATACLIENT *dc, GWEN_XMLNODE *nPage, GWEN_BUFFER *dbuf);
static void _writePageChildElements(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *n, GWEN_BUFFER *dbuf);
static void _writeHtmlElement(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf);
static void _writePageElement(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *n, GWEN_BUFFER *dbuf);
static void _writePageForEveryValue(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf);
static void _writePageDataSet(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf);
static void _writePageItem(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf);
static AQH_MD_PAGECTX *_createInitialCtx(AQCGI_MODULE *m, AQH_DATACLIENT *dc);
/* ------------------------------------------------------------------------------------------------
* code
* ------------------------------------------------------------------------------------------------
*/
void AQH_ModDevices_RunPage2Get(AQCGI_MODULE *m, AQCGI_REQUEST *rq,
GWEN_UNUSED AQCGI_SESSION *session,
AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
GWEN_DB_NODE *dbQuery;
const char *sPageId;
DBG_INFO(NULL, "RunPage2Get");
dbQuery=AQCGI_Request_GetDbQuery(rq);
sPageId=dbQuery?GWEN_DB_GetCharValue(dbQuery, "page", 0, NULL):NULL;
if (sPageId && *sPageId) {
GWEN_XMLNODE *fileNode;
fileNode=_readPage(m, sPageId);
if (fileNode) {
GWEN_XMLNODE *nPage;
nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", NULL, NULL);
if (nPage) {
_writePage(m, dc, nPage, dbuf);
AQCGI_Request_AddResponseHeaderData(rq, "Refresh: 120");
}
else {
DBG_ERROR(NULL, "No page element in file for \"%s\"", sPageId);
}
GWEN_XMLNode_free(fileNode);
}
else {
DBG_INFO(NULL, "here");
}
}
else {
DBG_ERROR(NULL, "Reading page list");
_sendPageList(m, dbuf);
}
}
void AQH_ModDevices_RunPage2Graph(AQCGI_MODULE *m, AQCGI_REQUEST *rq,
GWEN_UNUSED AQCGI_SESSION *session,
AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
GWEN_DB_NODE *dbQuery;
const char *sPageId;
const char *sGraphId;
DBG_INFO(NULL, "RunPageGraphGet");
dbQuery=AQCGI_Request_GetDbQuery(rq);
sPageId=GWEN_DB_GetCharValue(dbQuery, "page", 0, NULL);
sGraphId=GWEN_DB_GetCharValue(dbQuery, "graph", 0, NULL);
if (sPageId && *sPageId && sGraphId && *sGraphId)
AQH_ModDevices_HandleGraph(m, rq, dc, sPageId, sGraphId, dbuf);
}
void AQH_ModDevices_SetCharValue(GWEN_DB_NODE *dbDest, uint32_t flags, const char *sVar, const char *sValue, GWEN_DB_NODE *dbContext)
{
if (sValue && *sValue) {
GWEN_BUFFER *tbuf;
int rv;
tbuf=GWEN_Buffer_new(0, 256, 0, 1);
rv=GWEN_DB_ReplaceVars(dbContext, sValue, tbuf);
if (rv<0) {
DBG_ERROR(NULL, "Error replacing vars for %s=[%s]", sVar, sValue);
}
else {
/*DBG_ERROR(NULL, "Setting value [%s]=[%s] (%s)", sVar, GWEN_Buffer_GetStart(tbuf), sValue);*/
GWEN_DB_SetCharValue(dbDest, flags, sVar, GWEN_Buffer_GetStart(tbuf));
}
GWEN_Buffer_free(tbuf);
}
else
GWEN_DB_DeleteVar(dbDest, sVar);
}
void AQH_ModDevices_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 _sendPageList(AQCGI_MODULE *m, GWEN_BUFFER *dbuf)
{
GWEN_STRINGLIST *sl;
GBAS(dbuf, "<h1>Page List</h1>\n");
sl=_listPageFiles(m);
if (sl) {
GWEN_STRINGLISTENTRY *se;
GBAS(dbuf,
"<table class=\"datatable\">\n"
"<thead><tr><th>Page</th><th>Title</th></tr></thead>\n"
"<tbody>\n");
se=GWEN_StringList_FirstEntry(sl);
while(se) {
const char *filename;
filename=GWEN_StringListEntry_Data(se);
if (filename && *filename) {
GWEN_XMLNODE *node;
DBG_ERROR(NULL, "Reading file \"%s\"", filename);
node=_readPageFile(filename);
if (node) {
GWEN_XMLNODE *nPage;
nPage=GWEN_XMLNode_FindFirstTag(node, "page", NULL, NULL);
if (nPage) {
const char *sId;
const char *sTitle;
sId=GWEN_XMLNode_GetProperty(nPage, "id", NULL);
sTitle=GWEN_XMLNode_GetProperty(nPage, "title", sId);
if (sId && *sId)
GBAA(dbuf, "<tr><td><a href=\""DOC_PAGE2"?page=%s\">%s</a></td><td>%s</td></tr>\n", sId, sId, sTitle);
}
GWEN_XMLNode_free(node);
}
}
se=GWEN_StringListEntry_Next(se);
}
GBAS(dbuf, "</tbody></table>");
GWEN_StringList_free(sl);
}
else {
GBAS(dbuf, "No pages.");
}
}
GWEN_STRINGLIST *_listPageFiles(AQCGI_MODULE *m)
{
GWEN_BUFFER *fbuf;
AQCGI_SERVICE *sv;
GWEN_STRINGLIST *sl;
int rv;
sv=AQH_ModService_GetService(m);
fbuf=GWEN_Buffer_new(0, 256, 0, 1);
GBAA(fbuf, "%s%s"FOLDER_PAGES, AQCGI_Service_GetRuntimeFolder(sv), GWEN_DIR_SEPARATOR_S);
sl=GWEN_StringList_new();
rv=GWEN_Directory_GetMatchingFilesRecursively(GWEN_Buffer_GetStart(fbuf), sl, "*.xml");
if (rv<0) {
DBG_INFO(NULL, "Error reading pages (%d)", rv);
GWEN_StringList_free(sl);
GWEN_Buffer_free(fbuf);
return NULL;
}
if (GWEN_StringList_Count(sl)<1) {
GWEN_StringList_free(sl);
GWEN_Buffer_free(fbuf);
return NULL;
}
GWEN_Buffer_free(fbuf);
return sl;
}
GWEN_XMLNODE *_readPage(AQCGI_MODULE *m, const char *sPageName)
{
GWEN_BUFFER *fbuf;
AQCGI_SERVICE *sv;
GWEN_XMLNODE *fileNode;
sv=AQH_ModService_GetService(m);
fbuf=GWEN_Buffer_new(0, 256, 0, 1);
GBAA(fbuf, "%s%s"FOLDER_PAGES"%s", AQCGI_Service_GetRuntimeFolder(sv), GWEN_DIR_SEPARATOR_S, GWEN_DIR_SEPARATOR_S);
AQH_ModService_EscapeToBuffer(sPageName, fbuf);
GBAS(fbuf, ".xml");
fileNode=_readPageFile(GWEN_Buffer_GetStart(fbuf));
if (fileNode==NULL) {
DBG_INFO(NULL, "here");
GWEN_Buffer_free(fbuf);
return NULL;
}
GWEN_Buffer_free(fbuf);
return fileNode;
}
GWEN_XMLNODE *_readPageFile(const char *sFilename)
{
GWEN_XMLNODE *fileNode;
int rv;
fileNode=GWEN_XMLNode_new(GWEN_XMLNodeTypeTag, sFilename);
rv=GWEN_XML_ReadFile(fileNode, sFilename, GWEN_XML_FLAGS_DEFAULT);
if (rv<0) {
DBG_ERROR(NULL, "Error reading \"%s\": %s (%d)", sFilename?sFilename:"<no name>", strerror(errno), errno);
GWEN_XMLNode_free(fileNode);
return NULL;
}
return fileNode;
}
void _writePage(AQCGI_MODULE *m, AQH_DATACLIENT *dc, GWEN_XMLNODE *nPage, GWEN_BUFFER *dbuf)
{
const char *sPageId;
const char *s;
AQH_MD_PAGECTX *ctx;
ctx=_createInitialCtx(m, dc);
sPageId=GWEN_XMLNode_GetProperty(nPage, "id", NULL);
/* title */
s=GWEN_XMLNode_GetProperty(nPage, "title", NULL);
if (s && *s)
GBAA(dbuf, "<h1>%s</h1>\n", s);
_writePageChildElements(ctx, sPageId, nPage, dbuf);
AQH_MDPageCtx_free(ctx);
}
void _writePageChildElements(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
GWEN_XMLNODE *n;
n=GWEN_XMLNode_GetChild(nElement);
while(n) {
_writePageElement(ctx, sPageId, n, dbuf);
n=GWEN_XMLNode_Next(n);
}
}
void _writePageElement(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
if (GWEN_XMLNode_GetType(nElement)==GWEN_XMLNodeTypeTag) {
const char *sElementName;
sElementName=GWEN_XMLNode_GetData(nElement);
if (sElementName && *sElementName) {
if (strcasecmp(sElementName, "dataset")==0)
_writePageDataSet(ctx, sPageId, nElement, dbuf);
else if (strcasecmp(sElementName, "item")==0)
_writePageItem(ctx, sPageId, nElement, dbuf);
else if (strcasecmp(sElementName, "forEveryValue")==0)
_writePageForEveryValue(ctx, sPageId, nElement, dbuf);
else if (strcasecmp(sElementName, "table")==0 ||
strcasecmp(sElementName, "tr")==0 ||
strcasecmp(sElementName, "td")==0 ||
strcasecmp(sElementName, "p")==0)
_writeHtmlElement(ctx, sPageId, nElement, dbuf);
}
}
else if (GWEN_XMLNode_GetType(nElement)==GWEN_XMLNodeTypeData) {
const char *s;
/* copy data after stripping unnecessary blanks */
s=GWEN_XMLNode_GetData(nElement);
if (s && *s) {
GWEN_BUFFER *tbuf;
tbuf=GWEN_Buffer_new(0, 256, 0, 1);
GBAS(tbuf, s);
GWEN_Text_CondenseBuffer(tbuf);
GBAS(dbuf, GWEN_Buffer_GetStart(tbuf));
GWEN_Buffer_free(tbuf);
}
}
}
void _writeHtmlElement(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
const char *sElementName;
const char *sClass;
sElementName=GWEN_XMLNode_GetData(nElement);
sClass=GWEN_XMLNode_GetProperty(nElement, "class", NULL); /* TODO: copy all attributes */
if (sClass)
GBAA(dbuf, "<%s class=\"%s\">", sElementName, sClass);
else
GBAA(dbuf, "<%s>", sElementName);
_writePageChildElements(ctx, sPageId, nElement, dbuf);
GBAA(dbuf, "</%s>", sElementName);
}
void _writePageForEveryValue(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
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) {
DBG_ERROR(NULL, "Writing for value %s", AQH_Value_GetNameForSystem(v));
AQH_ModDevices_SetupDbForDeviceAndValue(v, dbCtx);
_writePageChildElements(ctx, sPageId, nElement, dbuf);
v=AQH_Value_List_Next(v);
}
}
else {
DBG_ERROR(NULL, "No values");
}
}
void _writePageDataSet(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
const char *sRoom;
const char *sVarType;
const char *sModality;
const char *sVarName;
int modality;
int varType;
AQH_MD_PAGECTX *newCtx;
/* prepare new context */
sVarType=GWEN_XMLNode_GetProperty(nElement, "vartype", NULL);
sRoom=GWEN_XMLNode_GetProperty(nElement, "room", NULL);
sModality=GWEN_XMLNode_GetProperty(nElement, "modality", NULL);
sVarName=GWEN_XMLNode_GetProperty(nElement, "varname", NULL);
modality=sModality?AQH_ValueModality_fromString(sModality):AQH_ValueModality_Unknown;
varType=sVarType?AQH_ValueType_fromString(sVarType):AQH_ValueType_Sensor;
newCtx=AQH_MDPageCtx_CopyWithMatchingVars(ctx, varType, sRoom, modality, sVarName);
/* enter context */
_writePageChildElements(newCtx, sPageId, nElement, dbuf);
/* free new context */
AQH_MDPageCtx_free(newCtx);
}
void _writePageItem(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf)
{
GWEN_XMLNODE *n;
const char *sTitle;
/* write title */
sTitle=GWEN_XMLNode_GetProperty(nElement, "title", NULL);
if (sTitle && *sTitle)
GBAA(dbuf, "<h2>%s</h2>\n", sTitle);
/* write real element */
n=GWEN_XMLNode_GetChild(nElement);
while(n) {
const char *sName;
sName=GWEN_XMLNode_GetData(n);
if (sName && *sName) {
if (strcasecmp(sName, "graph")==0)
AQH_ModDevices_PageWriteGraph(ctx, sPageId, n, dbuf);
// else if (strcasecmp(sName, "actor")==0)
// _writePageActor(ctx, sPageId, n, dbuf);
// else if (strcasecmp(sName, "valueTable")==0)
// _writePageValueTable(ctx, sPageId, n, dbuf);
else {
/* unhandled */
}
}
n=GWEN_XMLNode_Next(n);
}
}
AQH_MD_PAGECTX *_createInitialCtx(AQCGI_MODULE *m, AQH_DATACLIENT *dc)
{
AQH_DEVICE_LIST *deviceList;
AQH_VALUE_LIST *valueList;
AQH_MD_PAGECTX *ctx;
AQH_ModDevices_ReadDevicesAndValues(m, dc);
deviceList=AQH_ModDevices_GetDeviceList(m);
valueList=AQH_ModDevices_GetValueList(m);
ctx=AQH_MDPageCtx_new(m, dc);
AQH_MDPageCtx_SetDevices(ctx, deviceList);
if (valueList)
AQH_MDPageCtx_SetValues(ctx, AQH_Value_List_dup(valueList));
return ctx;
}

View File

@@ -0,0 +1,31 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MDEVICES_PAGE2_H
#define AQHOME_CGI_MDEVICES_PAGE2_H
#include "aqhome-cgi/modules/devices/mdevices.h"
#include "aqhome/aqhome.h"
#include "aqhome/dataclient/client.h"
#include <aqcgi/request.h>
#include <gwenhywfar/buffer.h>
#include <gwenhywfar/xml.h>
void AQH_ModDevices_RunPage2Get(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf);
void AQH_ModDevices_RunPage2Graph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf);
void AQH_ModDevices_SetCharValue(GWEN_DB_NODE *dbDest, uint32_t flags, const char *sVar, const char *sValue, GWEN_DB_NODE *dbCtx);
void AQH_ModDevices_SetupDbForDeviceAndValue(const AQH_VALUE *v, GWEN_DB_NODE *dbCtx);
#endif

View File

@@ -0,0 +1,419 @@
/****************************************************************************
* 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 _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, "<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:"");
}

View File

@@ -0,0 +1,25 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MDEVICES_PAGE2_GRAPH_H
#define AQHOME_CGI_MDEVICES_PAGE2_GRAPH_H
#include "aqhome-cgi/modules/devices/mdevices_page2.h"
#include "aqhome-cgi/modules/devices/mdevices_pagectx.h"
void AQH_ModDevices_PageWriteGraph(AQH_MD_PAGECTX *ctx, const char *sPageId, GWEN_XMLNODE *nElement, GWEN_BUFFER *dbuf);
void AQH_ModDevices_HandleGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc,
const char *sPageId, const char *sGraphId,
GWEN_BUFFER *dbuf);
#endif

View File

@@ -0,0 +1,29 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MDEVICES_PAGE2_P_H
#define AQHOME_CGI_MDEVICES_PAGE2_P_H
#include "aqhome-cgi/modules/devices/mdevices_page2.h"
struct AQH_MD_PAGECTX {
GWEN_TREE2_ELEMENT(AQH_MD_PAGECTX);
AQH_DEVICE_LIST *deviceList;
AQH_VALUE_LIST *valueList;
};
#endif

View File

@@ -0,0 +1,211 @@
/****************************************************************************
* 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_pagectx_p.h"
#include <gwenhywfar/misc.h>
#include <gwenhywfar/text.h>
#include <gwenhywfar/debug.h>
/* ------------------------------------------------------------------------------------------------
* global vars
* ------------------------------------------------------------------------------------------------
*/
GWEN_TREE2_FUNCTIONS(AQH_MD_PAGECTX, AQH_MDPageCtx);
/* ------------------------------------------------------------------------------------------------
* forward declarations
* ------------------------------------------------------------------------------------------------
*/
static int _valueMatches(const AQH_VALUE *value, int varType, const char *room, int modality, const char *vname);
/* ------------------------------------------------------------------------------------------------
* code
* ------------------------------------------------------------------------------------------------
*/
AQH_MD_PAGECTX *AQH_MDPageCtx_new(AQCGI_MODULE *module, AQH_DATACLIENT *dataClient)
{
AQH_MD_PAGECTX *ctx;
GWEN_NEW_OBJECT(AQH_MD_PAGECTX, ctx);
if (ctx) {
GWEN_TREE2_INIT(AQH_MD_PAGECTX, ctx, AQH_MDPageCtx);
ctx->module=module;
ctx->dataClient=dataClient;
ctx->dbVars=GWEN_DB_Group_new("vars");
}
return ctx;
}
void AQH_MDPageCtx_free(AQH_MD_PAGECTX *ctx)
{
if (ctx) {
GWEN_DB_Group_free(ctx->dbVars);
AQH_Value_List_free(ctx->valueList);
GWEN_TREE2_FINI(AQH_MD_PAGECTX, ctx, AQH_MDPageCtx);
GWEN_FREE_OBJECT(ctx);
}
}
AQH_MD_PAGECTX *AQH_MDPageCtx_CopyWithMatchingVars(const AQH_MD_PAGECTX *octx,
int varType, const char *room, int modality, const char *vname)
{
AQH_MD_PAGECTX *ctx=NULL;
if (octx) {
ctx=AQH_MDPageCtx_new(octx->module, octx->dataClient);
if (ctx) {
/* copy dbVars */
GWEN_DB_Group_free(ctx->dbVars);
ctx->dbVars=GWEN_DB_Group_dup(octx->dbVars);
/* copy device list */
ctx->deviceList=octx->deviceList;
/* copy matching values from value list */
if (octx->valueList) {
AQH_VALUE_LIST *valueList;
const AQH_VALUE *vSrc;
DBG_ERROR(NULL, "Copying values");
valueList=AQH_Value_List_new();
vSrc=AQH_Value_List_First(octx->valueList);
while(vSrc) {
if (_valueMatches(vSrc, varType, room, modality, vname)) {
AQH_VALUE *vDest;
DBG_ERROR(NULL, "+ %s", AQH_Value_GetNameForSystem(vSrc));
vDest=AQH_Value_dup(vSrc);
AQH_Value_List_Add(vDest, valueList);
}
vSrc=AQH_Value_List_Next(vSrc);
}
AQH_Value_List_free(ctx->valueList);
if (AQH_Value_List_GetCount(valueList))
ctx->valueList=valueList;
else
AQH_Value_List_free(valueList);
}
}
}
return ctx;
}
int _valueMatches(const AQH_VALUE *value, int varType, const char *room, int modality, const char *vname)
{
int matches=1;
/* fast compares first */
if ((varType!=AQH_ValueType_Unknown) && (AQH_Value_GetValueType(value)!=varType))
matches=0;
if (matches && modality && modality!=AQH_Value_GetModality(value))
matches=0;
/* then slower compares */
if (matches && vname) {
const char *sName;
sName=AQH_Value_GetNameForSystem(value);
if (GWEN_Text_ComparePattern(sName, vname, 0)==-1)
matches=0;
}
if (matches && room) {
const AQH_DEVICE *device;
device=AQH_Value_GetDevicePtr(value);
if (device) {
const char *s;
s=AQH_Device_GetRoomName(device);
if (s && strcasecmp(s, room)!=0)
matches=0;
}
}
return matches?1:0;
}
const AQH_DEVICE_LIST *AQH_MDPageCtx_GetDevices(const AQH_MD_PAGECTX *ctx)
{
return ctx?ctx->deviceList:NULL;
}
void AQH_MDPageCtx_SetDevices(AQH_MD_PAGECTX *ctx, const AQH_DEVICE_LIST *dList)
{
if (ctx)
ctx->deviceList=dList;
}
AQH_VALUE_LIST *AQH_MDPageCtx_GetValues(const AQH_MD_PAGECTX *ctx)
{
return ctx?ctx->valueList:NULL;
}
void AQH_MDPageCtx_SetValues(AQH_MD_PAGECTX *ctx, AQH_VALUE_LIST *vList)
{
if (ctx)
ctx->valueList=vList;
}
GWEN_DB_NODE *AQH_MDPageCtx_GetDbVars(const AQH_MD_PAGECTX *ctx)
{
return ctx?ctx->dbVars:NULL;
}
AQCGI_MODULE *AQH_MDPageCtx_GetModule(const AQH_MD_PAGECTX *ctx)
{
return ctx?ctx->module:NULL;
}
AQH_DATACLIENT *AQH_MDPageCtx_GetDataClient(const AQH_MD_PAGECTX *ctx)
{
return ctx?ctx->dataClient:NULL;
}

View File

@@ -0,0 +1,53 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MDEVICES_PAGECTX_H
#define AQHOME_CGI_MDEVICES_PAGECTX_H
#include "aqhome-cgi/modules/devices/mdevices.h"
#include "aqhome/aqhome.h"
#include "aqhome/dataclient/client.h"
#include "aqhome/data/device.h"
#include "aqhome/data/value.h"
#include <aqcgi/request.h>
#include <gwenhywfar/db.h>
#include <gwenhywfar/buffer.h>
#include <gwenhywfar/tree2.h>
typedef struct AQH_MD_PAGECTX AQH_MD_PAGECTX;
GWEN_TREE2_FUNCTION_LIB_DEFS(AQH_MD_PAGECTX, AQH_MDPageCtx, AQHOME_API);
AQH_MD_PAGECTX *AQH_MDPageCtx_new(AQCGI_MODULE *module, AQH_DATACLIENT *dataClient);
void AQH_MDPageCtx_free(AQH_MD_PAGECTX *ctx);
AQH_MD_PAGECTX *AQH_MDPageCtx_CopyWithMatchingVars(const AQH_MD_PAGECTX *octx,
int varType, const char *room, int modality, const char *vname);
AQCGI_MODULE *AQH_MDPageCtx_GetModule(const AQH_MD_PAGECTX *ctx);
AQH_DATACLIENT *AQH_MDPageCtx_GetDataClient(const AQH_MD_PAGECTX *ctx);
const AQH_DEVICE_LIST *AQH_MDPageCtx_GetDevices(const AQH_MD_PAGECTX *ctx);
void AQH_MDPageCtx_SetDevices(AQH_MD_PAGECTX *ctx, const AQH_DEVICE_LIST *dList);
AQH_VALUE_LIST *AQH_MDPageCtx_GetValues(const AQH_MD_PAGECTX *ctx);
void AQH_MDPageCtx_SetValues(AQH_MD_PAGECTX *ctx, AQH_VALUE_LIST *vList);
GWEN_DB_NODE *AQH_MDPageCtx_GetDbVars(const AQH_MD_PAGECTX *ctx);
#endif

View File

@@ -0,0 +1,33 @@
/****************************************************************************
* 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.
****************************************************************************/
#ifndef AQHOME_CGI_MDEVICES_PAGECTX_P_H
#define AQHOME_CGI_MDEVICES_PAGECTX_P_H
#include "aqhome-cgi/modules/devices/mdevices_pagectx.h"
struct AQH_MD_PAGECTX {
GWEN_TREE2_ELEMENT(AQH_MD_PAGECTX);
AQCGI_MODULE *module;
AQH_DATACLIENT *dataClient;
const AQH_DEVICE_LIST *deviceList;
AQH_VALUE_LIST *valueList;
GWEN_DB_NODE *dbVars;
};
#endif

View File

@@ -18,9 +18,13 @@
#include <gwenhywfar/debug.h> #include <gwenhywfar/debug.h>
#include <gwenhywfar/i18n.h> #include <gwenhywfar/i18n.h>
#include <gwenhywfar/stringlist.h> #include <gwenhywfar/stringlist.h>
#include <gwenhywfar/timestamp.h>
#include <gwenhywfar/directory.h> #include <gwenhywfar/directory.h>
#include <stdlib.h> #include <stdlib.h>
#include <time.h>
#include <ctype.h>
@@ -308,6 +312,67 @@ const char *AQH_ValueModality_toString(int i)
uint64_t AQH_ParseTime(const char *s)
{
if (s && *s) {
if (*s=='-') {
uint64_t x=0;
uint64_t now=time(NULL);
s++;
while(*s && isdigit(*s)) {
unsigned int i;
i=*(s++)-'0';
x*=10;
x+=i;
}
if (*s) {
switch(*s) {
case 0:
case 'm': x*=60; break;
case 'h': x*=(60*60); break;
case 'd': x*=(60*60*24); break;
case 'w': x*=(60*60*24*7); break;
case 'M': x*=(60*60*24*30); break;
case 'y': x*=(60*60*24*365); break;
default: break;
}
}
return (now-x);
}
if (*s=='@') {
int y, m, d, H, M, S;
if (6==sscanf(s+1, "%d/%d/%d-%d:%d:%d", &y, &m, &d, &H, &M, &S)) {
GWEN_TIMESTAMP *ts;
uint64_t x=0;
ts=GWEN_Timestamp_new(y, m, d, H, M, S);
x=GWEN_Timestamp_toTimeT(ts);
GWEN_Timestamp_free(ts);
return x;
}
else {
DBG_ERROR(NULL, "Invalid timespec [%s], expected: @YYYY/MM/DD-HH:MM:SS", s);
return (uint64_t) (-1);
}
}
else {
unsigned long int x;
if (1!=sscanf(s, "%lu", &x)) {
DBG_ERROR(NULL, "ERROR: Invalid timestamp");
return (uint64_t) (-1);
}
return (uint64_t) x;
}
}
return 0;
}

View File

@@ -87,6 +87,8 @@ AQHOME_API const char *AQH_ValueModality_toString(int i);
AQHOME_API int AQH_ReadDataFromString(int dataType, const char *s, uint16_t *pDataVal, uint16_t *pDataDenom); AQHOME_API int AQH_ReadDataFromString(int dataType, const char *s, uint16_t *pDataVal, uint16_t *pDataDenom);
AQHOME_API int AQH_ReadDataFromDouble(int dataType, double d, uint16_t *pDataVal, uint16_t *pDataDenom); AQHOME_API int AQH_ReadDataFromDouble(int dataType, double d, uint16_t *pDataVal, uint16_t *pDataDenom);
AQHOME_API uint64_t AQH_ParseTime(const char *s);
#endif #endif

View File

@@ -18,6 +18,7 @@
<headers> <headers>
<header type="sys" loc="pre">aqhome/api.h</header> <header type="sys" loc="pre">aqhome/api.h</header>
<header type="sys" loc="post">aqhome/data/device.h</header>
</headers> </headers>
<inlines> <inlines>
@@ -123,6 +124,18 @@
</member> </member>
<member name="devicePtr" type="AQH_DEVICE" >
<access>public</access>
<flags>volatile</flags>
<dupflags>assign</dupflags>
<copyflags>assign</copyflags>
<getflags>none</getflags>
<setflags>none</setflags>
<default>NULL</default>
<preset>NULL</preset>
</member>
</members> </members>
</type> </type>

View File

@@ -0,0 +1,120 @@
App for Trigger Activated Light
==============================
1. Introduction
---------------
This app listens for VALUE_REPORT messages on the bus. If a preselected value from a preselected node
is received and falls inside lower and upper limit the LED stripe attached to the node which runs this
app will be turned on in the preselected color for a preselected time. If in this time another matching
message is received the light timer gets restarted. If no matching message is received within the
preselected time the light is turned off again.
Typical usage is to watch for a motion message from some node with a motion sensor and react to that.
This app can monitor up to two triggers.
Turning on the light can be conditioned on received values from a VALUE_REPORT falling inside other specific
limits (e.g. only allow the light to be turned on if a LIGHT sensor reports that it is dark enough for the
light to be usefull).
2. Values
---------
2.1. LIGHTCTL_TRIGGER1_SOURCE
This can be used to make this app react to MOTION messages:
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_TRIGGER1_SOURCE -v 0:0:VALUEID:ADDR
VALUE is the value id from a VALUE_REPORT message
ADDR is the address of the sending node.
Example:
React to value report messages with value id 7 from node with address 4
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_TRIGGER1_SOURCE -v 0:0:7:4
2.2. LIGHTCTL_TRIGGER1_LIMITS
Sets the limits for the value of a trigger message which actually triggers the app.
E.g. MOTION messages have values 0 (no motion) and 1 (motion detected). So a value of 1 or higher should
trigger. In this case the lower limit is 1 and the upper limit is 65535 (the highest possible value, but in this
specific case of MOTION messages you could also use "1").
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_TRIGGER1_LIMITS -v 1:65535
This commands makes all values higher than 0 trigger.
2.3. LIGHTCTL_GUARD_SOURCE
A guard source can be set to make triggers conditional.
E.g. if there is enough light in a room turning on a lamp wouldn't make too much sense. In this case
you could define another VALUEID-ADDR pair which can modify the behaviour of this app.
Example:
React to value report messages with value id 0x0b from node with address 9
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_GUARD_SOURCE -v 0:0:0x0b:9
2.4. LIGHTCTL_GUARD_LIMITS
If a message matching LIGHTCTL_GUARD_SOURCE is received its value will be used to modify the
trigger behaviour of the app. If the value falls into the lower and upper limit triggers will
be generated (otherwise trigger messages will be ignored).
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_GUARD_LIMITS -v LOWER_LIMIT:HIGH_LIMIT
E.e. when using LIGHT sensors light is only to be turned when the reported value is below 60 the
correct limits could be set like this:
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_GUARD_LIMITS -v 0:60
2.5. LIGHTCTL_MODE
There are 3 modes:
- 0: OFF (unconditionally OFF)
- 1: ON (unconditionally ON)
- 2: AUTO (ON when triggered by e.g. MOTION message)
2.6. LIGHTCTL_VALUE
Set the color to be used when LED strip is switched ON.
aqhome-tool setdata -N nodes/XXXXXXXX/LIGHTCTL_VALUE -v GREEN:RED:WHITE:BLUE
Example:
Set color of LED strip to blue (half intensity)
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_VALUE -v 0x80
or in modern format:
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_VALUE -v 0:0:0:0x80
2.7. LIGHTCTL_HOLDTIME
On-Time after activation:
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_HOLDTIME TIME_IN_1/10_SECS
Example:
Keep light on after activation for 20 seconds
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_HOLDTIME -v 200
3. Examples
-----------
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_TRIGGER1_SOURCE -v 0:0:7:4 # Value 7 on device with address 4
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_TRIGGER1_LIMITS -v 1:65535 # lower limit 1, higher limit 65535
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_GUARD_SOURCE -v 0:0:0x0b:9 # value 0xb on device with address 9
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_GUARD_LIMITS -v 0:60 # allow ON between 0 (lower limit) and 60 (high limit)
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_MODE -v 2 # mode: 0=off, 1=on, 2=auto
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_HOLDTIME -v 6000 # keep ON for at least 600 secs
aqhome-tool setdata -N nodes/xxxxxxxx/LIGHTCTL_VALUE -v "#0000ff00" # set color (GGRRWWBB or GREEN:RED:WHITE:BLUE)

View File

@@ -143,7 +143,7 @@ appLightControlUpdate_auto:
; sbrs r16, HTIM_FLAGBIT_STATE ; sbrs r16, HTIM_FLAGBIT_STATE
; rjmp appLightControlUpdate_ret ; jump if clear (actor is off) ; rjmp appLightControlUpdate_ret ; jump if clear (actor is off)
; rcall appLightControlActorOn ; rcall appLightControlActorOn
rjmp appLightControlUpdate_ret ; rjmp appLightControlUpdate_ret
appLightControlUpdate_on: appLightControlUpdate_on:
rcall appLightControlActorOn rcall appLightControlActorOn
appLightControlUpdate_ret: appLightControlUpdate_ret:

View File

@@ -3,6 +3,7 @@
<gwbuild> <gwbuild>
<subdirs> <subdirs>
cgi-examples
mcu-examples mcu-examples
</subdirs> </subdirs>

20
doc/cgi-examples/0BUILD Normal file
View File

@@ -0,0 +1,20 @@
<?xml?>
<gwbuild>
<data dist="true">
buero_climate.xml
buero_hum.xml
buero_light.xml
buero_temp.xml
stube_climate.xml
stube_hum.xml
stube_light.xml
stube_temp.xml
</data>
<subdirs>
</subdirs>
</gwbuild>

View File

@@ -0,0 +1,51 @@
<page id="buero_climate" title="Klima im Buero">
<dataset room="Buero">
<table>
<tr>
<dataset modality="Temperature">
<td>
<item>
<graph id="temp4h" title="Temperature (last 4h)" link="page.html?page=buero_temp"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
<tr>
<dataset modality="Humidity">
<td>
<item >
<graph id="hum4h" title="Humidity (last 4h)" link="page.html?page=buero_hum"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
<tr>
<dataset modality="Light">
<td>
<item >
<graph id="light4h" title="Light (last 4h)" link="page.html?page=buero_light"
width="640" height="480" begin="-4h" end="0" refreshtime="120" lowerLimit="-5.0" upperLimit="1034.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="buero_hum" title="Luftfeuchtigkeit im Buero">
<dataset room="Buero" modality="Humidity">
<table>
<tr>
<td>
<item>
<graph id="hum4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="hum24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="hum30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="buero_light" title="Helligkeit im Buero">
<dataset room="Buero" modality="Light">
<table>
<tr>
<td>
<item>
<graph id="light4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="light24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="light30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="buero_temp" title="Temperaturen im Buero">
<dataset room="Buero" modality="Temperature">
<table>
<tr>
<td>
<item>
<graph id="temp4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="temp24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="temp30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,51 @@
<page id="stube_climate" title="Klima im Wohnzimmer">
<dataset room="Stube">
<table>
<tr>
<dataset modality="Temperature">
<td>
<item>
<graph id="temp4h" title="Temperature (last 4h)" link="page.html?page=stube_temp"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
<tr>
<dataset modality="Humidity">
<td>
<item >
<graph id="hum4h" title="Humidity (last 4h)" link="page.html?page=stube_hum"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
<tr>
<dataset modality="Light">
<td>
<item >
<graph id="light4h" title="Light (last 4h)" link="page.html?page=stube_light"
width="640" height="480" begin="-4h" end="0" refreshtime="120" lowerLimit="-5.0" upperLimit="1034.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
</dataset>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="stube_hum" title="Luftfeuchtigkeit im Wohnzimmer">
<dataset room="Stube" modality="Humidity">
<table>
<tr>
<td>
<item>
<graph id="hum4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="hum24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="hum30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="hum12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="110.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="stube_light" title="Helligkeit im Wohnzimmer">
<dataset room="Stube" modality="Light">
<table>
<tr>
<td>
<item>
<graph id="light4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="light24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="light30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="light12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="1034" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -0,0 +1,73 @@
<page id="stube_temp" title="Temperaturen im Wohnzimmer">
<dataset room="Stube" modality="Temperature">
<table>
<tr>
<td>
<item>
<graph id="temp4h" title="Letzten 4 Stunden"
width="640" height="480" begin="-4h" end="0" refreshtime="180" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp12h" title="Letzten 12 Stunden"
width="640" height="480" begin="-12h" end="0" refreshtime="300" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm20" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="temp24h" title="Letzten 24 Stunden"
width="640" height="480" begin="-24h" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm30" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp7d" title="Letzten 7 Tagen"
width="640" height="480" begin="-7d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm210" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
<tr>
<td>
<item>
<graph id="temp30d" title="Letzten 30 Tage"
width="640" height="480" begin="-30d" end="0" refreshtime="600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm840" />
</ForEveryValue>
</graph>
</item>
</td>
<td>
<item>
<graph id="temp12m" title="Letzten 12 Monate"
width="640" height="480" begin="-365d" end="0" refreshtime="3600" lowerLimit="-5.0" upperLimit="30.0" >
<ForEveryValue>
<curve title="$(Device/descriptiveName)" value="$(Value/NameForSystem)" modifier="Lm10080" />
</ForEveryValue>
</graph>
</item>
</td>
</tr>
</table>
</dataset>
</page>

View File

@@ -33,18 +33,18 @@ sensor.
## Schematic ## Schematic
![Schematic](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n28/n28_schematic.png) ![Schematic](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n28/n28_schematic.png)
## PCB Front ## PCB Front
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n28/n28_pcb_front.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n28/n28_pcb_front.png)
## PCB Back ## PCB Back
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n28/n28_pcb_back.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n28/n28_pcb_back.png)

View File

@@ -28,18 +28,18 @@ for light measurement.
## Schematic ## Schematic
![Schematic](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n29/n29_schematic.png) ![Schematic](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n29/n29_schematic.png)
## PCB Front ## PCB Front
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n29/n29_pcb_front.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n29/n29_pcb_front.png)
## PCB Back ## PCB Back
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n29/n29_pcb_back.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n29/n29_pcb_back.png)

View File

@@ -35,19 +35,19 @@ a driver for it).
## Schematic ## Schematic
![Schematic](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n30/n30_schematic.png) ![Schematic](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n30/n30_schematic.png)
## PCB Front ## PCB Front
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n30/n30_pcb_front.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n30/n30_pcb_front.png)
## PCB Back ## PCB Back
![PCB Front](https://gitea.aqbanking.de/martin/aqhcs/raw/branch/master/doc/mcu-examples/n30/n30_pcb_back.png) ![PCB Front](https://gitea.aqbanking.de/martin/aqhomecontrol/raw/branch/master/doc/mcu-examples/n30/n30_pcb_back.png)

View File

@@ -5,6 +5,9 @@
<data dist="true" install="$(pkgdatadir)/scripts"> <data dist="true" install="$(pkgdatadir)/scripts">
getlevel-nlwkn.sh getlevel-nlwkn.sh
getlevel-wsv.sh getlevel-wsv.sh
getweather-brightsky.sh
getweather-openmeteo.sh
getweather-weatherapi.sh
</data> </data>
</gwbuild> </gwbuild>

33
scripts/getweather-brightsky.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# see https://brightsky.dev
#AQHOME_TOOL="./aqhome-tool.sh"
AQHOME_TOOL="/usr/local/bin/aqhome-tool"
POS_CITY="$1"
STATION_ID="$2"
if test -z "$POS_CITY"; then
echo "Usage: $0 CITY STATION_ID"
exit 1;
fi
if test -z "$STATION_ID"; then
echo "Usage: $0 CITY STATION_ID"
exit 1;
fi
JSONFILE="/tmp/weather-brightsky-$POS_CITY.json"
#
# get current weather
#
if ( curl -s -S "https://api.brightsky.dev/current_weather?dwd_station_id=$STATION_ID" -o $JSONFILE ); then
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "brightsky/$POS_CITY" -N "Temperature" -v "@weather/temperature" -U "C"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "brightsky/$POS_CITY" -N "Humidity" -v "@current/relative_humidity" -U "%"
fi

View File

@@ -0,0 +1,39 @@
#!/bin/bash
# see https://open-meteo.com/en/docs/dwd-api
#AQHOME_TOOL="./aqhome-tool.sh"
AQHOME_TOOL="/usr/local/bin/aqhome-tool"
POS_CITY="$1"
POS_LAT="$2"
POS_LONG="$3"
if test -z "$POS_CITY"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE"
exit 1;
fi
if test -z "$POS_LAT"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE"
exit 1;
fi
if test -z "$POS_LONG"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE"
exit 1;
fi
JSONFILE="/tmp/weather-openmeteo-$POS_CITY.json"
#
# get current weather
#
if ( curl -s -S "https://api.open-meteo.com/v1/dwd-icon?latitude=$POS_LAT&longitude=$POS_LONG&current=temperature_2m,relative_humidity_2m" -o $JSONFILE ); then
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "open-meto/$POS_CITY" -N "Temperature" -v "@current/temperature_2m" -U "@current_units/temperature_2m"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "open-meto/$POS_CITY" -N "Humidity" -v "@current/relative_humidity_2m" -U "@current_units/relative_humidity_2m"
fi

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# see https://www.weatherapi.com
#AQHOME_TOOL="./aqhome-tool.sh"
AQHOME_TOOL="/usr/local/bin/aqhome-tool"
POS_CITY="$1"
POS_LAT="$2"
POS_LONG="$3"
API_KEY="$4"
if test -z "$POS_CITY"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$POS_LAT"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$POS_LONG"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$API_KEY"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
JSONFILE="/tmp/weather-oweathermap-$POS_CITY.json"
#
# get current weather
#
if ( curl -s -S "https://api.openweathermap.org/data/2.5/weather?lat=$POS_LAT&lon=$POS_LONG&appid=$API_KEY&units=metric" -o $JSONFILE ); then
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "openweathermap/$POS_CITY" -N "Temperature" -v "@main/temp" -U "C"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "openweathermap/$POS_CITY" -N "Humidity" -v "@main/humidity" -U "%"
fi

View File

@@ -0,0 +1,48 @@
#!/bin/bash
# see https://www.weatherapi.com
#AQHOME_TOOL="./aqhome-tool.sh"
AQHOME_TOOL="/usr/local/bin/aqhome-tool"
POS_CITY="$1"
POS_LAT="$2"
POS_LONG="$3"
API_KEY="$4"
if test -z "$POS_CITY"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$POS_LAT"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$POS_LONG"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
if test -z "$API_KEY"; then
echo "Usage: $0 CITY LATITUDE LONGITUDE APIKEY"
exit 1;
fi
JSONFILE="/tmp/weather-weatherapi-$POS_CITY.json"
#
# get current weather
#
if ( curl -s -S "https://api.weatherapi.com/v1/current.json?key=$API_KEY&q=$POS_LAT,$POS_LONG&aqi=yes" -o $JSONFILE ); then
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "weatherapi/$POS_CITY" -N "Temperature" -v "@current/temp_c" -U "C"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "weatherapi/$POS_CITY" -N "Humidity" -v "@current/humidity" -U "%"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "weatherapi/$POS_CITY" -N "Wetbulb" -v "@current/wetbulb_c" -U "C"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "weatherapi/$POS_CITY" -N "pm2_5" -v "@current/air_quality/pm2_5" -U "ug/m3"
$AQHOME_TOOL adddata -J $JSONFILE -c weather -d "weatherapi/$POS_CITY" -N "pm10" -v "@current/air_quality/pm10" -U "ug/m3"
fi