diff --git a/lib/ESP32_BleSerial/src/BleSerial.h b/lib/ESP32_BleSerial/src/BleSerial.h index 6eabd8c..1626d1f 100644 --- a/lib/ESP32_BleSerial/src/BleSerial.h +++ b/lib/ESP32_BleSerial/src/BleSerial.h @@ -2,8 +2,8 @@ #include #include -#include -#include +//#include +//#include #include #include "ByteRingBuffer.h" @@ -72,9 +72,9 @@ class BleSerial : public BLECharacteristicCallbacks, public BLEServerCallbacks, Bluetooth LE GATT UUIDs for the Nordic UART profile Change UUID here if required */ - const char *BLE_SERIAL_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; - const char *BLE_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; - const char *BLE_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + const char *BLE_SERIAL_SERVICE_UUID PROGMEM = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; + const char *BLE_RX_UUID PROGMEM = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; + const char *BLE_TX_UUID PROGMEM = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; bool started = false; }; diff --git a/lib/ESP32_BleSerial/src/ByteRingBuffer.h b/lib/ESP32_BleSerial/src/ByteRingBuffer.h index f23af8b..d1a665d 100644 --- a/lib/ESP32_BleSerial/src/ByteRingBuffer.h +++ b/lib/ESP32_BleSerial/src/ByteRingBuffer.h @@ -6,7 +6,7 @@ template class ByteRingBuffer { private: - uint8_t buffer[N]; + uint8_t buffer[N]{}; int head = 0; int tail = 0; diff --git a/lib/duktape-2.7.0/src/duk_config.h b/lib/duktape-2.7.0/src/duk_config.h index ebd78e2..1710372 100644 --- a/lib/duktape-2.7.0/src/duk_config.h +++ b/lib/duktape-2.7.0/src/duk_config.h @@ -2977,7 +2977,7 @@ typedef struct duk_hthread duk_context; #undef DUK_USE_FUNCPTR_ENC16 #define DUK_USE_FUNCTION_BUILTIN #define DUK_USE_FUNC_FILENAME_PROPERTY -#define DUK_USE_FUNC_NAME_PROPERTY +//#define DUK_USE_FUNC_NAME_PROPERTY #undef DUK_USE_GC_TORTURE #undef DUK_USE_GET_MONOTONIC_TIME #undef DUK_USE_GET_RANDOM_DOUBLE diff --git a/src/Config.h b/src/Config.h index a48ded0..57df274 100644 --- a/src/Config.h +++ b/src/Config.h @@ -33,7 +33,7 @@ #include "defs.h" #include -#ifndef DISABLE_AUTOMATION +#ifdef CONFIG_CREATE_AUTOMATION_TASK #include #endif #ifdef ESP32 diff --git a/src/HomeGenie.cpp b/src/HomeGenie.cpp index cdd6c56..f4a5ceb 100644 --- a/src/HomeGenie.cpp +++ b/src/HomeGenie.cpp @@ -98,7 +98,7 @@ namespace Service { xTaskCreate( reinterpret_cast(ProgramEngine::worker), "ScheduledTask", - 20480, // this might require some adjustments + 10240, // this might require some adjustments nullptr, tskIDLE_PRIORITY + 1, nullptr diff --git a/src/HomeGenie.h b/src/HomeGenie.h index d1431b8..1af3bec 100644 --- a/src/HomeGenie.h +++ b/src/HomeGenie.h @@ -126,7 +126,7 @@ namespace Service { Module* getDefaultModule(); Module* getModule(String* domain, String* address); - const char* getModuleJSON(Module* module); + static const char* getModuleJSON(Module* module); unsigned int writeModuleListJSON(ResponseCallback *outputCallback); unsigned int writeModuleJSON(ResponseCallback *outputCallback, String* domain, String* address); unsigned int writeGroupListJSON(ResponseCallback *outputCallback); diff --git a/src/Utility.cpp b/src/Utility.cpp index 2d25e5a..2708a18 100644 --- a/src/Utility.cpp +++ b/src/Utility.cpp @@ -121,7 +121,7 @@ String Utility::getByteString(uint64_t *data, uint16_t length) { return stringData; } -RGBColor Utility::hsv2rgb(float h, float s, float v) { +ColorRGB Utility::hsv2rgb(float h, float s, float v) { float r, g, b; int i = floor(h * 6); @@ -139,10 +139,19 @@ RGBColor Utility::hsv2rgb(float h, float s, float v) { case 5: r = v, g = p, b = q; break; } - RGBColor color; + ColorRGB color; color.r = r * 255; color.g = g * 255; color.b = b * 255; return color; } + +uint32_t Utility::getFreeMem() { +#ifdef ESP8266 + uint32_t freeMem = system_get_free_heap_size(); +#else + uint32_t freeMem = esp_get_free_heap_size(); +#endif + return freeMem; +} \ No newline at end of file diff --git a/src/Utility.h b/src/Utility.h index 98e203e..7732db5 100644 --- a/src/Utility.h +++ b/src/Utility.h @@ -33,11 +33,11 @@ #include "Config.h" -typedef struct RGBColor { +typedef struct ColorRGB { int r; int g; int b; -} RGBColor; +} ColorRGB; // renamed from RGBColor to ColorRGB to prevent conflicts with LGFX class Utility { @@ -54,7 +54,8 @@ class Utility { static String byteToHex(uint64_t b); static uint32_t reverseBits(uint32_t n); static uint8_t reverseByte(uint8_t n); - static RGBColor hsv2rgb(float H, float S, float V); + static ColorRGB hsv2rgb(float H, float S, float V); + static uint32_t getFreeMem(); }; #endif //HOMEGENIE_MINI_UTILITY_H diff --git a/src/automation/ExtendedCron.cpp b/src/automation/ExtendedCron.cpp new file mode 100644 index 0000000..04b4b62 --- /dev/null +++ b/src/automation/ExtendedCron.cpp @@ -0,0 +1,341 @@ +/* + * 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 + * + * + * Releases: + * - 2024-04-20 Initial release + * + */ + +#include "ExtendedCron.h" +#include "Scheduler.h" + +#include "Utility.h" + +namespace Automation { + + Schedule* getCustomEvent(const char* eventName) { + // TODO: to be implemented + return nullptr; + } + + bool ExtendedCron::IsScheduling(time_t date, String& cronExpression, int recursionCount) + { + return getScheduling(date, date, cronExpression, recursionCount).size() > 0; + } + + time_t ExtendedCron::normalizeStartTime(time_t timestamp) { + // TODO: dateStart.AddSeconds(-dateStart.Second).AddMilliseconds(-dateStart.Millisecond) + return timestamp - (timestamp % 60); + } + time_t ExtendedCron::normalizeEndTime(time_t timestamp) { + // TODO: dateEnd.AddSeconds(-dateEnd.Second).AddMilliseconds(-dateEnd.Millisecond) + // .AddSeconds(59).AddMilliseconds(999) + return normalizeStartTime(timestamp) + 59; + } + + LinkedList ExtendedCron::getScheduling(time_t dateStart, time_t dateEnd, String& cronExpression, int recursionCount) + { + // align input time + if (!hasSecondsField(cronExpression.c_str())) { + dateStart = normalizeStartTime(dateStart); + dateEnd = normalizeEndTime(dateEnd); + } + + // '[' and ']' are just aesthetic alias for '(' and ')' + cronExpression.replace("[", "("); + cronExpression.replace("]", ")"); + + String specialChars = "() ;&:|>%!"; + unsigned int charIndex = 0; + auto rootEvalNode = new EvalNode(); + auto evalNode = rootEvalNode; + while (charIndex < cronExpression.length()) + { + char token = cronExpression.charAt(charIndex); + if (token == '\t' || token == '\r' || token == '\n') + token = ' '; + if (specialChars.indexOf(token) >= 0) + { + switch (token) + { + case '(': + evalNode->Child = new EvalNode(evalNode); + evalNode = (EvalNode*)evalNode->Child; + break; + + case ')': + if (evalNode->Parent != nullptr) + { + evalNode = (EvalNode*)evalNode->Parent; + } + else + { + //masterControlProgram.HomeGenie.MigService.RaiseEvent(this, + // Domains.HomeAutomation_HomeGenie, + // SourceModule.Scheduler, cronExpression, Properties.SchedulerError, + // JsonConvert.SerializeObject("Unbalanced parenthesis in '" + cronExpression + "'")); + // TODO: throw error? --> "Unbalanced parenthesis in '" + cronExpression + "'" + return {}; + } + break; + + case ';': // AND + case '&': // AND + case ':': // OR + case '|': // OR + case '>': // TO + case '%': // NOT + case '!': // NOT + // collect operator and switch to next node + evalNode->Operator = token; + evalNode->Sibling = new EvalNode((EvalNode*)evalNode->Parent); + evalNode = (EvalNode*)evalNode->Sibling; + break; + } + + charIndex++; + continue; + } + + String currentExpression = String(token); + charIndex++; + while (charIndex < cronExpression.length()) // collecting plain cron expression + { + token = cronExpression[charIndex]; + if (token != ' ' && specialChars.indexOf(token) >= 0) + { + break; + } + + currentExpression += token; + charIndex++; + } + + currentExpression.trim(); + if (currentExpression.isEmpty()) + continue; + + evalNode->Expression = currentExpression; + + if (currentExpression.startsWith("#")) + { + // TODO: ...? + } + else if (currentExpression.startsWith("@")) + { + // TODO: example + // @SolarTimes.Sunset + 30 + auto start = dateStart; + int addMinutes = 0; + if (currentExpression.indexOf('+') > 0) + { + auto addMin = currentExpression.substring(currentExpression.lastIndexOf('+')); + removeWhiteSpaces(addMin); + addMinutes = addMin.toInt(); + currentExpression = currentExpression.substring(0, currentExpression.lastIndexOf('+')); + } + else if (currentExpression.indexOf('-') > 0) + { + auto addMin = currentExpression.substring(currentExpression.lastIndexOf('-')); + removeWhiteSpaces(addMin); + addMinutes = addMin.toInt(); + currentExpression = currentExpression.substring(0, currentExpression.lastIndexOf('-')); + } + auto eventName = currentExpression.substring(1); + removeWhiteSpaces(eventName); + if (eventName == "SolarTimes.Sunrise") { + // TODO: handleSunrise(evalNode, start, dateEnd, addMinutes); + } else + if (eventName == "SolarTimes.Sunset") { + // TODO: handleSunset(evalNode, start, dateEnd, addMinutes); + } else + if (eventName == "SolarTimes.SolarNoon") { + // TODO: handleSolarNoon(evalNode, start, dateEnd, addMinutes); + } else { + // TODO: lookup for user-defined @ events + // Check expression from scheduled item with a given name + auto eventItem = getCustomEvent(eventName.c_str()); + if (eventItem == nullptr) + { + // TODO: signal error --> "Unknown event name '" + currentExpression + "'" + } + else if (recursionCount >= EXTENDED_CRON_MAX_EVAL_RECURSION) + { + recursionCount = 0; + // TODO: signal error --> "Too much recursion in expression '" + currentExpression + "'" + eventItem->isEnabled = false; + } + else + { + recursionCount++; + if (eventItem->isEnabled) + { + evalNode->Occurrences = getScheduling(dateStart - (addMinutes * 60), + dateEnd - (addMinutes * 60), eventItem->cronExpression, recursionCount); + if (addMinutes != 0) + { + for (short o = 0; o < evalNode->Occurrences.size(); o++) + { + evalNode->Occurrences.set(o, evalNode->Occurrences.get(o) + (addMinutes * 60)); + } + } + } + + recursionCount--; + if (recursionCount < 0) + recursionCount = 0; + } + } + } + else + { + getNextOccurrences(evalNode->Occurrences, dateStart, dateEnd, currentExpression); + } + } + + auto result = evalNodes(rootEvalNode); + auto copy = LinkedList(); + for (int i = 0; i < result->size(); i++) { + copy.add(result->get(i)); + } + delete rootEvalNode; + return copy; + } + + int ExtendedCron::hasSecondsField(const char* str) { + char del = ' '; + size_t count = 0; + if (!str) return -1; + while ((str = strchr(str, del)) != NULL) { + count++; + do str++; while (del == *str); + } + return (int)count + 1 > 5; + } + + void ExtendedCron::getNextOccurrences(LinkedList& occurrences, time_t dateStart, time_t dateEnd, const String& cronExpression) + { + + for (time_t ts = dateStart - 1; ts < dateEnd; ts += 60) { + cron_expr expr; + const char *err = nullptr; + memset(&expr, 0, sizeof(expr)); + cron_parse_expr(cronExpression.c_str(), &expr, &err); + if (err) { + /* invalid expression */ + } else { + time_t checkTime = ts; // + (Config::TimeZone); + time_t next = cron_next(&expr, checkTime); + if (next - checkTime == 1) { + occurrences.add(ts + 1); + } + } + } + + } + + LinkedList* ExtendedCron::evalNodes(EvalNode* currentNode) + { + auto occurs = ¤tNode->Occurrences; + if (currentNode->Child != nullptr) + { + occurs = evalNodes((EvalNode*)currentNode->Child); + } + if (currentNode->Sibling != nullptr && currentNode->Operator != ' ' && currentNode->Operator != '\0') + { + auto matchList = evalNodes((EvalNode*)currentNode->Sibling); + if (currentNode->Operator == ':' || currentNode->Operator == '|') + { + // Include occurrences (OR) + for (auto o : *matchList) + occurs->add(o); + } + else if (currentNode->Operator == '%' || currentNode->Operator == '!') + { + // Exclude occurrences (NOT) + for (auto o : *matchList) { + for (int i = 0; i < occurs->size(); i++) { + auto oo = occurs->get(i); + if (oo == o) { + occurs->remove(i); + break; + } + } + } + } + else if (currentNode->Operator == ';' || currentNode->Operator == '&') + { + // Intersect occurrences (AND) + for (int i = occurs->size() - 1; i >= 0; i--) { + auto oo = occurs->get(i); + bool exists = false; + for (auto o : *matchList) { + if (oo == o) { + exists = true; + break; + } + } + if (!exists) occurs->remove(i); + } + } + else if (currentNode->Operator == '>') + { + if (matchList->size() > 0 && occurs->size() > 0) + { + time_t start = occurs->get(occurs->size() - 1); // last + time_t end = matchList->get(0); // first + time_t inc = start + 60; + while (end != inc && start != inc) + { + occurs->add(inc); + struct tm *tm_struct = localtime(&inc); + if (tm_struct->tm_hour == 23 && tm_struct->tm_min == 59) + { + inc = inc - (23 * 60 * 60); // 23hrs + inc = inc - (59 * 60); // 59min + } + else + { + inc = inc + 60; // move to next minute + } + } + for (auto o: *matchList) { + occurs->add(o); + } + } + } + } + return occurs; + } + + void ExtendedCron::removeWhiteSpaces(String &s) { + s.replace(" ", ""); + s.replace("\t", ""); + s.replace("\v", ""); + s.replace("\f", ""); + s.replace("\r", ""); + s.replace("\n", ""); + } + +} \ No newline at end of file diff --git a/src/automation/ExtendedCron.h b/src/automation/ExtendedCron.h new file mode 100644 index 0000000..b770df6 --- /dev/null +++ b/src/automation/ExtendedCron.h @@ -0,0 +1,85 @@ +/* + * 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 + * + * + * Releases: + * - 2024-04-20 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_EXTENDEDCRON_H +#define HOMEGENIE_MINI_EXTENDEDCRON_H + +#include + +#include "lib/supertinycron/ccronexpr.h" + +#include "Config.h" + +#define EXTENDED_CRON_MAX_EVAL_RECURSION 4 + +namespace Automation { + + class EvalNode + { + public: + LinkedList Occurrences; + void* Child = nullptr; // EvalNode + void* Parent = nullptr; // EvalNode + void* Sibling = nullptr; // EvalNode + String Expression; + char Operator; + + EvalNode() = default; + explicit EvalNode(EvalNode* parentNode): EvalNode() { + Parent = parentNode; + } + + ~EvalNode() { + Occurrences.clear(); + delete (EvalNode*)Child; + delete (EvalNode*)Sibling; + } + }; + + class ExtendedCron { + public: + static bool IsScheduling(time_t date, String& cronExpression, int recursionCount = 0); + + static time_t normalizeStartTime(time_t timestamp); + static time_t normalizeEndTime(time_t timestamp); + + static LinkedList getScheduling(time_t dateStart, time_t dateEnd, String& cronExpression, int recursionCount = 0); + + static int hasSecondsField(const char* str); + + private: + static LinkedList* evalNodes(EvalNode* currentNode); + static void getNextOccurrences(LinkedList& occurrences, time_t dateStart, time_t dateEnd, const String& cronExpression); + static void removeWhiteSpaces(String& s); + }; + +} + + +#endif //HOMEGENIE_MINI_EXTENDEDCRON_H diff --git a/src/automation/ProgramEngine.cpp b/src/automation/ProgramEngine.cpp index 5bbfeb3..40b38a2 100644 --- a/src/automation/ProgramEngine.cpp +++ b/src/automation/ProgramEngine.cpp @@ -42,7 +42,7 @@ namespace Automation { ProgramEngine::ProgramEngine() { #ifndef CONFIG_CREATE_AUTOMATION_TASK -// setLoopInterval(100); + setLoopInterval(100); #endif }; @@ -62,12 +62,16 @@ namespace Automation { } } #else + bool isRunning; void ProgramEngine::loop() { + if (isRunning) return; + isRunning = true; auto jobs = &ProgramEngine::scheduleList; if (jobs->size() > 0) { ScheduledScript scheduledScript(jobs->shift()); scheduledScript.run(); } + isRunning = false; } #endif diff --git a/src/automation/ScheduledScript.cpp b/src/automation/ScheduledScript.cpp index 446748c..1cf6dd3 100644 --- a/src/automation/ScheduledScript.cpp +++ b/src/automation/ScheduledScript.cpp @@ -43,6 +43,9 @@ namespace Automation { return; } + Logger::info(":%s [Scheduler::Event] >> ['%s' running]", PROGRAMENGINE_NS_PREFIX, + schedule->name.c_str()); + duk_context *ctx = duk_create_heap_default(); duk_push_c_lightfunc(ctx, helper_log, 2, 2, 0); @@ -119,8 +122,15 @@ namespace Automation { } duk_ret_t ScheduledScript::pause(duk_context *ctx) { - double seconds = duk_to_number(ctx, 0); - vTaskDelay(portTICK_PERIOD_MS * (seconds * 1000.0F)); + double pauseMs = (1000.0F * duk_to_number(ctx, 0)); +#ifdef CONFIG_CREATE_AUTOMATION_TASK + vTaskDelay(portTICK_PERIOD_MS * pauseMs); +#else + unsigned long start = millis(); + while (millis() - start < pauseMs) { + TaskManager::loop(); + } +#endif return 0; } @@ -132,7 +142,7 @@ namespace Automation { duk_ret_t ScheduledScript::boundModules_level_set(duk_context *ctx) { String level = duk_to_string(ctx, 0); - auto command = String("Control.Level") + String("/") + String(level); + auto command = String(ControlApi::Control_Level) + String("/") + String(level); apiCommand(ctx, command.c_str()); return 0; } @@ -183,10 +193,6 @@ namespace Automation { void ScheduledScript::apiCommand(duk_context *ctx, const char* command) { duk_get_global_string(ctx, DUK_HIDDEN_SYMBOL("schedule")); auto schedule = (Schedule*)duk_get_pointer(ctx, -1); - - Logger::info(":%s [Scheduler::Event] >> ['%s' running]", PROGRAMENGINE_NS_PREFIX, - schedule->name.c_str()); - for (auto mr: schedule->boundModules) { String apiCommand = String("/api/") + mr->domain + String("/") + mr->address + String("/") + String(command); @@ -195,7 +201,6 @@ namespace Automation { auto callback = DummyResponseCallback(); ProgramEngine::apiRequest(schedule, apiCommand.c_str(), &callback); } - } duk_ret_t ScheduledScript::helper_log(duk_context *ctx) { diff --git a/src/automation/Scheduler.cpp b/src/automation/Scheduler.cpp index 0dd4d02..554354c 100644 --- a/src/automation/Scheduler.cpp +++ b/src/automation/Scheduler.cpp @@ -29,6 +29,8 @@ #include "Scheduler.h" +#include "ExtendedCron.h" + namespace Automation { LinkedList Scheduler::scheduleList; SchedulerListener* Scheduler::listener = nullptr; @@ -73,11 +75,13 @@ namespace Automation { } void Scheduler::loop() { + auto now = time(0); //for(;;) { // int lastRun = millis() % 1000; for (int i = 0; i < scheduleList.size(); i++) { auto schedule = scheduleList.get(i); - if (schedule->check()) { + if (schedule->isEnabled && !schedule->wasScheduled(now) && schedule->occurs(now)) { + schedule->setScheduled(now); if (listener != nullptr && schedule->boundModules.size() > 0) { Logger::info(":%s [Scheduler::Event] >> ['%s' triggered]", SCHEDULER_NS_PREFIX, schedule->name.c_str()); diff --git a/src/automation/Scheduler.h b/src/automation/Scheduler.h index 33d4e03..bb2f3f9 100644 --- a/src/automation/Scheduler.h +++ b/src/automation/Scheduler.h @@ -34,9 +34,8 @@ #include #include -#include "Task.h" -#include "lib/supertinycron/ccronexpr.h" #include "data/Module.h" +#include "ExtendedCron.h" #define SCHEDULER_NS_PREFIX "Automation::Scheduler" @@ -74,21 +73,19 @@ namespace Automation { delete bm; } } - bool check() { - bool scheduling = false; - cron_expr expr; - const char* err = nullptr; - memset(&expr, 0, sizeof(expr)); - cron_parse_expr(cronExpression.c_str(), &expr, &err); - if (err) { - /* invalid expression */ - } else { - time_t now = time(nullptr) + (Config::TimeZone); - time_t next = cron_next(&expr, now); - scheduling = (next - now == 1); + bool occurs(time_t ts) { + return ExtendedCron::IsScheduling(ts, cronExpression); + } + bool wasScheduled(time_t ts) { + if (!ExtendedCron::hasSecondsField(cronExpression.c_str())) { + return ExtendedCron::normalizeStartTime(lastOccurrence) == ExtendedCron::normalizeStartTime(ts); } - return scheduling; + return lastOccurrence == ts; + } + void setScheduled(time_t ts) { + lastOccurrence = ts; } + bool isEnabled = true; String name; String description; String data; @@ -96,6 +93,8 @@ namespace Automation { String script; LinkedList boundDevices; // list of device types that can use this schedule LinkedList boundModules; // list of modules using this schedule + private: + time_t lastOccurrence; }; class SchedulerListener { diff --git a/src/defs.h b/src/defs.h index 5e07463..e35d4e9 100644 --- a/src/defs.h +++ b/src/defs.h @@ -38,7 +38,11 @@ #define DEBUGLOG_DEFAULT_LOG_LEVEL_ERROR + #define CONFIG_CREATE_AUTOMATION_TASK +//#define DISABLE_SSE +//#define DISABLE_MQTT + #ifdef ESP8266 #define ESP_WIFI_STATUS WiFi.status() @@ -50,7 +54,7 @@ #endif #ifdef ESP8266 - #define DISABLE_AUTOMATION + #undef CONFIG_CREATE_AUTOMATION_TASK #define DISABLE_UI #define DISABLE_BLUETOOTH_LE #define DISABLE_BLUETOOTH_CLASSIC diff --git a/src/io/sys/Diagnostics.cpp b/src/io/sys/Diagnostics.cpp index 730fb2b..844e637 100644 --- a/src/io/sys/Diagnostics.cpp +++ b/src/io/sys/Diagnostics.cpp @@ -36,19 +36,13 @@ namespace IO { namespace System { setLoopInterval(DIAGNOSTICS_SAMPLING_RATE); } - void Diagnostics::begin() { - - } + void Diagnostics::begin() { } void Diagnostics::loop() { -#ifdef ESP8266 - uint32_t freeMem = system_get_free_heap_size(); -#else - uint32_t freeMem = esp_get_free_heap_size(); -#endif + uint32_t freeMem = Utility::getFreeMem(); if (currentFreeMemory != freeMem) { Logger::trace("@%s [%s %lu]", DIAGNOSTICS_NS_PREFIX, (IOEventPaths::System_BytesFree), freeMem, UnsignedNumber); - sendEvent(domain.c_str(), address.c_str(), (const uint8_t*)(IOEventPaths::System_BytesFree), &freeMem, UnsignedNumber); + sendEvent(domain, address, (const uint8_t*)(IOEventPaths::System_BytesFree), &freeMem, UnsignedNumber); currentFreeMemory = freeMem; } } diff --git a/src/io/sys/Diagnostics.h b/src/io/sys/Diagnostics.h index adb88b8..f13741e 100644 --- a/src/io/sys/Diagnostics.h +++ b/src/io/sys/Diagnostics.h @@ -41,6 +41,7 @@ extern "C" { #include "io/IOEvent.h" #include "io/IOEventDomains.h" #include "io/IOEventPaths.h" +#include "Utility.h" #define DIAGNOSTICS_NS_PREFIX "IO::Sys::Diagnostics" #define DIAGNOSTICS_SAMPLING_RATE 5000L @@ -54,8 +55,8 @@ namespace IO { namespace System { void loop(); private: - String domain = IOEventDomains::HomeAutomation_HomeGenie; - String address = CONFIG_BUILTIN_MODULE_ADDRESS; + const char* domain = IOEventDomains::HomeAutomation_HomeGenie; + const char* address = CONFIG_BUILTIN_MODULE_ADDRESS; uint32_t currentFreeMemory; }; diff --git a/src/net/BluetoothManager.cpp b/src/net/BluetoothManager.cpp index fe34315..85c51c0 100644 --- a/src/net/BluetoothManager.cpp +++ b/src/net/BluetoothManager.cpp @@ -41,7 +41,13 @@ namespace Net { } void BluetoothManager::begin() { - if (Config::isDeviceConfigured()) return; + if (Config::isDeviceConfigured()) { +#ifndef DISABLE_BLUETOOTH_LE + BLEDevice::init(CONFIG_SYSTEM_NAME); + BLEDevice::deinit(true); +#endif + return; + } #ifndef DISABLE_BLUETOOTH_CLASSIC SerialBT.begin(CONFIG_BUILTIN_MODULE_NAME); @@ -50,6 +56,9 @@ namespace Net { #endif #ifndef DISABLE_BLUETOOTH_LE + if (SerialBTLE == nullptr) { + SerialBTLE = new BleSerial(); + } // Get unit MAC address //esp_read_mac(unitMACAddress, ESP_MAC_WIFI_STA); // Convert MAC address to Bluetooth MAC (add 2): https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system.html#mac-address @@ -58,8 +67,8 @@ namespace Net { //sprintf(deviceName, CONFIG_BUILTIN_MODULE_NAME, unitMACAddress[4], unitMACAddress[5]); //Init BLE Serial String bleName = String(CONFIG_BUILTIN_MODULE_NAME) + String("-LE"); - SerialBTLE.begin(bleName.c_str()); - SerialBTLE.setTimeout(10); + SerialBTLE->begin(bleName.c_str()); + SerialBTLE->setTimeout(10); IO::Logger::info("| ✔ Bluetooth LE enabled"); initialized = true; #endif @@ -70,8 +79,8 @@ namespace Net { if (!initialized) return; #ifndef DISABLE_BLUETOOTH_LE - if (SerialBTLE.available()) { - String message = SerialBTLE.readStringUntil('\n'); + if (SerialBTLE->available()) { + String message = SerialBTLE->readStringUntil('\n'); if (message != nullptr) { handleMessage(message); } @@ -128,7 +137,7 @@ namespace Net { if (message.equals("#RESET")) { #ifndef DISABLE_BLUETOOTH_LE - SerialBTLE.end(); + SerialBTLE->end(); #endif #ifndef DISABLE_BLUETOOTH_CLASSIC SerialBT.disconnect(); diff --git a/src/net/BluetoothManager.h b/src/net/BluetoothManager.h index c6d1372..e573286 100644 --- a/src/net/BluetoothManager.h +++ b/src/net/BluetoothManager.h @@ -59,15 +59,12 @@ namespace Net { private: #ifndef DISABLE_BLUETOOTH_LE - BleSerial SerialBTLE; + BleSerial* SerialBTLE = nullptr; #endif #ifndef DISABLE_BLUETOOTH_CLASSIC BluetoothSerial SerialBT; #endif bool initialized = false; - unsigned long lastTs; - uint8_t devicesConnected = 0; - uint8_t currentClients = 0; void handleMessage(String& message); }; diff --git a/src/net/HTTPServer.cpp b/src/net/HTTPServer.cpp index c8ffc37..f4a2de2 100644 --- a/src/net/HTTPServer.cpp +++ b/src/net/HTTPServer.cpp @@ -50,7 +50,9 @@ namespace Net { static WebServer httpServer(HTTP_SERVER_PORT); LinkedList wifiClients; +#ifndef DISABLE_SSE LinkedList events; +#endif String ipAddress; @@ -62,6 +64,8 @@ namespace Net { httpServer.on("/description.xml", HTTP_GET, []() { SSDPDevice.schema(httpServer.client()); }); + +#ifndef DISABLE_SSE static HTTPServer* i = this; httpServer.on("/api/HomeAutomation.HomeGenie/Logging/RealTime.EventStream/", HTTP_GET, []() { i->sseClientAccept(); @@ -71,6 +75,7 @@ namespace Net { i->sseClientAccept(); }); httpServer.addHandler(this); +#endif httpServer.begin(); Logger::info("| ✔ HTTP service"); @@ -114,7 +119,7 @@ namespace Net { httpServer.handleClient(); SSDPDevice.handleClient(); - // TODO: "if (millis() % 50 == 0) ..." lower priority routine +#ifndef DISABLE_SSE if (events.size() > 0) { auto e = events.pop(); @@ -127,12 +132,15 @@ namespace Net { } } - +#endif Logger::verbose("%s loop() << END", HTTPSERVER_LOG_PREFIX); } + void HTTPServer::addHandler(RequestHandler* handler) { + httpServer.addHandler(handler); + } - +#ifndef DISABLE_SSE // BEGIN RequestHandler interface methods bool HTTPServer::canHandle(HTTPMethod method, String uri) { return false; @@ -143,10 +151,6 @@ namespace Net { } // END RequestHandler interface methods - void HTTPServer::addHandler(RequestHandler* handler) { - httpServer.addHandler(handler); - } - void HTTPServer::sendSSEvent(String domain, String address, String event, String value) { auto m = QueuedMessage(domain, address, event, value, nullptr, IOEventDataType::Undefined); events.add(m); @@ -183,4 +187,5 @@ namespace Net { //sseClient.flush(); // connection: CLOSE } +#endif } diff --git a/src/net/HTTPServer.h b/src/net/HTTPServer.h index 4724b2c..4d63b63 100644 --- a/src/net/HTTPServer.h +++ b/src/net/HTTPServer.h @@ -54,15 +54,19 @@ namespace Net { public: void begin(); void loop() override; - void addHandler(RequestHandler* handler); + static void addHandler(RequestHandler* handler); +#ifndef DISABLE_SSE // RequestHandler interface methods bool canHandle(HTTPMethod method, String uri) override; bool handle(WebServer& server, HTTPMethod requestMethod, String requestUri) override; void sendSSEvent(String domain, String address, String event, String value); +#endif private: +#ifndef DISABLE_SSE void sseClientAccept(); void serverSentEventHeader(WiFiClient &client); void serverSentEvent(WiFiClient &client, String &domain, String &address, String &event, String &value); +#endif }; } diff --git a/src/net/NetManager.cpp b/src/net/NetManager.cpp index 6d45c9c..07c86dc 100644 --- a/src/net/NetManager.cpp +++ b/src/net/NetManager.cpp @@ -142,7 +142,8 @@ namespace Net { void NetManager::loop() { Logger::verbose("%s loop() >> BEGIN", NETMANAGER_LOG_PREFIX); - webSocket->loop(); + for (int i = 0; i < 8; i++) + webSocket->loop(); Logger::verbose("%s loop() << END", NETMANAGER_LOG_PREFIX); } diff --git a/src/net/NetManager.h b/src/net/NetManager.h index 53131ef..1b599dc 100644 --- a/src/net/NetManager.h +++ b/src/net/NetManager.h @@ -81,6 +81,7 @@ namespace Net { void error(const char* s) override {}; }; +#ifndef DISABLE_MQTT class MQTTResponseCallback : public ResponseCallback { public: MQTTResponseCallback(MQTTServer *server, uint8_t clientId, String* destinationTopic) { @@ -105,6 +106,7 @@ namespace Net { String* topic; String buffer; }; +#endif // WebSocketResponseCallback class WebSocketResponseCallback : public ResponseCallback { diff --git a/src/service/EventRouter.cpp b/src/service/EventRouter.cpp index a37935c..db1f37f 100644 --- a/src/service/EventRouter.cpp +++ b/src/service/EventRouter.cpp @@ -48,9 +48,10 @@ namespace Service { auto details = Service::HomeGenie::createModuleParameter(m.event.c_str(), m.value.c_str(), date.c_str()); netManager->getMQTTServer().broadcast(&topic, &details); #endif +#ifndef DISABLE_SSE // SSE netManager->getHttpServer().sendSSEvent(m.domain, m.sender, m.event, m.value); - +#endif // WS if (netManager->getWebSocketServer().connectedClients() > 0) { unsigned long epoch = TimeClient::getTimeClient().getEpochTime(); diff --git a/src/service/api/APIRequest.cpp b/src/service/api/APIRequest.cpp index 6484939..6db5fcf 100644 --- a/src/service/api/APIRequest.cpp +++ b/src/service/api/APIRequest.cpp @@ -34,17 +34,20 @@ namespace Service { namespace API { String APIRequest::getOption(unsigned int optionIndex) { unsigned int currentIndex = 0, ci = 0; int oi = OptionsString.indexOf('/'); - if (oi < 0) return OptionsString; + if (oi < 0) return urlDecode(OptionsString); String option; do { option = OptionsString.substring(ci, oi); if (currentIndex == optionIndex) - return option; + return urlDecode(option); ci = oi+1; currentIndex++; oi = OptionsString.indexOf('/', ci); } while (currentIndex < optionIndex); - if (currentIndex == optionIndex) return OptionsString.substring(ci, oi); + if (currentIndex == optionIndex) { + String s = OptionsString.substring(ci, oi); + return urlDecode(s); + } return ""; } diff --git a/src/service/api/APIRequest.h b/src/service/api/APIRequest.h index 159e48e..0cd6219 100644 --- a/src/service/api/APIRequest.h +++ b/src/service/api/APIRequest.h @@ -55,6 +55,7 @@ namespace Service { namespace API { static const char Scheduling_Update[] PROGMEM = {"Scheduling.Update"}; static const char Scheduling_Get[] PROGMEM = {"Scheduling.Get"}; static const char Scheduling_ModuleUpdate[] PROGMEM = {"Scheduling.ModuleUpdate"}; + static const char Scheduling_ListOccurrences[] PROGMEM = {"Scheduling.ListOccurrences"}; static const char Scheduling_Enable[] PROGMEM = {"Scheduling.Enable"}; static const char Scheduling_Disable[] PROGMEM = {"Scheduling.Disable"}; static const char Scheduling_Delete[] PROGMEM = {"Scheduling.Delete"}; @@ -80,6 +81,45 @@ namespace Service { namespace API { String Data; String getOption(unsigned int optionIndex); static APIRequest parse(String command); + + protected: + static String urlDecode(String& str) + { + String encodedString = ""; + char c; + char code0; + char code1; + for (int i = 0; i < str.length(); i++){ + c=str.charAt(i); + if (c == '+') { + encodedString += ' '; + } else if (c == '%') { + i++; + code0=str.charAt(i); + i++; + code1=str.charAt(i); + c = (hexToInt(code0) << 4) | hexToInt(code1); + encodedString += c; + } else { + encodedString += c; + } + //yield(); + } + return encodedString; + } + static unsigned char hexToInt(char c) + { + if (c >= '0' && c <='9'){ + return((unsigned char)c - '0'); + } + if (c >= 'a' && c <='f'){ + return((unsigned char)c - 'a' + 10); + } + if (c >= 'A' && c <='F'){ + return((unsigned char)c - 'A' + 10); + } + return(0); + } }; }} diff --git a/src/service/api/HomeGenieHandler.cpp b/src/service/api/HomeGenieHandler.cpp index bfda726..6efcb00 100644 --- a/src/service/api/HomeGenieHandler.cpp +++ b/src/service/api/HomeGenieHandler.cpp @@ -170,6 +170,69 @@ namespace Service { namespace API { } else { // TODO: report error (module not found) } + return false; + } else if (request->Command == AutomationApi::Scheduling_ListOccurrences) { + int hours = request->getOption(0).toInt(); + auto dateStart = request->getOption(1); //YYYY-MM-DD HH:mm:ss + auto cronExpression = request->getOption(2); + + int year, month, day, hour, min, sec; + if (sscanf(dateStart.c_str(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec) == 6) + { + struct tm t; + time_t start, end; + + t.tm_year = year - 1900; // Year - 1900 + t.tm_mon = month - 1; // Month, where 0 = jan + t.tm_mday = day; // Day of the month + t.tm_hour = hour; + t.tm_min = min; + t.tm_sec = sec; + t.tm_isdst = -1; // Is DST on? 1 = yes, 0 = no, -1 = unknown + + start = mktime(&t); + end = start + (hours * 60 * 60) - 60; + + auto list = ExtendedCron::getScheduling(start, end, cronExpression); + /* + String s; + s += (R"([{ "CronExpression": ")"); + s += (cronExpression.c_str()); + s += (R"(", "StartDate": ")"); + s += (dateStart.c_str()); + s += (R"(", "Occurrences": [)"); + + for (int i = 0; i < list.size(); i++) { + auto o = list.get(i); + String sv = String(o) + String("000"); + s += (sv.c_str()); + if (i < list.size() - 1) { + s += (","); + } + } + s += ("]}]"); + responseCallback->writeAll(s.c_str()); + //*/ + //* + JsonDocument doc; + auto arr = doc.add(); + auto obj = arr.add(); + obj["CronExpression"] = cronExpression; + obj["StartDate"] = dateStart; + auto occurrencesList = obj["Occurrences"] = doc.add(); + for (time_t o : list) { + occurrencesList.add((long long)o * 1000); // convert to milliseconds + } + String output; + serializeJson(arr, output); + + responseCallback->writeAll(output.c_str()); + //*/ + return true; + } else { + // TODO: report date format error? + } + return false; } else if (request->Command == AutomationApi::Scheduling_Delete) { // TODO: return -1 or the index of deleted item diff --git a/src/ui/Utility.h b/src/ui/Utility.h index ea35272..8543817 100644 --- a/src/ui/Utility.h +++ b/src/ui/Utility.h @@ -23,8 +23,8 @@ * */ -#ifndef HOMEGENIE_MINI_UTILITY_H -#define HOMEGENIE_MINI_UTILITY_H +#ifndef HOMEGENIE_MINI_UI_UTILITY_H +#define HOMEGENIE_MINI_UI_UTILITY_H namespace UI { @@ -93,4 +93,4 @@ namespace UI { } -#endif //HOMEGENIE_MINI_UTILITY_H +#endif //HOMEGENIE_MINI_UI_UTILITY_H