diff --git a/README.md b/README.md index e17ce28..541776b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ https://homegenie.it/mini - Easy device configuration using Wi-Fi protected setup button (WPS) or Bluetooth - Does not require an Internet connection to be configured or to work properly - Time synchronization using internal RTC (ESP32), mobile app time or NTP -- Integrated actions scheduler supporting *extended cron expressions* and JavaScript +- Integrated actions scheduler supporting *extended Cron expressions* and JavaScript - Device discovery via SSDP/UPnP with customizable device name - Multi-channel I/O: HTTP, WebSocket, SSE, MQTT, Serial - Built-in status LED logic diff --git a/examples/smart-sensor/CommonSensors.h b/examples/smart-sensor/CommonSensors.h index 818b8a3..5fc2840 100644 --- a/examples/smart-sensor/CommonSensors.h +++ b/examples/smart-sensor/CommonSensors.h @@ -5,6 +5,8 @@ #ifndef HOMEGENIE_MINI_COMMONSENSORS_H #define HOMEGENIE_MINI_COMMONSENSORS_H +#define COLOR_LIGHT_ADDRESS "C1" + #include "HomeGenie.h" #include "io/sensors/DS18B20.h" @@ -18,13 +20,13 @@ using namespace IO::Sensors; #ifndef DISABLE_AUTOMATION void setupMotionSensorSchedules(Module* sensorModule) { - // Create default Dawn scene + // Create "Motion.Activated" schedule if (Scheduler::get("Motion.Activated") == nullptr) { auto s = new Schedule("Motion.Activated", "Turn on light if motion is detected and luminosity is low.", "", "", "$$.boundModules.on();"); s->onModuleEvent = true; s->eventModules.add(sensorModule->getReference()); // UI state data - s->data = R"({"action":{"template":{"forEach":{"config":{},"enabled":true,"id":"command_turn_on"},"forEnd":{"config":{},"enabled":false,"id":null},"forStart":{"config":{},"enabled":false,"id":null}},"type":"template"},"event":[{"condition":">","module":"34b7da5-2220-6e69-654d-696e69dab734/HomeAutomation.HomeGenie/mini","property":"Sensor.MotionDetect","value":"0"},{"condition":"<","module":"34b7da5-2220-6e69-654d-696e69dab734/HomeAutomation.HomeGenie/mini","property":"Sensor.Luminance","value":"70"}],"from":"","itemType":1,"occur_dayom_sel":[],"occur_dayom_type":1,"occur_dayow_sel":[],"occur_hour_sel":[],"occur_hour_step":12,"occur_hour_type":1,"occur_min_sel":[],"occur_min_step":30,"occur_min_type":1,"occur_month_sel":[],"occur_month_type":1,"time":[],"to":""})"; + s->data = R"({"action":{"template":{"forEach":{"config":{},"enabled":true,"id":"command_turn_on"},"forEnd":{"config":{},"enabled":false,"id":null},"forStart":{"config":{},"enabled":false,"id":null}},"type":"template"},"event":[{"condition":">","module":"HomeAutomation.HomeGenie/mini","property":"Sensor.MotionDetect","value":"0"},{"condition":"<","module":"HomeAutomation.HomeGenie/mini","property":"Sensor.Luminance","value":"70"}],"from":"","itemType":1,"occur_dayom_sel":[],"occur_dayom_type":1,"occur_dayow_sel":[],"occur_hour_sel":[],"occur_hour_step":12,"occur_hour_type":1,"occur_min_sel":[],"occur_min_step":30,"occur_min_type":1,"occur_month_sel":[],"occur_month_type":1,"time":[],"to":""})"; // Device types allowed s->boundDevices.add(new String("Switch")); s->boundDevices.add(new String("Light")); @@ -33,18 +35,39 @@ void setupMotionSensorSchedules(Module* sensorModule) { Scheduler::addSchedule(s); } - // Create default Dusk scene + // Create "Motion.Timeout" schedule if (Scheduler::get("Motion.Timeout") == nullptr) { auto s = new Schedule("Motion.Timeout", "Turn off light if no motion is detected for 5 minutes.", "", "", "$$.boundModules.off();"); s->onModuleEvent = true; s->eventModules.add(sensorModule->getReference()); // UI state data - s->data = R"({"action":{"template":{"forEach":{"config":{},"enabled":true,"id":"command_turn_off"},"forEnd":{"config":{},"enabled":false,"id":null},"forStart":{"config":{},"enabled":false,"id":null}},"type":"template"},"event":[{"condition":">=","module":"34b7da5-2220-6e69-654d-696e69dab734/HomeAutomation.HomeGenie/mini","property":"Status.IdleTime","value":"5"},{"condition":">","module":"34b7da5-2220-6e69-654d-696e69dab734/HomeAutomation.HomeGenie/mini","property":"Sensor.Luminance","value":"50"}],"from":"","itemType":1,"occur_dayom_sel":[],"occur_dayom_type":1,"occur_dayow_sel":[],"occur_hour_sel":[],"occur_hour_step":12,"occur_hour_type":1,"occur_min_sel":[],"occur_min_step":30,"occur_min_type":1,"occur_month_sel":[],"occur_month_type":1,"time":[],"to":""})"; + s->data = R"({"action":{"template":{"forEach":{"config":{},"enabled":true,"id":"command_turn_off"},"forEnd":{"config":{},"enabled":false,"id":null},"forStart":{"config":{},"enabled":false,"id":null}},"type":"template"},"event":[{"condition":">=","module":"HomeAutomation.HomeGenie/mini","property":"Status.IdleTime","value":"5"},{"condition":">","module":"HomeAutomation.HomeGenie/mini","property":"Sensor.Luminance","value":"50"}],"from":"","itemType":1,"occur_dayom_sel":[],"occur_dayom_type":1,"occur_dayow_sel":[],"occur_hour_sel":[],"occur_hour_step":12,"occur_hour_type":1,"occur_min_sel":[],"occur_min_step":30,"occur_min_type":1,"occur_month_sel":[],"occur_month_type":1,"time":[],"to":""})"; + // Device types allowed + s->boundDevices.add(new String("Switch")); + s->boundDevices.add(new String("Light")); + s->boundDevices.add(new String("Dimmer")); + s->boundDevices.add(new String("Color")); + Scheduler::addSchedule(s); + } + + // Create "Motion.Blink" schedule + if (Scheduler::get("Motion.Blink") == nullptr) { + auto s = new Schedule("Motion.Blink", "Blink light when motion is detected.", "", "", "$$.boundModules.toggle();"); + s->onModuleEvent = true; + s->eventModules.add(sensorModule->getReference()); + // UI state data + s->data = R"({"action":{"template":{"forEach":{"config":{},"enabled":true,"id":"command_toggle"},"forEnd":{"config":{},"enabled":false,"id":null},"forStart":{"config":{},"enabled":false,"id":null}},"type":"template"},"event":[{"condition":">=","module":"HomeAutomation.HomeGenie/mini","property":"Sensor.MotionDetect","value":"0"}],"from":"","itemType":1,"occur_dayom_sel":[],"occur_dayom_type":1,"occur_dayow_sel":[],"occur_hour_sel":[],"occur_hour_step":12,"occur_hour_type":1,"occur_min_sel":[],"occur_min_step":30,"occur_min_type":1,"occur_month_sel":[],"occur_month_type":1,"time":[],"to":""})"; // Device types allowed s->boundDevices.add(new String("Switch")); s->boundDevices.add(new String("Light")); s->boundDevices.add(new String("Dimmer")); s->boundDevices.add(new String("Color")); + // Custom status led (builtin NeoPixel RGB LED) + auto pin = Config::getSetting("stld-pin"); + int statusLedPin = pin.isEmpty() ? -1 : pin.toInt(); + if (statusLedPin >= 0) { + s->boundModules.add(new ModuleReference(IO::IOEventDomains::HomeAutomation_HomeGenie, COLOR_LIGHT_ADDRESS)); + } Scheduler::addSchedule(s); } diff --git a/examples/smart-sensor/smart-sensor.cpp b/examples/smart-sensor/smart-sensor.cpp index 861e9c5..3f370f9 100644 --- a/examples/smart-sensor/smart-sensor.cpp +++ b/examples/smart-sensor/smart-sensor.cpp @@ -27,7 +27,6 @@ * */ - #include #include @@ -50,8 +49,6 @@ void setup() { miniModule->setProperty("Widget.Implements.Scheduling", "1"); miniModule->setProperty("Widget.Implements.Scheduling.ModuleEvents", "1"); - includeCommonSensors(homeGenie, miniModule); - // Get status LED config auto pin = Config::getSetting("stld-pin"); int statusLedPin = pin.isEmpty() ? -1 : pin.toInt(); @@ -67,7 +64,7 @@ void setup() { if (statusLED != nullptr) { // Setup main LEDs control module - auto colorLight = new ColorLight(IO::IOEventDomains::HomeAutomation_HomeGenie, "C1", "Color Light"); + auto colorLight = new ColorLight(IO::IOEventDomains::HomeAutomation_HomeGenie, COLOR_LIGHT_ADDRESS, "Status LED"); colorLight->module->setProperty("Widget.Implements.Scheduling", "1"); colorLight->module->setProperty("Widget.Implements.Scheduling.ModuleEvents", "1"); colorLight->module->setProperty("Widget.Preference.AudioLight", "true"); @@ -79,6 +76,8 @@ void setup() { } + includeCommonSensors(homeGenie, miniModule); + homeGenie->begin(); } diff --git a/src/automation/Schedule.h b/src/automation/Schedule.h index 0f99f1a..db7af30 100644 --- a/src/automation/Schedule.h +++ b/src/automation/Schedule.h @@ -79,6 +79,9 @@ namespace Automation { for (auto &&bm: boundModules) { delete bm; } + for (auto &&em: eventModules) { + delete em; + } } bool occursUpdate(time_t ts) { diff --git a/src/automation/ScheduledScript.cpp b/src/automation/ScheduledScript.cpp index bc86d75..016ac37 100644 --- a/src/automation/ScheduledScript.cpp +++ b/src/automation/ScheduledScript.cpp @@ -92,6 +92,12 @@ namespace Automation { " get isOff() {\n" " return parseFloat(__boundModules_property_avg('Status.Level')) === 0;\n" " },\n" + " get alarmed() {" + " return parseInt(__boundModules_property_avg('Sensor.Alarm')) > 0;" + " }," + " get motionDetected() {" + " return parseInt(__boundModules_property_avg('Sensor.MotionDetect')) > 0;" + " }," " get temperature() {\n" " return parseFloat(__boundModules_property_avg('Sensor.Temperature'));\n" " },\n" diff --git a/src/automation/helpers/NetHelper.cpp b/src/automation/helpers/NetHelper.cpp index feca593..77ad990 100644 --- a/src/automation/helpers/NetHelper.cpp +++ b/src/automation/helpers/NetHelper.cpp @@ -33,21 +33,32 @@ namespace Automation { namespace Helpers { HTTPClient NetHelper::http; String NetHelper::httpGet(String &url) { + WiFiClient* client; + if (url.startsWith("https://")) { + client = new WiFiClientSecure(); + ((WiFiClientSecure*)client)->setInsecure(); + } else { + client = new WiFiClient(); + } String response; - http.begin(url.c_str()); - //http.addHeader("Content-Type", "application/x-www-form-urlencoded"); - int httpCode = http.GET(); - if (httpCode > 0) { - // Server response - if (httpCode == HTTP_CODE_OK) { - response = http.getString(); + if (http.begin(*client, url)) { + //http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + int httpCode = http.GET(); + if (httpCode > 0) { + // Server response + if (httpCode == HTTP_CODE_OK) { + response = http.getString(); + } else { + // Server reported error code + //Serial.printf("[HTTP] GET... code: %d\n", httpCode); + } } else { - // Server reported error code - //Serial.printf("[HTTP] GET... code: %d\n", httpCode); + Serial.printf("[HTTP] GET... failed, error: %s\n", HTTPClient::errorToString(httpCode).c_str()); } } else { - //Serial.printf("[HTTP] GET... failed, error: %s\n", HTTPClient::errorToString(httpCode).c_str()); + // TODO: ... } + delete client; return response; } diff --git a/src/automation/helpers/NetHelper.h b/src/automation/helpers/NetHelper.h index c887cc1..c200be0 100644 --- a/src/automation/helpers/NetHelper.h +++ b/src/automation/helpers/NetHelper.h @@ -30,6 +30,8 @@ #ifndef HOMEGENIE_MINI_NETHELPER_H #define HOMEGENIE_MINI_NETHELPER_H +#include + #ifdef ESP8266 #include #else @@ -41,12 +43,13 @@ namespace Automation { namespace Helpers { - class NetHelper{ + class NetHelper { public: static String httpGet(String& url); static bool ping(String& host); private: + static WiFiClientSecure* wifiClientSecure; static HTTPClient http; }; diff --git a/src/net/HTTPServer.cpp b/src/net/HTTPServer.cpp index 7dc872e..ecb6414 100644 --- a/src/net/HTTPServer.cpp +++ b/src/net/HTTPServer.cpp @@ -89,11 +89,11 @@ namespace Net { if (!ipAddress.equals(localIP) && !localIP.equals("0.0.0.0") && !localIP.isEmpty()) { // New IP address ipAddress = localIP; - //Logger::info("| ✔ New IP address %s", ipAddress.c_str()); + Logger::info("| ✔ New IP address: %s", ipAddress.c_str()); // SSDP UDN uuid Config::system.id = SSDPDevice.getId(); - //Logger::info("| ✔ SSDP UDN uuid: ", Config::system.id.c_str()); + Logger::info("| ✔ SSDP UDN uuid: %s", Config::system.id.c_str()); String ssdpUri = Config::system.id + String(".xml"); SSDPDevice.setSchemaURL(FPSTR(ssdpUri.c_str())); diff --git a/src/net/WiFiManager.cpp b/src/net/WiFiManager.cpp index 0349700..a194be5 100644 --- a/src/net/WiFiManager.cpp +++ b/src/net/WiFiManager.cpp @@ -31,6 +31,7 @@ namespace Net { wl_status_t WiFiManager::wiFiStatus = WL_NO_SSID_AVAIL; + unsigned long WiFiManager::wiFiIdleTimestamp = 0; #ifdef ESP32 esp_wps_config_t WiFiManager::wps_config; bool WiFiManager::esp32_wps_started = false; @@ -79,7 +80,19 @@ namespace Net { } checkWiFiStatus(); - wiFiStatus = status; + wiFiStatus = ESP_WIFI_STATUS; + if (wiFiStatus == WL_DISCONNECTED) { + wiFiStatus = WL_NO_SSID_AVAIL; + } + } else if (wiFiStatus == WL_IDLE_STATUS) { + if (wiFiIdleTimestamp == 0) { + wiFiIdleTimestamp = millis(); + } else if (millis() - wiFiIdleTimestamp > 15000) { + // force reconnect + wiFiStatus = WL_NO_SSID_AVAIL; + wiFiIdleTimestamp = 0; + connect(); + } } } diff --git a/src/net/WiFiManager.h b/src/net/WiFiManager.h index 5509d06..69e66bc 100644 --- a/src/net/WiFiManager.h +++ b/src/net/WiFiManager.h @@ -82,7 +82,7 @@ namespace Net { #endif private: static wl_status_t wiFiStatus; - static unsigned long lastStatusCheckTs; + static unsigned long wiFiIdleTimestamp; #ifdef ESP32 static unsigned long esp32_wps_waiting_connect_ts; #endif diff --git a/src/service/EventRouter.cpp b/src/service/EventRouter.cpp index db18ddf..7b5c166 100644 --- a/src/service/EventRouter.cpp +++ b/src/service/EventRouter.cpp @@ -81,11 +81,10 @@ namespace Service { } // Only supports events coming from this very system - // TODO: this might not work if MAC address changes -// if (!serverId.isEmpty() && !serverId.equals(Config::system.id)) { -// matchesConditionsProperty = false; -// break; -// } + if (!serverId.isEmpty() && !serverId.equals(Config::system.id)) { + matchesConditionsProperty = false; + break; + } auto property = c["property"].as(); if (strcmp(domain, moduleDomain.c_str()) == 0 && diff --git a/src/service/api/HomeGenieHandler.cpp b/src/service/api/HomeGenieHandler.cpp index 6eb1dc1..11c275d 100644 --- a/src/service/api/HomeGenieHandler.cpp +++ b/src/service/api/HomeGenieHandler.cpp @@ -377,8 +377,12 @@ namespace Service { namespace API { return true; } else { Config::system.friendlyName = deviceName; - Config::saveSetting(CONFIG_KEY_device_name, deviceName); miniModule->name = deviceName; + // persist setting + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, false); + preferences.putString(CONFIG_KEY_device_name, deviceName); + preferences.end(); } } if (doc.containsKey("group")) {