From c9a3df4eec5464f2b7e3c4c7b37de180834fe6b4 Mon Sep 17 00:00:00 2001 From: Raphael Hehl Date: Sat, 24 Aug 2024 00:05:16 +0200 Subject: [PATCH] add Webhook #3148 (#3163) * WIP add Webhook * fix config html for webhook add tooltips for webhook * webhook: fix not enabling webhook * send webhook as json * Update ApiKey.md * webhook: fix only sending last "Number" * webhook JSON is now closer to the data log in CSV format * webhook: drop timeStampTimeUTC and switch from timeStamp to lastvalue like lokal csv to fix no timestamp on error --------- Co-authored-by: CaCO3 --- .../jomjol_flowcontroll/CMakeLists.txt | 2 +- .../jomjol_flowcontroll/ClassFlowControll.cpp | 15 +- .../jomjol_flowcontroll/ClassFlowControll.h | 3 + .../jomjol_flowcontroll/ClassFlowWebhook.cpp | 143 ++++++++++++++++++ .../jomjol_flowcontroll/ClassFlowWebhook.h | 39 +++++ code/components/jomjol_webhook/CMakeLists.txt | 7 + .../jomjol_webhook/interface_webhook.cpp | 128 ++++++++++++++++ .../jomjol_webhook/interface_webhook.h | 16 ++ code/platformio.ini | 9 +- param-docs/parameter-pages/Webhook/ApiKey.md | 4 + param-docs/parameter-pages/Webhook/Uri.md | 4 + sd-card/config/config.ini | 4 + sd-card/html/edit_config_template.html | 41 +++++ sd-card/html/readconfigparam.js | 8 + 14 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 code/components/jomjol_flowcontroll/ClassFlowWebhook.cpp create mode 100644 code/components/jomjol_flowcontroll/ClassFlowWebhook.h create mode 100644 code/components/jomjol_webhook/CMakeLists.txt create mode 100644 code/components/jomjol_webhook/interface_webhook.cpp create mode 100644 code/components/jomjol_webhook/interface_webhook.h create mode 100644 param-docs/parameter-pages/Webhook/ApiKey.md create mode 100644 param-docs/parameter-pages/Webhook/Uri.md diff --git a/code/components/jomjol_flowcontroll/CMakeLists.txt b/code/components/jomjol_flowcontroll/CMakeLists.txt index 4ad9f317f..2df515fc7 100644 --- a/code/components/jomjol_flowcontroll/CMakeLists.txt +++ b/code/components/jomjol_flowcontroll/CMakeLists.txt @@ -2,6 +2,6 @@ FILE(GLOB_RECURSE app_sources ${CMAKE_CURRENT_SOURCE_DIR}/*.*) idf_component_register(SRCS ${app_sources} INCLUDE_DIRS "." - REQUIRES esp_timer esp_wifi jomjol_tfliteclass jomjol_helper jomjol_controlcamera jomjol_mqtt jomjol_influxdb jomjol_fileserver_ota jomjol_image_proc jomjol_wlan openmetrics) + REQUIRES esp_timer esp_wifi jomjol_tfliteclass jomjol_helper jomjol_controlcamera jomjol_mqtt jomjol_influxdb jomjol_webhook jomjol_fileserver_ota jomjol_image_proc jomjol_wlan openmetrics) diff --git a/code/components/jomjol_flowcontroll/ClassFlowControll.cpp b/code/components/jomjol_flowcontroll/ClassFlowControll.cpp index 255103f1f..51578e1ce 100644 --- a/code/components/jomjol_flowcontroll/ClassFlowControll.cpp +++ b/code/components/jomjol_flowcontroll/ClassFlowControll.cpp @@ -64,6 +64,11 @@ std::string ClassFlowControll::doSingleStep(std::string _stepname, std::string _ _classname = "ClassFlowInfluxDBv2"; } #endif //ENABLE_INFLUXDB + #ifdef ENABLE_WEBHOOK + if ((_stepname.compare("[Webhook]") == 0) || (_stepname.compare(";[Webhook]") == 0)){ + _classname = "ClassFlowWebhook"; + } + #endif //ENABLE_WEBHOOK for (int i = 0; i < FlowControll.size(); ++i) if (FlowControll[i]->name().compare(_classname) == 0){ @@ -109,7 +114,11 @@ std::string ClassFlowControll::TranslateAktstatus(std::string _input) return ("Sending InfluxDBv2"); } #endif //ENABLE_INFLUXDB - + #ifdef ENABLE_WEBHOOK + if (_input.compare("ClassFlowWebhook") == 0) { + return ("Sending Webhook"); + } + #endif //ENABLE_WEBHOOK if (_input.compare("ClassFlowPostProcessing") == 0) { return ("Post-Processing"); } @@ -251,6 +260,10 @@ ClassFlow* ClassFlowControll::CreateClassFlow(std::string _type) cfc = new ClassFlowInfluxDBv2(&FlowControll); } #endif //ENABLE_INFLUXDB + #ifdef ENABLE_WEBHOOK + if (toUpper(_type).compare("[WEBHOOK]") == 0) + cfc = new ClassFlowWebhook(&FlowControll); + #endif //ENABLE_WEBHOOK if (toUpper(_type).compare("[POSTPROCESSING]") == 0) { cfc = new ClassFlowPostProcessing(&FlowControll, flowanalog, flowdigit); diff --git a/code/components/jomjol_flowcontroll/ClassFlowControll.h b/code/components/jomjol_flowcontroll/ClassFlowControll.h index 3021338ba..4e8e2f167 100644 --- a/code/components/jomjol_flowcontroll/ClassFlowControll.h +++ b/code/components/jomjol_flowcontroll/ClassFlowControll.h @@ -17,6 +17,9 @@ #include "ClassFlowInfluxDB.h" #include "ClassFlowInfluxDBv2.h" #endif //ENABLE_INFLUXDB +#ifdef ENABLE_WEBHOOK + #include "ClassFlowWebhook.h" +#endif //ENABLE_WEBHOOK #include "ClassFlowCNNGeneral.h" class ClassFlowControll : diff --git a/code/components/jomjol_flowcontroll/ClassFlowWebhook.cpp b/code/components/jomjol_flowcontroll/ClassFlowWebhook.cpp new file mode 100644 index 000000000..a60919bda --- /dev/null +++ b/code/components/jomjol_flowcontroll/ClassFlowWebhook.cpp @@ -0,0 +1,143 @@ +#ifdef ENABLE_WEBHOOK +#include +#include "ClassFlowWebhook.h" +#include "Helper.h" +#include "connect_wlan.h" + +#include "time_sntp.h" +#include "interface_webhook.h" + +#include "ClassFlowPostProcessing.h" +#include "esp_log.h" +#include "../../include/defines.h" + +#include "ClassLogFile.h" + +#include + +static const char* TAG = "WEBHOOK"; + +void ClassFlowWebhook::SetInitialParameter(void) +{ + uri = ""; + flowpostprocessing = NULL; + previousElement = NULL; + ListFlowControll = NULL; + disabled = false; + WebhookEnable = false; +} + +ClassFlowWebhook::ClassFlowWebhook() +{ + SetInitialParameter(); +} + +ClassFlowWebhook::ClassFlowWebhook(std::vector* lfc) +{ + SetInitialParameter(); + + ListFlowControll = lfc; + for (int i = 0; i < ListFlowControll->size(); ++i) + { + if (((*ListFlowControll)[i])->name().compare("ClassFlowPostProcessing") == 0) + { + flowpostprocessing = (ClassFlowPostProcessing*) (*ListFlowControll)[i]; + } + } +} + +ClassFlowWebhook::ClassFlowWebhook(std::vector* lfc, ClassFlow *_prev) +{ + SetInitialParameter(); + + previousElement = _prev; + ListFlowControll = lfc; + + for (int i = 0; i < ListFlowControll->size(); ++i) + { + if (((*ListFlowControll)[i])->name().compare("ClassFlowPostProcessing") == 0) + { + flowpostprocessing = (ClassFlowPostProcessing*) (*ListFlowControll)[i]; + } + } +} + + +bool ClassFlowWebhook::ReadParameter(FILE* pfile, string& aktparamgraph) +{ + std::vector splitted; + + aktparamgraph = trim(aktparamgraph); + printf("akt param: %s\n", aktparamgraph.c_str()); + + if (aktparamgraph.size() == 0) + if (!this->GetNextParagraph(pfile, aktparamgraph)) + return false; + + if (toUpper(aktparamgraph).compare("[WEBHOOK]") != 0) + return false; + + + + while (this->getNextLine(pfile, &aktparamgraph) && !this->isNewParagraph(aktparamgraph)) + { + ESP_LOGD(TAG, "while loop reading line: %s", aktparamgraph.c_str()); + splitted = ZerlegeZeile(aktparamgraph); + std::string _param = GetParameterName(splitted[0]); + + if ((toUpper(_param) == "URI") && (splitted.size() > 1)) + { + this->uri = splitted[1]; + } + if (((toUpper(_param) == "APIKEY")) && (splitted.size() > 1)) + { + this->apikey = splitted[1]; + } + } + + WebhookInit(uri,apikey); + WebhookEnable = true; + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Webhook Enabled for Uri " + uri); + + printf("uri: %s\n", uri.c_str()); + return true; +} + + +void ClassFlowWebhook::handleMeasurement(string _decsep, string _value) +{ + string _digit, _decpos; + int _pospunkt = _decsep.find_first_of("."); +// ESP_LOGD(TAG, "Name: %s, Pospunkt: %d", _decsep.c_str(), _pospunkt); + if (_pospunkt > -1) + _digit = _decsep.substr(0, _pospunkt); + else + _digit = "default"; + for (int j = 0; j < flowpostprocessing->NUMBERS.size(); ++j) + { + if (_digit == "default") // Set to default first (if nothing else is set) + { + flowpostprocessing->NUMBERS[j]->MeasurementV2 = _value; + } + if (flowpostprocessing->NUMBERS[j]->name == _digit) + { + flowpostprocessing->NUMBERS[j]->MeasurementV2 = _value; + } + } +} + + +bool ClassFlowWebhook::doFlow(string zwtime) +{ + if (!WebhookEnable) + return true; + + if (flowpostprocessing) + { + printf("vor sende WebHook"); + WebhookPublish(flowpostprocessing->GetNumbers()); + } + + return true; +} +#endif //ENABLE_WEBHOOK \ No newline at end of file diff --git a/code/components/jomjol_flowcontroll/ClassFlowWebhook.h b/code/components/jomjol_flowcontroll/ClassFlowWebhook.h new file mode 100644 index 000000000..3900fc092 --- /dev/null +++ b/code/components/jomjol_flowcontroll/ClassFlowWebhook.h @@ -0,0 +1,39 @@ +#ifdef ENABLE_WEBHOOK + +#pragma once + +#ifndef CLASSFWEBHOOK_H +#define CLASSFWEBHOOK_H + +#include "ClassFlow.h" + +#include "ClassFlowPostProcessing.h" + +#include + +class ClassFlowWebhook : + public ClassFlow +{ +protected: + std::string uri, apikey; + ClassFlowPostProcessing* flowpostprocessing; + bool WebhookEnable; + + void SetInitialParameter(void); + + void handleFieldname(string _decsep, string _value); + void handleMeasurement(string _decsep, string _value); + + +public: + ClassFlowWebhook(); + ClassFlowWebhook(std::vector* lfc); + ClassFlowWebhook(std::vector* lfc, ClassFlow *_prev); + + bool ReadParameter(FILE* pfile, string& aktparamgraph); + bool doFlow(string time); + string name(){return "ClassFlowWebhook";}; +}; + +#endif //CLASSFWEBHOOK_H +#endif //ENABLE_WEBHOOK \ No newline at end of file diff --git a/code/components/jomjol_webhook/CMakeLists.txt b/code/components/jomjol_webhook/CMakeLists.txt new file mode 100644 index 000000000..10271c118 --- /dev/null +++ b/code/components/jomjol_webhook/CMakeLists.txt @@ -0,0 +1,7 @@ +FILE(GLOB_RECURSE app_sources ${CMAKE_CURRENT_SOURCE_DIR}/*.*) + +idf_component_register(SRCS ${app_sources} + INCLUDE_DIRS "." + REQUIRES esp_http_client jomjol_logfile jomjol_flowcontroll json) + + diff --git a/code/components/jomjol_webhook/interface_webhook.cpp b/code/components/jomjol_webhook/interface_webhook.cpp new file mode 100644 index 000000000..4fed5ffd8 --- /dev/null +++ b/code/components/jomjol_webhook/interface_webhook.cpp @@ -0,0 +1,128 @@ +#ifdef ENABLE_WEBHOOK +#include "interface_webhook.h" + +#include "esp_log.h" +#include +#include "ClassLogFile.h" +#include "esp_http_client.h" +#include "time_sntp.h" +#include "../../include/defines.h" +#include +#include + + +static const char *TAG = "WEBHOOK"; + +std::string _webhookURI; +std::string _webhookApiKey; + +static esp_err_t http_event_handler(esp_http_client_event_t *evt); + +void WebhookInit(std::string _uri, std::string _apiKey) +{ + _webhookURI = _uri; + _webhookApiKey = _apiKey; +} + +void WebhookPublish(std::vector* numbers) +{ + + + + + cJSON *jsonArray = cJSON_CreateArray(); + + for (int i = 0; i < (*numbers).size(); ++i) + { + string timezw = ""; + char buffer[80]; + struct tm* timeinfo = localtime(&(*numbers)[i]->lastvalue); + strftime(buffer, 80, PREVALUE_TIME_FORMAT_OUTPUT, timeinfo); + timezw = std::string(buffer); + + cJSON *json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "timestamp", timezw.c_str()); + cJSON_AddStringToObject(json, "name", (*numbers)[i]->name.c_str()); + cJSON_AddStringToObject(json, "rawValue", (*numbers)[i]->ReturnRawValue.c_str()); + cJSON_AddStringToObject(json, "value", (*numbers)[i]->ReturnValue.c_str()); + cJSON_AddStringToObject(json, "preValue", (*numbers)[i]->ReturnPreValue.c_str()); + cJSON_AddStringToObject(json, "rate", (*numbers)[i]->ReturnRateValue.c_str()); + cJSON_AddStringToObject(json, "changeAbsolute", (*numbers)[i]->ReturnChangeAbsolute.c_str()); + cJSON_AddStringToObject(json, "error", (*numbers)[i]->ErrorMessageText.c_str()); + + cJSON_AddItemToArray(jsonArray, json); + } + + char *jsonString = cJSON_PrintUnformatted(jsonArray); + + LogFile.WriteToFile(ESP_LOG_INFO, TAG, "sending webhook"); + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "sending JSON: " + std::string(jsonString)); + + char response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0}; + esp_http_client_config_t http_config = { + .url = _webhookURI.c_str(), + .user_agent = "ESP32 Meter reader", + .method = HTTP_METHOD_POST, + .event_handler = http_event_handler, + .buffer_size = MAX_HTTP_OUTPUT_BUFFER, + .user_data = response_buffer + }; + + esp_http_client_handle_t http_client = esp_http_client_init(&http_config); + + esp_http_client_set_header(http_client, "Content-Type", "application/json"); + esp_http_client_set_header(http_client, "APIKEY", _webhookApiKey.c_str()); + + ESP_ERROR_CHECK(esp_http_client_set_post_field(http_client, jsonString, strlen(jsonString))); + + esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_http_client_perform(http_client)); + + if(err == ESP_OK) { + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP request was performed"); + int status_code = esp_http_client_get_status_code(http_client); + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP status code: " + std::to_string(status_code)); + } else { + LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "HTTP request failed"); + } + + + + esp_http_client_cleanup(http_client); + cJSON_Delete(jsonArray); + free(jsonString); +} + +static esp_err_t http_event_handler(esp_http_client_event_t *evt) +{ + switch(evt->event_id) + { + case HTTP_EVENT_ERROR: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client Error encountered"); + break; + case HTTP_EVENT_ON_CONNECTED: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client connected"); + ESP_LOGI(TAG, "HTTP Client Connected"); + break; + case HTTP_EVENT_HEADERS_SENT: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client sent all request headers"); + break; + case HTTP_EVENT_ON_HEADER: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Header: key=" + std::string(evt->header_key) + ", value=" + std::string(evt->header_value)); + break; + case HTTP_EVENT_ON_DATA: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client data recevied: len=" + std::to_string(evt->data_len)); + break; + case HTTP_EVENT_ON_FINISH: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client finished"); + break; + case HTTP_EVENT_DISCONNECTED: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Client Disconnected"); + break; + case HTTP_EVENT_REDIRECT: + LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "HTTP Redirect"); + break; + } + return ESP_OK; +} + +#endif //ENABLE_WEBHOOK \ No newline at end of file diff --git a/code/components/jomjol_webhook/interface_webhook.h b/code/components/jomjol_webhook/interface_webhook.h new file mode 100644 index 000000000..0d1cb0dd1 --- /dev/null +++ b/code/components/jomjol_webhook/interface_webhook.h @@ -0,0 +1,16 @@ +#ifdef ENABLE_WEBHOOK + +#pragma once +#ifndef INTERFACE_WEBHOOK_H +#define INTERFACE_WEBHOOK_H + +#include +#include +#include +#include + +void WebhookInit(std::string _webhookURI, std::string _apiKey); +void WebhookPublish(std::vector* numbers); + +#endif //INTERFACE_WEBHOOK_H +#endif //ENABLE_WEBHOOK \ No newline at end of file diff --git a/code/platformio.ini b/code/platformio.ini index c174baa25..3c86e4a6d 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -58,7 +58,8 @@ build_flags = ${flags:runtime.build_flags} ; ### Sofware options : (can be set in defines.h) -D ENABLE_MQTT - -D ENABLE_INFLUXDB + -D ENABLE_INFLUXDB + -D ENABLE_WEBHOOK -D ENABLE_SOFTAP board_build.partitions = partitions.csv monitor_speed = 115200 @@ -79,7 +80,8 @@ build_flags = ${flags:clangtidy.build_flags} ; ### Sofware options : (can be set in defines.h) -D ENABLE_MQTT - -D ENABLE_INFLUXDB + -D ENABLE_INFLUXDB + -D ENABLE_WEBHOOK ;-D ENABLE_SOFTAP ; ### Debug options : ;-D DEBUG_DETAIL_ON @@ -211,5 +213,6 @@ build_flags = ${flags:clangtidy.build_flags} ; ### Sofware options : -D ENABLE_MQTT - -D ENABLE_INFLUXDB + -D ENABLE_INFLUXDB + -D ENABLE_WEBHOOK ;-D ENABLE_SOFTAP ; disabled diff --git a/param-docs/parameter-pages/Webhook/ApiKey.md b/param-docs/parameter-pages/Webhook/ApiKey.md new file mode 100644 index 000000000..79cf21615 --- /dev/null +++ b/param-docs/parameter-pages/Webhook/ApiKey.md @@ -0,0 +1,4 @@ +# Parameter `ApiKey` +Default Value: `undefined` + +ApiKey sent as Header diff --git a/param-docs/parameter-pages/Webhook/Uri.md b/param-docs/parameter-pages/Webhook/Uri.md new file mode 100644 index 000000000..7a08cf0bd --- /dev/null +++ b/param-docs/parameter-pages/Webhook/Uri.md @@ -0,0 +1,4 @@ +# Parameter `Uri` +Default Value: `undefined` + +URI of the HTTP Endpoint receiving requests, e.g. `http://192.168.1.1/watermeter/webhook`. diff --git a/sd-card/config/config.ini b/sd-card/config/config.ini index 2b133857f..8f40444e5 100644 --- a/sd-card/config/config.ini +++ b/sd-card/config/config.ini @@ -103,6 +103,10 @@ HomeassistantDiscovery = false ;Token = undefined ;main.Fieldname = undefined +;[Webhook] +;Uri = undefined +;ApiKey = undefined + ;[GPIO] ;MainTopicMQTT = wasserzaehler/GPIO ;IO0 = input disabled 10 false false diff --git a/sd-card/html/edit_config_template.html b/sd-card/html/edit_config_template.html index 68b9cd134..713834095 100644 --- a/sd-card/html/edit_config_template.html +++ b/sd-card/html/edit_config_template.html @@ -1324,6 +1324,37 @@

$TOOLTIP_InfluxDBv2_NUMBER.Field + + + +

+ +

+ + + + + + + + + + + + $TOOLTIP_Webhook_Uri + + + + + + + + + + + $TOOLTIP_Webhook_ApiKey + + @@ -2169,6 +2200,9 @@