From 429f1e9ee37cb5fbd1b77a96240b25b82bbf9848 Mon Sep 17 00:00:00 2001 From: gene Date: Mon, 9 Dec 2024 00:19:13 +0100 Subject: [PATCH] v1.2.41 - Fixed possible memory leak in JSON-serialization of *Module* and *ModuleParameter* objects - Added *MQTT Network* program: HG-Mini devices can now be controlled from anywhere via Internet - Implemented *Modules.ParameterGet* API - Added JSON array argument to *Modules.ParameterSet* - Implemented *Programs.Enable* and *Programs.Disable* API - Added *addModule(..)* public method to *HomeGenieHandler* --- src/HomeGenie.cpp | 82 ++++++-- src/HomeGenie.h | 46 ++++- src/data/JsonStore.cpp | 31 +++ src/data/JsonStore.h | 112 ++++++++++ src/data/ProgramStore.cpp | 31 +++ src/data/ProgramStore.h | 166 +++++++++++++++ src/defs.h | 3 +- src/io/IOEventDomains.h | 1 + src/io/IOEventPaths.h | 5 +- src/net/MQTTChannel.h | 40 ++++ src/net/MQTTClient.cpp | 37 ++++ src/net/MQTTClient.h | 293 +++++++++++++++++++++++++++ src/net/MQTTServer.cpp | 10 +- src/net/MQTTServer.h | 9 +- src/net/NetManager.cpp | 41 +++- src/net/NetManager.h | 43 ++-- src/service/EventRouter.cpp | 11 +- src/service/api/APIRequest.h | 3 + src/service/api/HomeGenieHandler.cpp | 112 ++++++++-- src/service/api/HomeGenieHandler.h | 1 + src/version.h | 2 +- 21 files changed, 1001 insertions(+), 78 deletions(-) create mode 100644 src/data/JsonStore.cpp create mode 100644 src/data/JsonStore.h create mode 100644 src/data/ProgramStore.cpp create mode 100644 src/data/ProgramStore.h create mode 100644 src/net/MQTTChannel.h create mode 100644 src/net/MQTTClient.cpp create mode 100644 src/net/MQTTClient.h diff --git a/src/HomeGenie.cpp b/src/HomeGenie.cpp index 8c3aec7..1a522cc 100644 --- a/src/HomeGenie.cpp +++ b/src/HomeGenie.cpp @@ -67,6 +67,24 @@ namespace Service { PowerManager::setWakeUpInterval(0); PowerManager::init(); #endif + + programs.setStatusCallback(this); + programs.load(); +#ifndef DISABLE_MQTT_CLIENT + // create "MQTT Network" program if not already there + auto mqttNetwork = programs.getItem(MQTT_NETWORK_CONFIGURATION); + if (mqttNetwork == nullptr) { + mqttNetwork = new Program(MQTT_NETWORK_CONFIGURATION, "MQTT Network", ""); + programs.addItem(mqttNetwork); + programs.save(); + } + auto programStatus = mqttNetwork->getProperty(IOEventPaths::Program_Status); + if (programStatus == nullptr) { + mqttNetwork->properties.add(new ModuleParameter(IOEventPaths::Program_Status, "")); + } + homeGenieHandler->addModule(mqttNetwork); +#endif + Logger::info("+ Starting HomeGenie service"); } @@ -115,6 +133,18 @@ namespace Service { #endif #endif + +#ifndef DISABLE_MQTT_CLIENT + // Configure MQTT client + auto mqttProgram = programs.getItem(MQTT_NETWORK_CONFIGURATION); + if (mqttProgram != nullptr) { + netManager.getMQTTClient().configure(mqttProgram->properties); + if (mqttProgram->isEnabled) { + netManager.getMQTTClient().enable(); + } + } +#endif + // Add System Diagnostics event handler auto systemDiagnostics = new System::Diagnostics(); systemDiagnostics->setModule(getDefaultModule()); @@ -260,22 +290,24 @@ namespace Service { String parameters = ""; for(int p = 0; p < module->properties.size(); p++) { auto param = module->properties.get(p); - parameters += HomeGenie::createModuleParameter(param->name.c_str(), param->value.c_str(), param->updateTime.c_str()); + auto json = HomeGenie::createModuleParameter(param->name.c_str(), param->value.c_str(), param->updateTime.c_str()); + parameters += String(json); + free((void*)json); if (p < module->properties.size() - 1) { parameters += ","; } } - String out = HomeGenie::createModule(module->domain.c_str(), module->address.c_str(), - module->name.c_str(), module->description.c_str(), module->type.c_str(), - parameters.c_str()); - return out.c_str(); + return HomeGenie::createModule(module->domain.c_str(), module->address.c_str(), + module->name.c_str(), module->description.c_str(), module->type.c_str(), + parameters.c_str()); } unsigned int HomeGenie::writeModuleJSON(ResponseCallback *responseCallback, String* domain, String* address) { auto module = getModule(domain, address); if (module != nullptr) { - String out = getModuleJSON(module); - responseCallback->write(out.c_str()); + auto json = getModuleJSON(module); + responseCallback->write(json); + free((void*)json); } return responseCallback->contentLength; } @@ -296,7 +328,9 @@ namespace Service { firstModule = false; out = ""; } - out += getModuleJSON(module); + auto json = getModuleJSON(module); + out += String(json); + free((void*)json); responseCallback->write(out.c_str()); } } @@ -434,27 +468,29 @@ namespace Service { } #endif - String HomeGenie::createModuleParameter(const char *name, const char *value, const char *timestamp) { + const char* HomeGenie::createModuleParameter(const char *name, const char *value, const char *timestamp) { static const char *parameterTemplate = R"({ - "Name": "%s", - "Value": "%s", - "Description": "%s", - "FieldType": "%s", - "UpdateTime": "%s" - })"; + "Name": "%s", + "Value": "%s", + "Description": "%s", + "FieldType": "%s", + "UpdateTime": "%s" +})"; ssize_t size = snprintf(nullptr, 0, parameterTemplate, name, value, "", "", timestamp ) + 1; +#ifdef BOARD_HAS_PSRAM + char *parameterJson = (char *) ps_malloc(size); +#else char *parameterJson = (char *) malloc(size); +#endif snprintf(parameterJson, size, parameterTemplate, name, value, "", "", timestamp ); - auto p = String(parameterJson); - free(parameterJson); - return p; + return parameterJson; } - String HomeGenie::createModule(const char *domain, const char *address, const char *name, const char *description, + const char* HomeGenie::createModule(const char *domain, const char *address, const char *name, const char *description, const char *deviceType, const char *parameters) { static const char *moduleTemplate = R"({ "Name": "%s", @@ -469,15 +505,17 @@ namespace Service { domain, address, parameters ) + 1; +#ifdef BOARD_HAS_PSRAM + char *moduleJson = (char *) ps_malloc(size); +#else char *moduleJson = (char *) malloc(size); +#endif snprintf(moduleJson, size, moduleTemplate, name, description, deviceType, domain, address, parameters ); - auto m = String(moduleJson); - free(moduleJson); - return m; + return moduleJson; } } diff --git a/src/HomeGenie.h b/src/HomeGenie.h index 729fce1..67e718d 100644 --- a/src/HomeGenie.h +++ b/src/HomeGenie.h @@ -42,6 +42,7 @@ #include "automation/ProgramEngine.h" #include "automation/Scheduler.h" #endif +#include "data/ProgramStore.h" #include "data/Module.h" #include "io/gpio/GPIOPort.h" @@ -58,7 +59,8 @@ #include "service/EventRouter.h" -#define HOMEGENIEMINI_NS_PREFIX "Service::HomeGenie" +#define HOMEGENIEMINI_NS_PREFIX "Service::HomeGenie" +#define MQTT_NETWORK_CONFIGURATION "77" namespace Service { @@ -73,7 +75,7 @@ namespace Service { using namespace Automation; #endif - class HomeGenie: IIOEventReceiver, NetRequestHandler + class HomeGenie: IIOEventReceiver, NetRequestHandler, ProgramStatusListener #ifndef DISABLE_AUTOMATION , SchedulerListener #endif @@ -104,6 +106,40 @@ namespace Service { void onSchedule(Schedule* schedule) override; #endif + // ProgramStatusListener events + void onProgramEnabled(Program* program) override { +#ifndef DISABLE_MQTT_CLIENT + if (program->address == MQTT_NETWORK_CONFIGURATION) { + auto mqttNetwork = programs.getItem(MQTT_NETWORK_CONFIGURATION); + mqttNetwork->setProperty(IOEventPaths::Program_Status, "Running"); + QueuedMessage m; + m.domain = IOEventDomains::HomeAutomation_HomeGenie_Automation; + m.sender = MQTT_NETWORK_CONFIGURATION; + m.event = IOEventPaths::Program_Status; + m.value = "Running"; + eventRouter.signalEvent(m); + netManager.getMQTTClient().enable(); + } +#endif + programs.save(); + } + void onProgramDisabled(Program* program) override { +#ifndef DISABLE_MQTT_CLIENT + if (program->address == MQTT_NETWORK_CONFIGURATION) { + auto mqttNetwork = programs.getItem(MQTT_NETWORK_CONFIGURATION); + mqttNetwork->setProperty(IOEventPaths::Program_Status, "Stopped"); + QueuedMessage m; + m.domain = IOEventDomains::HomeAutomation_HomeGenie_Automation; + m.sender = MQTT_NETWORK_CONFIGURATION; + m.event = IOEventPaths::Program_Status; + m.value = "Stopped"; + eventRouter.signalEvent(m); + netManager.getMQTTClient().disable(); + } +#endif + programs.save(); + } + /** * * @param handler @@ -130,6 +166,8 @@ namespace Service { IOManager& getIOManager(); EventRouter& getEventRouter(); + ProgramStore programs; + Module* getDefaultModule(); Module* getModule(String* domain, String* address); @@ -140,8 +178,8 @@ namespace Service { #ifndef DISABLE_DATA_PROCESSING unsigned int writeParameterHistoryJSON(ModuleParameter* parameter, ResponseCallback *outputCallback, int pageNumber = 0, int pageSize = STATS_HISTORY_RESULTS_DEFAULT_PAGE_SIZE, double rangeStart = 0, double rangeEnd = 0, double maxWidth = 0); #endif - static String createModule(const char *domain, const char *address, const char *name, const char* description, const char *deviceType, const char *parameters); - static String createModuleParameter(const char *name, const char* value, const char *timestamp); + static const char* createModule(const char *domain, const char *address, const char *name, const char* description, const char *deviceType, const char *parameters); + static const char* createModuleParameter(const char *name, const char* value, const char *timestamp); private: static HomeGenie* serviceInstance; diff --git a/src/data/JsonStore.cpp b/src/data/JsonStore.cpp new file mode 100644 index 0000000..28199f7 --- /dev/null +++ b/src/data/JsonStore.cpp @@ -0,0 +1,31 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + + +#include "JsonStore.h" + +namespace Data { + +} \ No newline at end of file diff --git a/src/data/JsonStore.h b/src/data/JsonStore.h new file mode 100644 index 0000000..5cc02a9 --- /dev/null +++ b/src/data/JsonStore.h @@ -0,0 +1,112 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + + +#ifndef HOMEGENIE_MINI_JSONSTORE_H +#define HOMEGENIE_MINI_JSONSTORE_H + +#include +#include + +#include "Config.h" + +namespace Data { + + template + class JsonStore { + public: + LinkedList getItemList(){ + return itemList; + } + void load() { + auto fs = LittleFS; +#ifdef ESP8266 + if(true==fs.begin( )) { +#else + int maxFiles = 1; + if(true == fs.begin(true, "/littlefs", maxFiles , "spiffs") && fs.exists(fileName)) { +#endif + auto f = LittleFS.open(fileName, FILE_READ); + auto jsonItems = f.readString(); + f.close(); + JsonDocument doc; + deserializeJson(doc, jsonItems); + + JsonArray array = doc.as(); + for(JsonVariant v : array) { + + addItem(getItemFromJson(v)); + + } + + } else { + + // TODO: report error / disable scheduler + + } + } + void save() { + auto fs = LittleFS; +#ifdef ESP8266 + if(true==fs.begin( )) { +#else + int maxFiles = 1; + if(true==fs.begin( true, "/littlefs", maxFiles , "spiffs")) { +#endif + auto f = LittleFS.open(fileName, FILE_WRITE); + f.print(getJsonList()); + f.close(); + + } else { + + // TODO: report error / disable scheduler + + } + } + String getJsonList() { + JsonDocument doc; + for (int i = 0; i < itemList.size(); i++) { + auto schedule = itemList.get(i); + auto jsonItems = doc.add(); + getJson(&jsonItems, schedule); + } + String output; + serializeJson(doc, output); + return output; + } + + virtual void addItem(T* item) = 0; + virtual T* getItemFromJson(JsonVariant& json) = 0; + virtual void getJson(JsonObject* jsonItem, T* item) = 0; + + protected: + LinkedList itemList; + const char* fileName; + }; + +} + + +#endif //HOMEGENIE_MINI_JSONSTORE_H diff --git a/src/data/ProgramStore.cpp b/src/data/ProgramStore.cpp new file mode 100644 index 0000000..e39f695 --- /dev/null +++ b/src/data/ProgramStore.cpp @@ -0,0 +1,31 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + +#include "ProgramStore.h" + +namespace Data { + + +} \ No newline at end of file diff --git a/src/data/ProgramStore.h b/src/data/ProgramStore.h new file mode 100644 index 0000000..5e76a07 --- /dev/null +++ b/src/data/ProgramStore.h @@ -0,0 +1,166 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + +#ifndef HOMEGENIE_MINI_PROGRAMSTORE_H +#define HOMEGENIE_MINI_PROGRAMSTORE_H + + +#include "JsonStore.h" +#include "Module.h" +#include "io/IOEventDomains.h" + +namespace Data { + + using namespace Data; + + namespace ProgramField { + static const char address[] = "Address"; + static const char name[] = "Name"; + static const char description[] = "Description"; + static const char isEnabled[] = "IsEnabled"; + static const char properties[] = "Properties"; + } + namespace ProgramData { + static const char fileName[] = "/programs.json"; + } + + class Program: public Module { + public: + Program(const char* address, const char* name, const char* description) { + this->domain = IOEventDomains::HomeAutomation_HomeGenie_Automation; + this->address = address; + this->name = name; + this->description = description; + } + bool isEnabled = false; + }; + + class ProgramStatusListener { + public: + // TODO: virtual void onProgramStopped() = 0; + // TODO: virtual void onProgramStarted() = 0; + virtual void onProgramEnabled(Program* p) = 0; + virtual void onProgramDisabled(Program* p) = 0; + }; + + class ProgramStore: public JsonStore { + public: + ProgramStore() : JsonStore() { + fileName = ProgramData::fileName; + } + + void enable(Program* program) { + if (program!= nullptr && statusCallback != nullptr) { + program->isEnabled = true; + statusCallback->onProgramEnabled(program); + } + } + void disable(Program* program) { + if (program!= nullptr && statusCallback != nullptr) { + program->isEnabled = false; + statusCallback->onProgramDisabled(program); + } + } + + void setStatusCallback(ProgramStatusListener* callback) { + statusCallback = callback; + } + + + // Adds or updates + void addItem(Program* program) override { + int existingIndex = getItemIndex(program->address.c_str()); + if (existingIndex != -1) { + existingIndex = existingIndex; + itemList.remove(existingIndex); + } + itemList.add(existingIndex, program); + save(); + } + Program* getItem(const char* address) { + int idx = getItemIndex(address); + return idx != -1 ? itemList.get(idx) : nullptr; + } + int getItemIndex(const char* address) { + for (int i = 0; i < itemList.size(); i++) { + auto s = itemList.get(i); + if (s->address == address) { + return i; + } + } + return -1; + } + + Program* getItemFromJson(JsonVariant& json) override { + + auto program = new Program( + json[ProgramField::address].as(), + json[ProgramField::name].as(), + json[ProgramField::description].as() + ); + + if (json.containsKey(ProgramField::isEnabled)) { + program->isEnabled = json[ProgramField::isEnabled].as(); + } + + if (json.containsKey(ProgramField::properties)) { + auto properties = json[ProgramField::properties].as(); + for (auto jsonProperty: properties) { + auto p = jsonProperty.as(); + auto moduleProperty = parseModuleParameter(p); + program->properties.add(moduleProperty); + } + } + + return program; + } + void getJson(JsonObject* jsonItem, Program* item) override { + auto s = (*jsonItem); + s[ProgramField::isEnabled] = item->isEnabled; + s[ProgramField::address] = item->address; + s[ProgramField::name] = item->name; + s[ProgramField::description] = item->description; + JsonArray properties = s[ProgramField::properties].to(); + for (int b = 0; b < item->properties.size(); b++) { + auto p = item->properties.get(b); + JsonObject mr = properties.add(); + mr["Name"] = p->name; + mr["Value"] = p->value; + } + } + + static ModuleParameter* parseModuleParameter(JsonObject& json) { + String name = json["Name"].as(); + String value = json["Value"].as(); + return new ModuleParameter(name, value); + } + + private: + ProgramStatusListener* statusCallback = nullptr; + }; + +} + +#endif //HOMEGENIE_MINI_PROGRAMSTORE_H diff --git a/src/defs.h b/src/defs.h index 9f003b2..76503e2 100644 --- a/src/defs.h +++ b/src/defs.h @@ -56,7 +56,7 @@ // disabling SSE and MQTT saves only ~2K of RAM //#define DISABLE_SSE -//#define DISABLE_MQTT +//#define DISABLE_MQTT_BROKER // WPS active for all configurations by default #define CONFIGURE_WITH_WPS @@ -72,6 +72,7 @@ #ifdef ESP8266 #undef CONFIG_AUTOMATION_SPAWN_FREERTOS_TASK + #define DISABLE_MQTT_CLIENT #define DISABLE_UI #define DISABLE_BLUETOOTH #define WebServer ESP8266WebServer diff --git a/src/io/IOEventDomains.h b/src/io/IOEventDomains.h index 496eebc..107e31e 100644 --- a/src/io/IOEventDomains.h +++ b/src/io/IOEventDomains.h @@ -33,6 +33,7 @@ namespace IO { namespace IOEventDomains { const char HomeAutomation_HomeGenie[] = "HomeAutomation.HomeGenie"; + const char HomeAutomation_HomeGenie_Automation[] = "HomeAutomation.HomeGenie.Automation"; const char HomeAutomation_X10[] = "HomeAutomation.X10"; const char HomeAutomation_RemoteControl[] = "HomeAutomation.RemoteControl"; const char Automation_Components[] = "Automation.Components"; diff --git a/src/io/IOEventPaths.h b/src/io/IOEventPaths.h index 85c7c7a..50b2f21 100644 --- a/src/io/IOEventPaths.h +++ b/src/io/IOEventPaths.h @@ -32,15 +32,16 @@ namespace IO { namespace IOEventPaths { + const char Program_Status[] = "Program.Status"; const char Receiver_RawData[] = "Receiver.RawData"; const char Receiver_Command[] = "Receiver.Command"; - const char Status_Level[] = "Status.Level"; - const char Status_ColorHsb[] = "Status.ColorHsb"; const char Sensor_Luminance[] = "Sensor.Luminance"; const char Sensor_ColorHsv[] = "Sensor.ColorHsv"; const char Sensor_Temperature[] = "Sensor.Temperature"; const char Sensor_Humidity[] = "Sensor.Humidity"; const char Sensor_MotionDetect[] = "Sensor.MotionDetect"; + const char Status_Level[] = "Status.Level"; + const char Status_ColorHsb[] = "Status.ColorHsb"; const char Status_IdleTime[] = "Status.IdleTime"; const char Status_Battery[] = "Status.Battery"; const char Status_Error[] = "Status.Error"; diff --git a/src/net/MQTTChannel.h b/src/net/MQTTChannel.h new file mode 100644 index 0000000..b0c560f --- /dev/null +++ b/src/net/MQTTChannel.h @@ -0,0 +1,40 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + + +#ifndef HOMEGENIE_MINI_MQTTCHANNEL_H +#define HOMEGENIE_MINI_MQTTCHANNEL_H + +#include "Config.h" + +namespace Net { + class MQTTChannel { + public: + virtual void broadcast(uint8_t num, String *topic, String *payload) {}; + virtual void broadcast(String *topic, String *payload) = 0; + }; +} + +#endif //HOMEGENIE_MINI_MQTTCHANNEL_H diff --git a/src/net/MQTTClient.cpp b/src/net/MQTTClient.cpp new file mode 100644 index 0000000..3802ace --- /dev/null +++ b/src/net/MQTTClient.cpp @@ -0,0 +1,37 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + + +#include "defs.h" + +#ifndef DISABLE_MQTT_CLIENT + +#include "MQTTClient.h" + +namespace Net { + MQTTRequestHandler* MQTTClient::requestHandler = nullptr; +} + +#endif \ No newline at end of file diff --git a/src/net/MQTTClient.h b/src/net/MQTTClient.h new file mode 100644 index 0000000..3cd40a1 --- /dev/null +++ b/src/net/MQTTClient.h @@ -0,0 +1,293 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + + +#ifndef HOMEGENIE_MINI_MQTTCLIENT_H +#define HOMEGENIE_MINI_MQTTCLIENT_H + +#include +#include + + +#include "MQTTChannel.h" +#include "Task.h" +#include "data/Module.h" +#include "service/api/APIRequest.h" + + +#include "esp_crt_bundle.h" + +// TODO: TLS/SSL support to be completed + +static const char *mosquitto_org_pem PROGMEM = R"EOF(-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL +BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG +A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU +BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv +by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE +BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES +MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp +dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg +UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW +Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA +s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH +3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo +E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT +MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV +6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC +6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf ++pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK +sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 +LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE +m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= +-----END CERTIFICATE-----)EOF"; + + +namespace Net { + using namespace Data; + using namespace Service::API; + + // NetRequestHandler interface + class MQTTRequestHandler { + public: + virtual bool onMqttRequest(void* sender, String&, String&, String&) = 0; // pure virtual + }; + + class MQTTClient: MQTTChannel, Task { + public: + static MQTTRequestHandler* requestHandler; + + MQTTClient() { + setLoopInterval(5000); + }; + + void loop() override { + + if (isEnabled && !clientStarted) { + start(); + } + + } + + void configure(LinkedList& parameters) { + auto address = String(); + auto port = String(); + auto tls = String(); + auto webSockets = String(); + auto username = String(); + auto password = String(); + for (ModuleParameter* p: parameters) { + if(p->name.equals("ConfigureOptions.ServerAddress")) { + address = String(p->value); + } else if (p->name.equals("ConfigureOptions.ServerPort")) { + port = String(p->value); + } else if (p->name.equals("ConfigureOptions.TLS")) { + tls = String(p->value); + } else if (p->name.equals("ConfigureOptions.WebSockets")) { + webSockets = String(p->value); + } else if (p->name.equals("ConfigureOptions.Username")) { + username = String(p->value); + } else if (p->name.equals("ConfigureOptions.Password")) { + password = String(p->value); + } + } + + auto brokerUrl = new String(); + if (tls.equals("on")) { + *brokerUrl = webSockets.equals("on") ? "wss://" : "mqtts://"; + } else { + *brokerUrl = webSockets.equals("on") ? "ws://" : "mqtt://"; + } + *brokerUrl += address + String(":") + port; + + stop(); + + mqtt_cfg.uri = brokerUrl->c_str(); + mqtt_cfg.username = username.c_str(); + mqtt_cfg.password = password.c_str(); + + } + + void enable() { + isEnabled = true; + } + void disable() { + isEnabled = false; + stop(); + } + + void start() { + + if (!clientStarted && ESP_WIFI_STATUS == WL_CONNECTED) { + + /* + mbedtls_ssl_config conf; + mbedtls_ssl_config_init(&conf); + arduino_esp_crt_bundle_attach(&conf); + */ + + client = esp_mqtt_client_init(&mqtt_cfg); + /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */ + esp_mqtt_client_register_event(client, static_cast(ESP_EVENT_ANY_ID), mqtt_event_handler, nullptr); + + if (esp_mqtt_client_start(client) == ESP_OK) { + clientStarted = true; + } + } + + } + + void stop() { + + if (clientStarted) { + esp_mqtt_client_stop(client); + clientStarted = false; + } + + } + + void broadcast(String *topic, String *payload) override { + esp_mqtt_client_publish(client, topic->c_str(), payload->c_str(), (uint16_t)payload->length(), 0, 0); + } + + private: + bool clientStarted = false; + bool isEnabled = false; + esp_mqtt_client_handle_t client = nullptr; + esp_mqtt_client_config_t mqtt_cfg { .uri = "" }; + + /* + * @brief Event handler registered to receive MQTT events + * + * This function is called by the MQTT client event loop. + * + * @param handler_args user data registered to the event. + * @param base Event base for the handler(always MQTT Base in this example). + * @param event_id The id for the received event. + * @param event_data The data for the event, esp_mqtt_event_handle_t. + */ + static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) + { + String topic = Config::system.id + "/#"; + + // Event dispatched from event loop `base` with `event_id` + auto event = static_cast(event_data); + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + + case MQTT_EVENT_CONNECTED: + esp_mqtt_client_subscribe(client, topic.c_str(), 1); + break; + + case MQTT_EVENT_DISCONNECTED: + break; + case MQTT_EVENT_SUBSCRIBED: + break; + case MQTT_EVENT_UNSUBSCRIBED: + break; + case MQTT_EVENT_PUBLISHED: + break; + + case MQTT_EVENT_DATA: { + auto t = String(event->topic) + "/"; + String cid; + String domain; + String address; + String recipient; + String type; + int i = t.indexOf('/'); + while (i > 0) { + if (cid.isEmpty()) { + cid = t.substring(0, i); + } else + if (domain.isEmpty()) { + domain = t.substring(0, i); + } else + if (address.isEmpty()) { + address = t.substring(0, i); + } else + if (type.isEmpty()) { + type = t.substring(0, i); + break; + } + t = t.substring(i + 1); + i = t.indexOf('/'); + } + + if (cid == Config::system.id && domain == "MQTT.Listeners") { + if (address == Config::system.id) { + auto jsonRequest = String(event->data, event->data_len); + JsonDocument doc; + DeserializationError error = deserializeJson(doc, jsonRequest); + if (error.code()) break; + if (type == "command") { + + auto apiUrl = String("/api/") + doc["Domain"].as() + String("/") + doc["Address"].as() + String("/") + doc["Command"].as(); + String data = ""; + auto tid = doc["TransactionId"].as(); + if (requestHandler != nullptr) { + requestHandler->onMqttRequest(nullptr, apiUrl, data, tid); + } + + } else if (type == "request") { + + auto apiUrl = String("/api/") + doc["Request"].as(); + auto data = doc["Data"].as(); + auto tid = doc["TransactionId"].as(); + if (requestHandler != nullptr) { + requestHandler->onMqttRequest(nullptr, apiUrl, data, tid); + } + + } + } + } + } break; + + case MQTT_EVENT_ERROR: + /* + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGI(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGI(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, + strerror(event->error_handle->esp_transport_sock_errno)); + } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGI(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } else { + ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); + } + break; + */ + default: + break; + } + } + }; + +} + +#endif //HOMEGENIE_MINI_MQTTCLIENT_H diff --git a/src/net/MQTTServer.cpp b/src/net/MQTTServer.cpp index 6f84520..6e24a4f 100644 --- a/src/net/MQTTServer.cpp +++ b/src/net/MQTTServer.cpp @@ -79,7 +79,7 @@ namespace Net { case EVENT_PUBLISH: { IO::Logger::trace(":%s [%d] >> PUBLISH to '%s'", MQTTBROKER_NS_PREFIX, num, (*topic_name).c_str()); - auto controlTopic = String ("/") + WiFi.macAddress() + String("/command"); + auto controlTopic = String ("/") + Config::system.id + String("/command"); #if ESP8266 auto msg = mb->data_to_string(payload, length_payload); #else @@ -172,12 +172,12 @@ namespace Net { } } - void MQTTServer::broadcast(uint8_t num, String *topic, String *payload) { - mb->broadcast(num, *topic, (uint8_t *)payload->c_str(), (uint16_t)payload->length()); - } - void MQTTServer::broadcast(String *topic, String *payload) { mb->broadcast(*topic, (uint8_t *)payload->c_str(), (uint16_t)payload->length()); } + void MQTTServer::broadcast(uint8_t num, String *topic, String *payload) { + mb->broadcast(num, *topic, (uint8_t *)payload->c_str(), (uint16_t)payload->length()); + } + } diff --git a/src/net/MQTTServer.h b/src/net/MQTTServer.h index 83fe0b8..ca00077 100644 --- a/src/net/MQTTServer.h +++ b/src/net/MQTTServer.h @@ -34,7 +34,7 @@ #include #include -#include "defs.h" +#include "MQTTChannel.h" #include "Task.h" #include "net/mqtt/MQTTBrokerMini.h" @@ -46,13 +46,11 @@ namespace Net { typedef std::function ApiRequestEvent; /// Simple MQTT Broker implementation over WebSockets - class MQTTServer : Task { + class MQTTServer : MQTTChannel, Task { public: void begin(); void loop() override; - void broadcast(uint8_t num, String* topic, String* payload); - void broadcast(String* topic, String* payload); void onRequest(ApiRequestEvent cb) { apiCallback = cb; } @@ -60,6 +58,9 @@ namespace Net { void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); void mqttCallback(uint8_t num, const Events_t* event, const String* topic_name, uint8_t* payload, uint16_t length_payload); + void broadcast(String *topic, String *payload) override; + void broadcast(uint8_t num, String *topic, String *payload) override; + private: WebSocketsServer* webSocket = nullptr; MQTTBrokerMini* mqttBroker = nullptr; diff --git a/src/net/NetManager.cpp b/src/net/NetManager.cpp index 4b54f2f..391ad12 100644 --- a/src/net/NetManager.cpp +++ b/src/net/NetManager.cpp @@ -40,7 +40,10 @@ namespace Net { // TODO: !!!! IMPLEMENT DESTRUCTOR AS WELL FOR HttpServer and MQTTServer classes delete httpServer; delete webSocket; -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_CLIENT + delete mqttClient; +#endif +#ifndef DISABLE_MQTT_BROKER delete mqttServer; #endif } @@ -115,26 +118,33 @@ namespace Net { webSocket->begin(); Logger::info("| ✔ WebSocket server"); -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_BROKER mqttServer = new MQTTServer(); mqttServer->onRequest([this](uint8_t num, const char* domain, const char* address, const char* command) { auto c = String(command); if (c == "Module.Describe") { - String topic = WiFi.macAddress() + "/" + domain + "/" + address + "/description"; + String topic = Config::system.id + "/" + domain + "/" + address + "/description"; String apiCommand = "/api/" + String(IOEventDomains::HomeAutomation_HomeGenie) + "/Config/Modules.Get/" + domain + "/" + address; - auto cb = MQTTResponseCallback(mqttServer, num, &topic); + auto cb = MQTTResponseCallback((MQTTChannel*)mqttServer, &topic); netRequestHandler->onNetRequest(mqttServer, apiCommand.c_str(), &cb); } else { String apiCommand = "/api/" + String(domain) + "/" + String(address) + "/" + c; - auto cb = MQTTResponseCallback(mqttServer, 0, nullptr); + auto cb = MQTTResponseCallback((MQTTChannel*)mqttServer, nullptr); netRequestHandler->onNetRequest(mqttServer, apiCommand.c_str(), &cb); } }); mqttServer->begin(); - Logger::info("| ✔ MQTT service"); + Logger::info("| ✔ MQTT broker"); +#endif + +#ifndef DISABLE_MQTT_CLIENT + mqttClient = new MQTTClient(); + Net::MQTTClient::requestHandler = this; + Logger::info("| ✔ MQTT client"); #endif + timeClient = new TimeClient(); timeClient->begin(); } @@ -143,10 +153,7 @@ namespace Net { void NetManager::loop() { Logger::verbose("%s loop() >> BEGIN", NETMANAGER_LOG_PREFIX); - for (int i = 0; i < 5; i++) { // higher priority task - webSocket->loop(); - yield(); - } + webSocket->loop(); Logger::verbose("%s loop() << END", NETMANAGER_LOG_PREFIX); } @@ -169,11 +176,23 @@ namespace Net { return *httpServer; } -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_BROKER MQTTServer& NetManager::getMQTTServer() { return *mqttServer; } #endif +#ifndef DISABLE_MQTT_CLIENT + MQTTClient& NetManager::getMQTTClient() { + return *mqttClient; + } + + bool NetManager::onMqttRequest(void *sender, String &req, String &data, String &tid) { + String topic = Config::system.id + "/MQTT.Listeners/" + tid + "/response"; + auto callback = MQTTResponseCallback((MQTTChannel*)mqttClient, &topic); + netRequestHandler->onNetRequest(sender, req.c_str(), &callback); + return false; + } +#endif WebSocketsServer& NetManager::getWebSocketServer() { return *webSocket; diff --git a/src/net/NetManager.h b/src/net/NetManager.h index 089ecdd..be1835e 100644 --- a/src/net/NetManager.h +++ b/src/net/NetManager.h @@ -45,9 +45,12 @@ #include "net/HTTPServer.h" #include "net/WiFiManager.h" -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_BROKER #include "net/MQTTServer.h" #endif +#ifndef DISABLE_MQTT_CLIENT +#include "net/MQTTClient.h" +#endif #ifndef DISABLE_BLUETOOTH #include @@ -81,28 +84,29 @@ namespace Net { void error(const char* s) override {}; }; -#ifndef DISABLE_MQTT +#if !(defined DISABLE_MQTT_BROKER && defined DISABLE_MQTT_CLIENT) class MQTTResponseCallback : public ResponseCallback { public: - MQTTResponseCallback(MQTTServer *server, uint8_t clientId, String* destinationTopic) { - mq = server; - cid = clientId; + MQTTResponseCallback(MQTTChannel *mqttChannel, String* destinationTopic) { + mqtt = mqttChannel; topic = destinationTopic; } void beginGetLength() override { buffer = ""; }; void endGetLength() override { - mq->broadcast(topic, &buffer); + mqtt->broadcast(topic, &buffer); }; void write(const char* s) override { buffer += s; }; - void writeAll(const char* s) override {}; + void writeAll(const char* s) override { + buffer = s; + mqtt->broadcast(topic, &buffer); + }; void error(const char* s) override {}; private: - MQTTServer* mq; - uint8_t cid; + MQTTChannel* mqtt; String* topic; String buffer; }; @@ -215,7 +219,11 @@ namespace Net { /// Network services management - class NetManager : Task, RequestHandler { + class NetManager : Task, RequestHandler +#if !(defined DISABLE_MQTT_BROKER || defined DISABLE_MQTT_CLIENT) + , MQTTRequestHandler +#endif + { public: NetManager(); ~NetManager(); @@ -230,8 +238,11 @@ namespace Net { WiFiManager& getWiFiManager(); HTTPServer& getHttpServer(); -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_BROKER MQTTServer& getMQTTServer(); +#endif +#ifndef DISABLE_MQTT_CLIENT + MQTTClient& getMQTTClient(); #endif WebSocketsServer& getWebSocketServer(); @@ -271,14 +282,22 @@ namespace Net { // END HTTP RequestHandler interface methods +#ifndef DISABLE_MQTT_CLIENT + // MqttRequestHandler overrides + bool onMqttRequest(void* sender, String&, String&, String&) override; +#endif + private: #ifndef DISABLE_BLUETOOTH BluetoothManager *bluetoothManager; #endif WiFiManager *wiFiManager; HTTPServer *httpServer; -#ifndef DISABLE_MQTT +#ifndef DISABLE_MQTT_BROKER MQTTServer *mqttServer; +#endif +#ifndef DISABLE_MQTT_CLIENT + MQTTClient *mqttClient; #endif WebSocketsServer *webSocket; NetRequestHandler* netRequestHandler; diff --git a/src/service/EventRouter.cpp b/src/service/EventRouter.cpp index 7b5c166..3e70780 100644 --- a/src/service/EventRouter.cpp +++ b/src/service/EventRouter.cpp @@ -165,12 +165,17 @@ namespace Service { #endif // #ifndef DISABLE_AUTOMATION -#ifndef DISABLE_MQTT +#if !(defined DISABLE_MQTT_BROKER || defined DISABLE_MQTT_CLIENT) // MQTT auto date = TimeClient::getTimeClient().getFormattedDate(); - auto topic = String(WiFi.macAddress() + "/" + domain + "/" + sender + "/event"); - auto details = Service::HomeGenie::createModuleParameter(eventPath, m.value.c_str(), date.c_str()); + auto topic = String(Config::system.id + "/" + domain + "/" + sender + "/event"); + auto json = HomeGenie::createModuleParameter(eventPath, m.value.c_str(), date.c_str()); + auto details = String(json); + free((void*)json); netManager->getMQTTServer().broadcast(&topic, &details); +#ifndef DISABLE_MQTT_CLIENT + netManager->getMQTTClient().broadcast(&topic, &details); +#endif #endif #ifndef DISABLE_SSE // SSE diff --git a/src/service/api/APIRequest.h b/src/service/api/APIRequest.h index fd11508..11c0102 100644 --- a/src/service/api/APIRequest.h +++ b/src/service/api/APIRequest.h @@ -52,6 +52,8 @@ namespace Service { namespace API { } namespace AutomationApi { + static const char Programs_Enable[] = {"Programs.Enable"}; + static const char Programs_Disable[] = {"Programs.Disable"}; static const char Scheduling_Add[] = {"Scheduling.Add"}; static const char Scheduling_Update[] = {"Scheduling.Update"}; static const char Scheduling_Get[] = {"Scheduling.Get"}; @@ -67,6 +69,7 @@ namespace Service { namespace API { namespace ConfigApi { static const char Modules_List[] = {"Modules.List"}; static const char Modules_Get[] = {"Modules.Get"}; + static const char Modules_ParameterGet[] = {"Modules.ParameterGet"}; static const char Modules_ParameterSet[] = {"Modules.ParameterSet"}; static const char Modules_StatisticsGet[] = {"Modules.StatisticsGet"}; static const char Groups_List[] = {"Groups.List"}; diff --git a/src/service/api/HomeGenieHandler.cpp b/src/service/api/HomeGenieHandler.cpp index 47b7fbf..a8c5262 100644 --- a/src/service/api/HomeGenieHandler.cpp +++ b/src/service/api/HomeGenieHandler.cpp @@ -75,7 +75,8 @@ namespace Service { namespace API { } bool HomeGenieHandler::canHandleDomain(String* domain) { - return domain->equals(IO::IOEventDomains::HomeAutomation_HomeGenie); + return domain->equals(IO::IOEventDomains::HomeAutomation_HomeGenie) + ||domain->equals(IO::IOEventDomains::HomeAutomation_HomeGenie_Automation); } bool HomeGenieHandler::handleRequest(Service::APIRequest *request, ResponseCallback* responseCallback) { @@ -261,6 +262,19 @@ namespace Service { namespace API { } else if (request->Command == AutomationApi::Scheduling_Templates) { responseCallback->writeAll(SCHEDULER_ACTION_TEMPLATES); return true; + } else if (request->Command == AutomationApi::Programs_Enable || request->Command == AutomationApi::Programs_Disable) { + auto address = request->OptionsString.c_str(); + auto program = homeGenie->programs.getItem(address); + if (program != nullptr) { + if (request->Command == AutomationApi::Programs_Enable) { + homeGenie->programs.enable(program); + } else { + homeGenie->programs.disable(program); + } + homeGenie->programs.save(); + responseCallback->writeAll(ApiHandlerResponseText::OK); + return true; + } } #endif } else if (request->Address == "Config") { @@ -285,22 +299,85 @@ namespace Service { namespace API { if (contentLength == 0) return false; homeGenie->writeModuleJSON(responseCallback, &domain, &address); return true; - } else if (request->Command == ConfigApi::Modules_ParameterSet) { + } else if (request->Command == ConfigApi::Modules_ParameterGet) { auto domain = request->getOption(0); auto address = request->getOption(1); - auto propName = request->getOption(2); - auto propValue = WebServer::urlDecode(request->getOption(3)); auto module = homeGenie->getModule(&domain, &address); if (module != nullptr) { - module->setProperty(propName, propValue, nullptr, IOEventDataType::Undefined); - QueuedMessage m; - m.domain = domain; - m.sender = address; - m.event = propName; - m.value = propValue; - homeGenie->getEventRouter().signalEvent(m); - responseCallback->writeAll(ApiHandlerResponseText::OK); - return true; + auto parameter = module->getProperty(request->getOption(2)); + if (parameter != nullptr) { + /* + { + "Name": "ConfigureOptions.Username", + "Value": "", +//TODO: "Description": "5. Username (optional)", +//TODO: "FieldType": "text", +// "UpdateTime": "2024-12-08T02:46:23.1173437Z", +//TODO: (?) "NeedsUpdate": false + } + */ + auto jsonParameter = HomeGenie::createModuleParameter(parameter->name.c_str(), parameter->value.c_str(), parameter->updateTime.c_str()); + responseCallback->writeAll(jsonParameter); + return true; + } + } + } else if (request->Command == ConfigApi::Modules_ParameterSet) { + auto domain = request->getOption(0); + auto address = request->getOption(1); + auto module = homeGenie->getModule(&domain, &address); + + bool isProgramConfiguration = domain.equals(IOEventDomains::HomeAutomation_HomeGenie_Automation); + if (module != nullptr || isProgramConfiguration) { + // Collect parameters + auto parameters = LinkedList(); + if (request->Data.length() > 0) { + JsonDocument doc; + DeserializationError error = deserializeJson(doc, request->Data); + if (!error.code()) { + auto list = doc.as(); + for (JsonPair kv : list) { + auto p = ModuleParameter(kv.key().c_str(), kv.value().as()); + parameters.add(p); + } + } + } else { + auto propName = request->getOption(2); + auto propValue = WebServer::urlDecode(request->getOption(3)); + auto p = ModuleParameter(propName, propValue); + parameters.add(p); + } + // Update module parameters + if (module != nullptr) { + for (ModuleParameter p: parameters) { + if (p.name.isEmpty()) continue; + module->setProperty(p.name, p.value, nullptr, IOEventDataType::Undefined); + QueuedMessage m; + m.domain = domain; + m.sender = address; + m.event = p.name; + m.value = p.value; + homeGenie->getEventRouter().signalEvent(m); + } + responseCallback->writeAll(ApiHandlerResponseText::OK); + return true; + } + // Update program configuration + if (isProgramConfiguration) { +// TODO: move this to a "ProgramManager" class (extend actual ProgramStore) +#ifndef DISABLE_MQTT_CLIENT + if (address.equals(MQTT_NETWORK_CONFIGURATION)) { + auto mqttNetwork = homeGenie->programs.getItem(MQTT_NETWORK_CONFIGURATION); + for (const ModuleParameter& p: parameters) { + mqttNetwork->setProperty(p.name, p.value); + } + homeGenie->programs.save(); + homeGenie->getNetManager().getMQTTClient().configure(mqttNetwork->properties); + } +#endif + responseCallback->writeAll(ApiHandlerResponseText::OK); + return true; + } + return false; } #ifndef DISABLE_DATA_PROCESSING } else if (request->Command == ConfigApi::Modules_StatisticsGet) { @@ -582,4 +659,13 @@ namespace Service { namespace API { return &moduleList; } + bool HomeGenieHandler::addModule(Module* module) { + auto m = getModule(module->name.c_str(), module->address.c_str()); + if (m != nullptr) { + return false; + } + moduleList.add(module); + return true; + } + }} diff --git a/src/service/api/HomeGenieHandler.h b/src/service/api/HomeGenieHandler.h index e047c70..2dcd966 100644 --- a/src/service/api/HomeGenieHandler.h +++ b/src/service/api/HomeGenieHandler.h @@ -55,6 +55,7 @@ namespace Service { namespace API { IOEventDataType dataType) override; Module* getModule(const char* domain, const char* address) override; LinkedList* getModuleList() override; + bool addModule(Module* module); }; static const char* SCHEDULER_ACTION_TEMPLATES PROGMEM = "[\n" diff --git a/src/version.h b/src/version.h index 10fbaa6..a243052 100644 --- a/src/version.h +++ b/src/version.h @@ -29,7 +29,7 @@ #define VERSION_MAJOR 1 #define VERSION_MINOR 2 -#define VERSION_PATCH 40 +#define VERSION_PATCH 41 #define STRING_VALUE(...) STRING_VALUE__(__VA_ARGS__) #define STRING_VALUE__(...) #__VA_ARGS__