diff --git a/.github/workflows/build-esp8266-esp32.yml b/.github/workflows/build-esp8266-esp32.yml index b92a9d9..59aa68d 100644 --- a/.github/workflows/build-esp8266-esp32.yml +++ b/.github/workflows/build-esp8266-esp32.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - example: [examples/ACUnit, examples/AirQualitySensor/AirQualitySensor, examples/Blinds, examples/ContactSensor, examples/DimSwitch, examples/doorbell, examples/Fan, examples/GarageDoor, examples/Light/Light, examples/Lock/Lock, examples/Lock/Lock_with_feedback, examples/MotionSensor, examples/PowerSensor, examples/Relay/MultiRelays_advance, examples/Relay/Relay, examples/Speaker, examples/Switch/MultiSwitch_advance, examples/Switch/MultiSwitch_beginner, examples/Switch/MultiSwitch_intermediate, examples/Switch/Switch, examples/Thermostat, examples/TV] + example: [examples/ACUnit, examples/AirQualitySensor/AirQualitySensor, examples/Blinds, examples/ContactSensor, examples/DimSwitch, examples/doorbell, examples/Fan, examples/GarageDoor, examples/Light/Light, examples/Lock/Lock, examples/Lock/Lock_with_feedback, examples/MotionSensor, examples/PowerSensor, examples/Relay/MultiRelays_advance, examples/Relay/Relay, examples/Speaker, examples/Switch/MultiSwitch_advance, examples/Switch/MultiSwitch_beginner, examples/Switch/MultiSwitch_intermediate, examples/Switch/Switch, examples/Thermostat, examples/TV, examples/OTAUpdate, examples/Health] steps: diff --git a/.github/workflows/build-rpipicow.yml b/.github/workflows/build-rpipicow.yml index e02d2a4..db03a8d 100644 --- a/.github/workflows/build-rpipicow.yml +++ b/.github/workflows/build-rpipicow.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - example: [examples/ACUnit, examples/AirQualitySensor/AirQualitySensor, examples/Blinds, examples/ContactSensor, examples/DimSwitch, examples/doorbell, examples/Fan, examples/GarageDoor, examples/Light/Light, examples/Lock/Lock, examples/Lock/Lock_with_feedback, examples/MotionSensor, examples/PowerSensor, examples/Relay/MultiRelays_advance, examples/Relay/Relay, examples/Speaker, examples/Switch/MultiSwitch_advance, examples/Switch/MultiSwitch_beginner, examples/Switch/MultiSwitch_intermediate, examples/Switch/Switch, examples/Thermostat, examples/TV] + example: [examples/ACUnit, examples/AirQualitySensor/AirQualitySensor, examples/Blinds, examples/ContactSensor, examples/DimSwitch, examples/doorbell, examples/Fan, examples/GarageDoor, examples/Light/Light, examples/Lock/Lock, examples/Lock/Lock_with_feedback, examples/MotionSensor, examples/PowerSensor, examples/Relay/MultiRelays_advance, examples/Relay/Relay, examples/Speaker, examples/Switch/MultiSwitch_advance, examples/Switch/MultiSwitch_beginner, examples/Switch/MultiSwitch_intermediate, examples/Switch/Switch, examples/Thermostat, examples/TV, examples/Health] steps: - name: Step 1 - Checkout Repo diff --git a/changelog.md b/changelog.md index 5f6b3c4..97b863e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,9 @@ # Changelog +## Version 3.2.0 +### New + - Support OTA Updates + - Module level command support for WiFi settings, ESP Health + ## Version 3.1.0 Upgrade: - Upgrade to ArduinoJson 7 diff --git a/examples/Health/Health.ino b/examples/Health/Health.ino new file mode 100644 index 0000000..0dd9ac3 --- /dev/null +++ b/examples/Health/Health.ino @@ -0,0 +1,98 @@ +/* + * Example for how to monitor ESP health. + * + * If you encounter any issues: + * - check the readme.md at https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md + * - ensure all dependent libraries are installed + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#arduinoide + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#dependencies + * - open serial monitor and check whats happening + * - check full user documentation at https://sinricpro.github.io/esp8266-esp32-sdk + * - visit https://github.com/sinricpro/esp8266-esp32-sdk/issues and check for existing issues or open a new one + */ + +// Uncomment the following line to enable serial debug output +#define ENABLE_DEBUG + +#ifdef ENABLE_DEBUG + #define DEBUG_ESP_PORT Serial + #define NODEBUG_WEBSOCKETS + #define NDEBUG +#endif + +#include +#include + +#if defined(ESP8266) + #include +#elif defined(ESP32) + #include +#endif + +#include "SinricPro.h" +#include "SinricProSwitch.h" +#include "HealthDiagnostics.h" + +#define WIFI_SSID "YOUR-WIFI-SSID" +#define WIFI_PASS "YOUR-WIFI-PASSWORD" +#define APP_KEY "YOUR-APP-KEY" // Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx" +#define APP_SECRET "YOUR-APP-SECRET" // Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx" +#define SWITCH_ID "YOUR-DEVICE-ID" // Should look like "5dc1564130xxxxxxxxxxxxxx" + +#define BAUD_RATE 115200 // Change baudrate to your need + +HealthDiagnostics healthDiagnostics; + +// setup function for WiFi connection +void setupWiFi() { + Serial.printf("\r\n[Wifi]: Connecting"); + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); + WiFi.setAutoReconnect(true); +#elif defined(ESP32) + WiFi.setSleep(false); + WiFi.setAutoReconnect(true); +#endif + + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while (WiFi.status() != WL_CONNECTED) { + Serial.printf("."); + delay(250); + } + + Serial.printf("connected!\r\n[WiFi]: IP-Address is %s\r\n", WiFi.localIP().toString().c_str()); +} + +// setup function for SinricPro +void setupSinricPro() { + SinricProSwitch& mySwitch = SinricPro[SWITCH_ID]; + + // setup SinricPro + SinricPro.onConnected([]() { + Serial.printf("Connected to SinricPro\r\n"); + }); + + SinricPro.onDisconnected([]() { + Serial.printf("Disconnected from SinricPro\r\n"); + }); + + SinricPro.onReportHealth([&](String &healthReport) { + return healthDiagnostics.reportHealth(healthReport); + }); + + SinricPro.begin(APP_KEY, APP_SECRET); +} + +// main setup function +void setup() { + Serial.begin(BAUD_RATE); + Serial.printf("\r\n\r\n"); + setupWiFi(); + setupSinricPro(); +} + +void loop() { + SinricPro.handle(); +} diff --git a/examples/Health/HealthDiagnostics.cpp b/examples/Health/HealthDiagnostics.cpp new file mode 100644 index 0000000..c8ac06c --- /dev/null +++ b/examples/Health/HealthDiagnostics.cpp @@ -0,0 +1,137 @@ +#include "HealthDiagnostics.h" + +String HealthDiagnostics::getChipId() { +#if defined(ESP32) + return String((uint32_t)ESP.getEfuseMac(), HEX); +#elif defined(ESP8266) + return String(ESP.getChipId(), HEX); +#elif defined(ARDUINO_ARCH_RP2040) + return String(rp2040.getChipID(), HEX); +#endif +} + +void HealthDiagnostics::addHeapInfo(JsonObject& doc) { +#if defined(ESP32) + doc["freeHeap"] = ESP.getFreeHeap(); + doc["totalHeap"] = ESP.getHeapSize(); + doc["minFreeHeap"] = ESP.getMinFreeHeap(); + doc["maxAllocHeap"] = ESP.getMaxAllocHeap(); + + multi_heap_info_t heap_info; + heap_caps_get_info(&heap_info, MALLOC_CAP_INTERNAL); + + JsonObject internalHeap = doc["internalHeap"].to(); + internalHeap["totalFreeBytes"] = heap_info.total_free_bytes; + internalHeap["totalAllocatedBytes"] = heap_info.total_allocated_bytes; + internalHeap["largestFreeBlock"] = heap_info.largest_free_block; + internalHeap["minimumFreeBytes"] = heap_info.minimum_free_bytes; + internalHeap["allocatedBlocks"] = heap_info.allocated_blocks; + internalHeap["freeBlocks"] = heap_info.free_blocks; + internalHeap["totalBlocks"] = heap_info.total_blocks; + + heap_caps_get_info(&heap_info, MALLOC_CAP_SPIRAM); + + JsonObject psram = doc["psram"].to(); + psram["totalFreeBytes"] = heap_info.total_free_bytes; + psram["totalAllocatedBytes"] = heap_info.total_allocated_bytes; + psram["largestFreeBlock"] = heap_info.largest_free_block; + psram["minimumFreeBytes"] = heap_info.minimum_free_bytes; + psram["allocatedBlocks"] = heap_info.allocated_blocks; + psram["freeBlocks"] = heap_info.free_blocks; + psram["totalBlocks"] = heap_info.total_blocks; + +#elif defined(ESP8266) + doc["freeHeap"] = ESP.getFreeHeap(); + doc["heapFragmentation"] = ESP.getHeapFragmentation(); + doc["maxFreeBlockSize"] = ESP.getMaxFreeBlockSize(); + + // Get detailed heap information. + JsonObject heapInfo = doc["heapInfo"].to(); + UMM_HEAP_INFO ummHeapInfo; + umm_info(&ummHeapInfo, 0); + heapInfo["freeBlocks"] = ummHeapInfo.freeBlocks; + heapInfo["usedBlocks"] = ummHeapInfo.usedBlocks; + heapInfo["totalBlocks"] = ummHeapInfo.totalBlocks; + heapInfo["totalEntries"] = ummHeapInfo.totalEntries; + heapInfo["usedEntries"] = ummHeapInfo.usedEntries; + heapInfo["freeEntries"] = ummHeapInfo.freeEntries; + heapInfo["maxFreeContiguousBytes"] = umm_max_block_size(); + +#elif defined(ARDUINO_ARCH_RP2040) + doc["freeHeap"] = rp2040.getFreeHeap(); + doc["totalHeap"] = rp2040.getTotalHeap(); +#endif +} + +void HealthDiagnostics::addWiFiInfo(JsonObject& doc) { + doc["ssid"] = WiFi.SSID(); + +#if defined(ESP32) || defined(ESP8266) + doc["bssid"] = WiFi.BSSIDstr(); +#endif + + doc["rssi"] = WiFi.RSSI(); + doc["ipAddress"] = WiFi.localIP().toString(); + doc["subnetMask"] = WiFi.subnetMask().toString(); + doc["gateway"] = WiFi.gatewayIP().toString(); + doc["dns"] = WiFi.dnsIP().toString(); + doc["macAddress"] = WiFi.macAddress(); + doc["channel"] = WiFi.channel(); +} + +void HealthDiagnostics::addSketchInfo(JsonObject& doc) { +#if defined(ESP32) || defined(ESP8266) + doc["cpuFreq"] = ESP.getCpuFreqMHz(); + doc["sketchSize"] = ESP.getSketchSize(); + doc["freeSketchSpace"] = ESP.getFreeSketchSpace(); + doc["flashChipSize"] = ESP.getFlashChipSize(); + doc["flashChipSpeed"] = ESP.getFlashChipSpeed(); +#endif +} + +void HealthDiagnostics::addResetCause(JsonObject& doc) { +#if defined(ESP32) + switch (esp_reset_reason()) { + case ESP_RST_POWERON: doc["reason"] = "Power-on event"; break; + case ESP_RST_EXT: doc["reason"] = "External pin reset"; break; + case ESP_RST_SW: doc["reason"] = "Software reset via esp_restart"; break; + case ESP_RST_PANIC: doc["reason"] = "Software reset due to exception/panic"; break; + case ESP_RST_INT_WDT: doc["reason"] = "Reset (software or hardware) due to interrupt watchdog"; break; + case ESP_RST_TASK_WDT: doc["reason"] = "Reset due to task watchdog"; break; + case ESP_RST_WDT: doc["reason"] = "Reset due to other watchdogs"; break; + case ESP_RST_DEEPSLEEP: doc["reason"] = "Deep sleep reset"; break; + case ESP_RST_BROWNOUT: doc["reason"] = "Brownout reset"; break; + case ESP_RST_SDIO: doc["reason"] = "Reset over SDIO"; break; + default: doc["reason"] = "Unknown reset reason"; break; + } +#elif defined(ESP8266) + doc["reason"] = ESP.getResetReason(); +#endif + + //doc["crashDetails"] = "" // Use something like https://github.com/krzychb/EspSaveCrash +} + +bool HealthDiagnostics::reportHealth(String& healthReport) { + JsonDocument doc; + doc["chipId"] = getChipId(); + doc["uptime"] = millis() / 1000; // seconds + + // Add detailed heap information. + JsonObject heap = doc["heap"].to(); + addHeapInfo(heap); + + // Detailed Sketch information. + JsonObject sketch = doc["sketch"].to(); + addSketchInfo(sketch); + + // Detailed WiFi information. + JsonObject wifi = doc["wifi"].to(); + addWiFiInfo(wifi); + + // Add last reset reson + JsonObject resetInfo = doc["reset"].to(); + addResetCause(resetInfo); + + serializeJson(doc, healthReport); + return true; +} \ No newline at end of file diff --git a/examples/Health/HealthDiagnostics.h b/examples/Health/HealthDiagnostics.h new file mode 100644 index 0000000..f64524b --- /dev/null +++ b/examples/Health/HealthDiagnostics.h @@ -0,0 +1,44 @@ +#ifndef HEALTH_DIAGNOSTICS_H +#define HEALTH_DIAGNOSTICS_H + +#include +#include + +#if defined(ESP32) +#include +#include "esp_system.h" +#include +#include +#elif defined(ESP8266) +#include +#include +extern "C" { +#include "umm_malloc/umm_heap_select.h" +#include "umm_malloc/umm_malloc.h" +} +#elif defined(ARDUINO_ARCH_RP2040) +#include +#endif + +/** + * @brief Class to handle health diagnostics + */ +class HealthDiagnostics { +public: + /** + * @brief Report the health diagnostic information. + * + * @param healthReport A reference to a String to store the health report in JSON format. + * @return True on success, otherwise false. + */ + bool reportHealth(String& healthReport); + +private: + String getChipId(); + void addHeapInfo(JsonObject& doc); + void addWiFiInfo(JsonObject& doc); + void addSketchInfo(JsonObject& doc); + void addResetCause(JsonObject& doc); +}; + +#endif // HEALTH_DIAGNOSTICS_H \ No newline at end of file diff --git a/examples/OTAUpdate/Cert.cpp b/examples/OTAUpdate/Cert.cpp new file mode 100644 index 0000000..58084f9 --- /dev/null +++ b/examples/OTAUpdate/Cert.cpp @@ -0,0 +1,33 @@ +#include "Cert.h" + +// Ref: https://projects.petrucci.ch/esp32/?page=ssl.php&url=otaupdates.sinric.pro + + +const char* rootCACertificate = R"EOF( +-----BEGIN CERTIFICATE----- +MIIEVzCCAj+gAwIBAgIRAIOPbGPOsTmMYgZigxXJ/d4wDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCRTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNCzqK +a2GOtu/cX1jnxkJFVKtj9mZhSAouWXW0gQI3ULc/FnncmOyhKJdyIBwsz9V8UiBO +VHhbhBRrwJCuhezAUUE8Wod/Bk3U/mDR+mwt4X2VEIiiCFQPmRpM5uoKrNijgfgw +gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfK1/PPCFPnQS37SssxMZw +i9LXDTAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB +AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g +BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu +Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAH3KdNEVCQdqk0LKyuNImTKdRJY1C +2uw2SJajuhqkyGPY8C+zzsufZ+mgnhnq1A2KVQOSykOEnUbx1cy637rBAihx97r+ +bcwbZM6sTDIaEriR/PLk6LKs9Be0uoVxgOKDcpG9svD33J+G9Lcfv1K9luDmSTgG +6XNFIN5vfI5gs/lMPyojEMdIzK9blcl2/1vKxO8WGCcjvsQ1nJ/Pwt8LQZBfOFyV +XP8ubAp/au3dc4EKWG9MO5zcx1qT9+NXRGdVWxGvmBFRAajciMfXME1ZuGmk3/GO +koAM7ZkjZmleyokP1LGzmfJcUd9s7eeu1/9/eg5XlXd/55GtYjAM+C4DG5i7eaNq +cm2F+yxYIPt6cbbtYVNJCGfHWqHEQ4FYStUyFnv8sjyqU8ypgZaNJ9aVcWSICLOI +E1/Qv/7oKsnZCWJ926wU6RqG1OYPGOi1zuABhLw61cuPVDT28nQS/e6z95cJXq0e +K1BcaJ6fJZsmbjRgD5p3mvEf5vdQM7MCEvU0tHbsx2I5mHHJoABHb8KVBgWp/lcX +GWiWaeOyB7RP+OfDtvi2OsapxXiV7vNVs7fMlrRjY1joKaqmmycnBvAq14AEbtyL +sVfOS66B8apkeFX2NY4XPEYV4ZSCe8VHPrdrERk2wILG3T/EGmSIkCYVUMSnjmJd +VQD9F6Na/+zmXCc= +-----END CERTIFICATE----- +)EOF"; \ No newline at end of file diff --git a/examples/OTAUpdate/Cert.h b/examples/OTAUpdate/Cert.h new file mode 100644 index 0000000..56b76eb --- /dev/null +++ b/examples/OTAUpdate/Cert.h @@ -0,0 +1,3 @@ +#pragma once + +extern const char* rootCACertificate; \ No newline at end of file diff --git a/examples/OTAUpdate/ESP32OTAHelper.h b/examples/OTAUpdate/ESP32OTAHelper.h new file mode 100644 index 0000000..43f59b4 --- /dev/null +++ b/examples/OTAUpdate/ESP32OTAHelper.h @@ -0,0 +1,48 @@ +#if defined(ESP32) + +#include +#include +#include +#include +#include "Cert.h" + +String startOtaUpdate(const String &url) { + WiFiClientSecure client; + client.setCACert(rootCACertificate); + + HTTPClient https; + Serial.print("[startOtaUpdate()] begin...\n"); + if (!https.begin(client, url)) return "Unable to connect"; + + Serial.print("[startOtaUpdate()] GET...\n"); + // start connection and send HTTP header + int httpCode = https.GET(); + if (httpCode < 0) return "GET... failed, error: " + https.errorToString(httpCode); + if (httpCode != HTTP_CODE_OK || httpCode != HTTP_CODE_MOVED_PERMANENTLY) return "HTTP response code: " + String(httpCode); + + int contentLength = https.getSize(); + Serial.printf("Content-Length: %d\n", contentLength); + + if (contentLength == 0) return "There was no content length in the response"; + + bool canBegin = Update.begin(contentLength); + + if (!canBegin) return "Not enough space to begin OTA"; + + WiFiClient *stream = https.getStreamPtr(); + size_t written = Update.writeStream(*stream); + + if (written != contentLength) return "Written only : " + String(written) + "/" + String(contentLength) + ". Retry?"; + Serial.println("[startOtaUpdate()] Written : " + String(written) + " successfully"); + + if (!Update.end()) return "Error Occurred. Error #: " + String(Update.getError()); + Serial.println("[startOtaUpdate()] OTA done!"); + + if (!Update.isFinished()) return "Update not finished? Something went wrong!"; + Serial.println("[startOtaUpdate()] Update successfully completed. Rebooting."); + ESP.restart(); + + return ""; +} + +#endif \ No newline at end of file diff --git a/examples/OTAUpdate/ESP8266OTAHelper.h b/examples/OTAUpdate/ESP8266OTAHelper.h new file mode 100644 index 0000000..0cb223e --- /dev/null +++ b/examples/OTAUpdate/ESP8266OTAHelper.h @@ -0,0 +1,85 @@ +#if defined(ESP8266) || defined(ARDUINO_ARCH_RP2040) + +#include +#include +#include "Cert.h" + +#if defined(ARDUINO_ARCH_RP2040) + #include + #include + #include + + #define OTA_CLASS httpUpdate + +#elif defined(ESP8266) + #include + #include + #include "ESP8266httpUpdate.h" + + #define OTA_CLASS ESPhttpUpdate +#endif + +String extractOTAHostname(const String& url) { + int index = url.indexOf("//") + 2; + if (index < 0) { + return ""; // Handle invalid URL format + } + + int endIndex = url.indexOf("/", index); + if (endIndex < 0) { + endIndex = url.length(); + } + + return url.substring(index, endIndex); +} + +// Function to perform the OTA update +String startOtaUpdate(const String& url) { + #if defined(ARDUINO_ARCH_RP2040) + WiFiClientSecure client; + client.setBufferSizes(4096, 4096); // For OTA to work on limited RAM + #elif defined(ESP8266) + BearSSL::WiFiClientSecure client; + // Use MFLN to reduce the memory usage + String host = extractOTAHostname(url); + bool mfln = client.probeMaxFragmentLength(host, 443, 512); + Serial.printf("[startOtaUpdate()] MFLN supported: %s\n", mfln ? "yes" : "no"); + if (mfln) { client.setBufferSizes(512, 512); } else client.setBufferSizes(4096, 4096); + #endif + + client.setInsecure(); + + // The line below is optional. It can be used to blink the LED on the board during flashing + // The LED will be on during download of one buffer of data from the network. The LED will + // be off during writing that buffer to flash + // On a good connection the LED should flash regularly. On a bad connection the LED will be + // on much longer than it will be off. Other pins than LED_BUILTIN may be used. The second + // value is used to put the LED on. If the LED is on with HIGH, that value should be passed + //ESPhttpUpdate.setLedPin(LED_BUILTIN, LOW); + + Serial.printf("[startOtaUpdate()] Starting the OTA update. This may take few mins to complete!\n"); + auto http_ret = OTA_CLASS.update(client, url); + + //if success reboot will reboot! + String errorMsg = ""; + + switch (http_ret) { + case HTTP_UPDATE_OK: + Serial.printf("[startOtaUpdate()] HTTP_UPDATE_OK\n"); + break; + + case HTTP_UPDATE_FAILED: + errorMsg = String("HTTP_UPDATE_FAILED Error (") + OTA_CLASS.getLastError() + "): " + OTA_CLASS.getLastErrorString(); + Serial.printf("%s\n", errorMsg.c_str()); + break; + + case HTTP_UPDATE_NO_UPDATES: + errorMsg = "HTTP_UPDATE_NO_UPDATES"; + Serial.println(errorMsg); + break; + } + + return errorMsg; +} + +#endif \ No newline at end of file diff --git a/examples/OTAUpdate/OTAUpdate.ino b/examples/OTAUpdate/OTAUpdate.ino new file mode 100644 index 0000000..1d3af1e --- /dev/null +++ b/examples/OTAUpdate/OTAUpdate.ino @@ -0,0 +1,135 @@ +/* + * Example for how to use SinricPro OTA Service: + * + * If you encounter any issues: + * - check the readme.md at https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md + * - ensure all dependent libraries are installed + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#arduinoide + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#dependencies + * - open serial monitor and check whats happening + * - check full user documentation at https://sinricpro.github.io/esp8266-esp32-sdk + * - visit https://github.com/sinricpro/esp8266-esp32-sdk/issues and check for existing issues or open a new one + */ + +// Uncomment the following line to enable serial debug output +// #define SINRICPRO_NOSSL // Uncomment if you have memory limitation issues. +// #define ENABLE_DEBUG + +// Your firmware version. Must be above SinricPro.h. Do not rename this. +#define FIRMWARE_VERSION "1.1.1" + +// Sketch -> Export Compiled Binary to export + +#ifdef ENABLE_DEBUG + #define DEBUG_ESP_PORT Serial + #define NODEBUG_WEBSOCKETS + #define NDEBUG +#endif + +#include + +#if defined(ESP8266) + #include + #include "ESP8266OTAHelper.h" +#elif defined(ESP32) + #include + #include "ESP32OTAHelper.h" +#elif defined(ARDUINO_ARCH_RP2040) + #include + #include "ESP8266OTAHelper.h" +#endif + +#include "SemVer.h" +#include "SinricPro.h" +#include "SinricProSwitch.h" + + +#define WIFI_SSID "YOUR-WIFI-SSID" +#define WIFI_PASS "YOUR-WIFI-PASSWORD" +#define APP_KEY "YOUR-APP-KEY" // Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx" +#define APP_SECRET "YOUR-APP-SECRET" // Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx" +#define SWITCH_ID "SWITCH_ID" // Should look like "5dc1564130xxxxxxxxxxxxxx" + +#define BAUD_RATE 115200 // Change baudrate to your need + +bool handleOTAUpdate(const String& url, int major, int minor, int patch, bool forceUpdate) { + Version currentVersion = Version(FIRMWARE_VERSION); + Version newVersion = Version(String(major) + "." + String(minor) + "." + String(patch)); + bool updateAvailable = newVersion > currentVersion; + + Serial.print("URL: "); + Serial.println(url.c_str()); + Serial.print("Current version: "); + Serial.println(currentVersion.toString()); + Serial.print("New version: "); + Serial.println(newVersion.toString()); + if (forceUpdate) Serial.println("Enforcing OTA update!"); + + // Handle OTA update based on forceUpdate flag and update availability + if (forceUpdate || updateAvailable) { + if (updateAvailable) { + Serial.println("Update available!"); + } + + String result = startOtaUpdate(url); + if (!result.isEmpty()) { + SinricPro.setResponseMessage(std::move(result)); + return false; + } + return true; + } else { + String result = "Current version is up to date."; + SinricPro.setResponseMessage(std::move(result)); + Serial.println(result); + return false; + } +} + +// setup function for WiFi connection +void setupWiFi() { + Serial.printf("\r\n[Wifi]: Connecting"); + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); + WiFi.setAutoReconnect(true); +#elif defined(ESP32) + WiFi.setSleep(false); + WiFi.setAutoReconnect(true); +#endif + + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while (WiFi.status() != WL_CONNECTED) { + Serial.printf("."); + delay(250); + } + + Serial.printf("connected!\r\n[WiFi]: IP-Address is %s\r\n", WiFi.localIP().toString().c_str()); +} + +// setup function for SinricPro +void setupSinricPro() { + SinricProSwitch& mySwitch = SinricPro[SWITCH_ID]; + + // setup SinricPro + SinricPro.onConnected([]() { + Serial.printf("Connected to SinricPro\r\n"); + }); + SinricPro.onDisconnected([]() { + Serial.printf("Disconnected from SinricPro\r\n"); + }); + SinricPro.onOTAUpdate(handleOTAUpdate); + SinricPro.begin(APP_KEY, APP_SECRET); +} + +// main setup function +void setup() { + Serial.begin(BAUD_RATE); + Serial.printf("\r\n\r\n"); + setupWiFi(); + setupSinricPro(); +} + +void loop() { + SinricPro.handle(); +} diff --git a/examples/OTAUpdate/SemVer.h b/examples/OTAUpdate/SemVer.h new file mode 100644 index 0000000..a7684fd --- /dev/null +++ b/examples/OTAUpdate/SemVer.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +class Version { + public: + Version(const String& versionStr); + String toString() const; + + bool operator > (const Version& other) const; + bool operator < (const Version& other) const; + bool operator == (const Version& other) const; + bool operator != (const Version& other) const; + + protected: + int major; + int minor; + int patch; +}; + +Version::Version(const String& versionStr) { + int firstDot = versionStr.indexOf('.'); + int secondDot = versionStr.lastIndexOf('.'); + major = versionStr.substring(0, firstDot).toInt(); + minor = versionStr.substring(firstDot + 1, secondDot).toInt(); + patch = versionStr.substring(secondDot + 1).toInt(); +} + +String Version::toString() const { + return String(major) + "." + String(minor) + "." + String(patch); +} + +bool Version::operator>(const Version& other) const { + if (major > other.major) return true; + if (minor > other.minor) return true; + if (patch > other.patch) return true; + return false; +} + +bool Version::operator<(const Version& other) const { + if (major < other.major) return true; + if (minor < other.minor) return true; + if (patch < other.patch) return true; + return false; +} + +bool Version::operator==(const Version& other) const { + if (major != other.major) return false; + if (minor != other.minor) return false; + if (patch != other.patch) return false; + return true; +} + +bool Version::operator!=(const Version& other) const { + return !operator==(other); +} \ No newline at end of file diff --git a/examples/Settings/MultiWiFi/MultiWiFi.ino b/examples/Settings/MultiWiFi/MultiWiFi.ino new file mode 100644 index 0000000..16381c9 --- /dev/null +++ b/examples/Settings/MultiWiFi/MultiWiFi.ino @@ -0,0 +1,216 @@ +/* + * Example for how to use SinricPro Primary and Secondary WiFi settings with ESP buit-in WiFiMulti. + * + * If you encounter any issues: + * - check the readme.md at https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md + * - ensure all dependent libraries are installed + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#arduinoide + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#dependencies + * - open serial monitor and check whats happening + * - check full user documentation at https://sinricpro.github.io/esp8266-esp32-sdk + * - visit https://github.com/sinricpro/esp8266-esp32-sdk/issues and check for existing issues or open a new one + */ + +// Uncomment the following line to enable serial debug output +//#define ENABLE_DEBUG + +#ifdef ENABLE_DEBUG +#define DEBUG_ESP_PORT Serial +#define NODEBUG_WEBSOCKETS +#define NDEBUG +#endif + +#include + +#if defined(ESP8266) +#include +#include +ESP8266WiFiMulti wifiMulti; ///< ESP8266 WiFi multi instance. +#elif defined(ESP32) +#include +#include +WiFiMulti wifiMulti; ///< ESP32 WiFi multi instance. +#endif + +#include "FS.h" +#include "LittleFS.h" +#include "ArduinoJson.h" + +#include "SinricPro.h" +#include "SinricProSwitch.h" +#include "SinricProWiFiSettings.h" + +#define APP_KEY "" // Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx" +#define APP_SECRET "" // Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx" +#define SWITCH_ID "" // Should look like "5dc1564130xxxxxxxxxxxxxx" + +#define BAUD_RATE 115200 // Change baudrate to your need + +#define SET_WIFI_PRIMARY "pro.sinric::set.wifi.primary" +#define SET_WIFI_SECONDARY "pro.sinric::set.wifi.secondary" + +const bool formatLittleFSIfFailed = true; +const unsigned long NO_WIFI_REBOOT_TIMEOUT = 300000; // 5 minutes in milliseconds +unsigned long wifiStartTime; + +const char* primary_ssid = ""; // Set to your primary wifi's ssid +const char* primary_password = ""; // Set to your primary wifi's password +const char* secondary_ssid = ""; // Set to your secondary wifi's ssid +const char* secondary_password = ""; // Set to your secondary wifi's password + +SinricProWiFiSettings spws(primary_ssid, primary_password, secondary_ssid, secondary_password, "/wificonfig.dat"); + +bool onSetModuleSetting(const String& id, const String& value) { + // Handle module settings. + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, value); + + if (error) { + Serial.print(F("onSetModuleSetting::deserializeJson() failed: ")); + Serial.println(error.f_str()); + return false; + } + + const char* password = doc["password"].as(); + const char* ssid = doc["ssid"].as(); + + if (id == SET_WIFI_PRIMARY) { // Set primary WiFi + spws.updatePrimarySettings(ssid, password); + } else if (id == SET_WIFI_SECONDARY) { // Set secondary WiFi + spws.updateSecondarySettings(ssid, password); + } + + bool connect = doc["connectNow"] | false; + if (connect) { + #if defined(ESP8266) + wifiMulti.cleanAPlist(); + #elif defined(ESP32) + wifiMulti.APlistClean(); + #endif + + wifiMulti.addAP(ssid, password); + return waitForConnectResult(); + } + + return true; +} + +bool setupLittleFS() { + // Sets up the LittleFS. + #if defined(ESP8266) + if (!LittleFS.begin()) { + #elif defined(ESP32) + if (!LittleFS.begin(true)) { + #endif + + Serial.println("An Error has occurred while mounting LittleFS"); + + if (formatLittleFSIfFailed) { + Serial.println("Formatting LittleFS..."); + if (LittleFS.format()) { + Serial.println("LittleFS formatted successfully!"); + return true; + } else { + Serial.println("LittleFS formatting error!"); + return false; + } + } else { + Serial.println("LittleFS error!"); + return false; + } + } else { + Serial.println("LittleFS Mount successful"); + return true; + } + + return true; +} + +bool waitForConnectResult() { + unsigned long startTime = millis(); + constexpr unsigned int connectTimeout = 10000; + + Serial.println("Connecting Wifi..."); + while (wifiMulti.run() != WL_CONNECTED) { + Serial.print("."); + delay(500); + if (millis() - startTime >= connectTimeout) { + Serial.println("WIFI not connected"); + return false; + } + } + + Serial.printf("\nWiFi connected\nIP address: %s\n", WiFi.localIP().toString().c_str()); + return true; +} + +// setup function for WiFi connection +void setupWiFi() { + Serial.printf("\r\n[Wifi]: Connecting"); + + WiFi.mode(WIFI_STA); +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); +#elif defined(ESP32) + WiFi.setSleep(false); +#endif + WiFi.setAutoReconnect(true); + + // Load settings from file or using defaults if loading fails. + spws.begin(); + + const SinricProWiFiSettings::wifi_settings_t& settings = spws.getWiFiSettings(); + + if (spws.isValidSetting(settings.primarySSID, settings.primaryPassword)) { + wifiMulti.addAP(settings.primarySSID, settings.primaryPassword); + } + + if (spws.isValidSetting(settings.secondarySSID, settings.secondaryPassword)) { + wifiMulti.addAP(settings.secondarySSID, settings.secondaryPassword); + } + + waitForConnectResult(); +} + +// setup function for SinricPro +void setupSinricPro() { + SinricProSwitch& mySwitch = SinricPro[SWITCH_ID]; + + // setup SinricPro + SinricPro.onConnected([]() { + Serial.printf("Connected to SinricPro\r\n"); + }); + SinricPro.onDisconnected([]() { + Serial.printf("Disconnected from SinricPro\r\n"); + }); + + SinricPro.onSetSetting(onSetModuleSetting); + SinricPro.begin(APP_KEY, APP_SECRET); +} + +// main setup function +void setup() { + Serial.begin(BAUD_RATE); + Serial.printf("\r\n\r\n"); + setupLittleFS(); + setupWiFi(); + setupSinricPro(); +} + +void rebootIfNoWiFi() { + // If no WiFI connection for 5 mins reboot the ESP. ESP will connect to either primary or secondary based on availability + if (WiFi.status() != WL_CONNECTED && (millis() - wifiStartTime >= NO_WIFI_REBOOT_TIMEOUT)) { + Serial.println("WiFi connection timed out. Rebooting..."); + ESP.restart(); + } else { + // Reset the start time if WiFi is connected + wifiStartTime = millis(); + } +} + +void loop() { + wifiMulti.run(); + SinricPro.handle(); + rebootIfNoWiFi(); +} diff --git a/examples/Settings/MultiWiFi/SinricProWiFiSettings.cpp b/examples/Settings/MultiWiFi/SinricProWiFiSettings.cpp new file mode 100644 index 0000000..8896ff3 --- /dev/null +++ b/examples/Settings/MultiWiFi/SinricProWiFiSettings.cpp @@ -0,0 +1,108 @@ +#include "SinricProWiFiSettings.h" + +SinricProWiFiSettings::SinricProWiFiSettings(const char* defaultPrimarySSID, const char* defaultPrimaryPassword, + const char* defaultSecondarySSID, const char* defaultSecondaryPassword, + const char* configFileName) + : defaultPrimarySSID(defaultPrimarySSID), defaultPrimaryPassword(defaultPrimaryPassword), + defaultSecondarySSID(defaultSecondarySSID), defaultSecondaryPassword(defaultSecondaryPassword), + configFileName(configFileName) { + memset(&wifiSettings, 0, sizeof(wifiSettings)); +} + +void SinricProWiFiSettings::begin() { + if (!loadFromFile()) { + saveDefaultSettings(); + } + printSettings(); +} + +void SinricProWiFiSettings::updatePrimarySettings(const char* newSSID, const char* newPassword) { + if (isValidSetting(newSSID, newPassword)) { + strncpy(wifiSettings.primarySSID, newSSID, sizeof(wifiSettings.primarySSID)); + strncpy(wifiSettings.primaryPassword, newPassword, sizeof(wifiSettings.primaryPassword)); + saveToFile(); + } else { + Serial.println("Invalid Primary SSID or Password"); + } +} + +void SinricProWiFiSettings::updateSecondarySettings(const char* newSSID, const char* newPassword) { + if (isValidSetting(newSSID, newPassword)) { + strncpy(wifiSettings.secondarySSID, newSSID, sizeof(wifiSettings.secondarySSID)); + strncpy(wifiSettings.secondaryPassword, newPassword, sizeof(wifiSettings.secondaryPassword)); + saveToFile(); + } else { + Serial.println("Invalid Secondary SSID or Password"); + } +} + +void SinricProWiFiSettings::printSettings() const { + Serial.printf("Primary SSID: %s\n", wifiSettings.primarySSID); + Serial.printf("Primary Password: %s\n", wifiSettings.primaryPassword); + Serial.printf("Secondary SSID: %s\n", wifiSettings.secondarySSID); + Serial.printf("Secondary Password: %s\n", wifiSettings.secondaryPassword); +} + +void SinricProWiFiSettings::saveToFile() { + + #if defined(ESP8266) + File file = LittleFS.open(configFileName, "w"); + #elif defined(ESP32) + File file = LittleFS.open(configFileName, FILE_WRITE); + #endif + + if (file) { + file.write(reinterpret_cast(&wifiSettings), sizeof(wifiSettings)); + file.close(); + } +} + +bool SinricProWiFiSettings::loadFromFile() { + #if defined(ESP8266) + File file = LittleFS.open(configFileName, "r"); + #elif defined(ESP32) + File file = LittleFS.open(configFileName, FILE_READ); + #endif + + if (file && file.size() == sizeof(wifiSettings)) { + file.read(reinterpret_cast(&wifiSettings), sizeof(wifiSettings)); + file.close(); + return true; + } + return false; +} + +void SinricProWiFiSettings::saveDefaultSettings() { + Serial.println("Saving default WiFi login!"); + + strncpy(wifiSettings.primarySSID, defaultPrimarySSID, sizeof(wifiSettings.primarySSID)); + strncpy(wifiSettings.primaryPassword, defaultPrimaryPassword, sizeof(wifiSettings.primaryPassword)); + strncpy(wifiSettings.secondarySSID, defaultSecondarySSID, sizeof(wifiSettings.secondarySSID)); + strncpy(wifiSettings.secondaryPassword, defaultSecondaryPassword, sizeof(wifiSettings.secondaryPassword)); + + saveToFile(); +} + +void SinricProWiFiSettings::deleteAllSettings() { + memset(&wifiSettings, 0, sizeof(wifiSettings)); + if (LittleFS.exists(configFileName)) { + LittleFS.remove(configFileName); + } + Serial.println("All WiFi settings have been deleted."); +} + +bool SinricProWiFiSettings::isValidSetting(const char* ssid, const char* password) const { + return validateSSID(ssid) && validatePassword(password); +} + +bool SinricProWiFiSettings::validateSSID(const char* ssid) const { + return ssid && strlen(ssid) > 0 && strlen(ssid) < sizeof(wifi_settings_t::primarySSID); +} + +bool SinricProWiFiSettings::validatePassword(const char* password) const { + return password && strlen(password) < sizeof(wifi_settings_t::primaryPassword); +} + +const SinricProWiFiSettings::wifi_settings_t& SinricProWiFiSettings::getWiFiSettings() const { + return wifiSettings; +} diff --git a/examples/Settings/MultiWiFi/SinricProWiFiSettings.h b/examples/Settings/MultiWiFi/SinricProWiFiSettings.h new file mode 100644 index 0000000..b3352db --- /dev/null +++ b/examples/Settings/MultiWiFi/SinricProWiFiSettings.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include "FS.h" +#include "LittleFS.h" + + +/** + * @brief Manages SinricPro using primary and secondary SSID configurations. + */ +class SinricProWiFiSettings { +public: + struct wifi_settings_t { + char primarySSID[32]; ///< Primary SSID of the WiFi network. + char primaryPassword[64]; ///< Primary password of the WiFi network. + char secondarySSID[32]; ///< Secondary SSID of the WiFi network. + char secondaryPassword[64]; ///< Secondary password of the WiFi network. + }; + + /** + * @brief Construct a new SinricProWiFiSettings object with default WiFi settings. + * + * @param defaultPrimarySSID Default primary SSID. + * @param defaultPrimaryPassword Default primary password. + * @param defaultSecondarySSID Default secondary SSID. + * @param defaultSecondaryPassword Default secondary password. + * @param configFileName File name for storing WiFi settings. + */ + SinricProWiFiSettings(const char* defaultPrimarySSID, const char* defaultPrimaryPassword, + const char* defaultSecondarySSID, const char* defaultSecondaryPassword, + const char* configFileName); + + /** + * @brief Initializes the WiFi manager, loading settings from file or using defaults if loading fails. + */ + void begin(); + + /** + * @brief Updates the primary WiFi settings. + * + * @param newSSID New primary SSID. + * @param newPassword New primary password. + */ + void updatePrimarySettings(const char* newSSID, const char* newPassword); + + /** + * @brief Updates the secondary WiFi settings. + * + * @param newSSID New secondary SSID. + * @param newPassword New secondary password. + */ + void updateSecondarySettings(const char* newSSID, const char* newPassword); + + /** + * @brief Prints the current WiFi settings to the Serial console. + */ + void printSettings() const; + + /** + * @brief Checks if the provided SSID and password are valid. + * + * @param ssid SSID to check. + * @param password Password to check. + * @return true if both SSID and password are valid, false otherwise. + */ + bool isValidSetting(const char* ssid, const char* password) const; + + /** + * @brief Returns WiFi settings. + */ + const wifi_settings_t& getWiFiSettings() const; + +private: + const char* defaultPrimarySSID; ///< Default primary SSID. + const char* defaultPrimaryPassword; ///< Default primary password. + const char* defaultSecondarySSID; ///< Default secondary SSID. + const char* defaultSecondaryPassword; ///< Default secondary password. + const char* configFileName; ///< File name to store WiFi settings. + + wifi_settings_t wifiSettings; + + /** + * @brief Saves the current WiFi settings to a file. + */ + void saveToFile(); + + /** + * @brief Loads WiFi settings from a file. + * + * @return true if loaded successfully, false otherwise. + */ + bool loadFromFile(); + + /** + * @brief Saves the default WiFi settings to a file and updates the current settings. + */ + void saveDefaultSettings(); + + + /** + * @brief Validates the given SSID. + * + * @param ssid SSID to validate. + * @return true if the SSID is valid, false otherwise. + */ + bool validateSSID(const char* ssid) const; + + /** + * @brief Validates the given password. + * + * @param password Password to validate. + * @return true if the password is valid, false otherwise. + */ + bool validatePassword(const char* password) const; + + /** + * @brief Deletes all the stored WiFi settings. + */ + void deleteAllSettings(); +}; diff --git a/examples/Settings/Settings/Settings.ino b/examples/Settings/Settings/Settings.ino new file mode 100644 index 0000000..53a61ad --- /dev/null +++ b/examples/Settings/Settings/Settings.ino @@ -0,0 +1,100 @@ +/* + * Example for how to use SinricPro Settings: + * + * If you encounter any issues: + * - check the readme.md at https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md + * - ensure all dependent libraries are installed + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#arduinoide + * - see https://github.com/sinricpro/esp8266-esp32-sdk/blob/master/README.md#dependencies + * - open serial monitor and check whats happening + * - check full user documentation at https://sinricpro.github.io/esp8266-esp32-sdk + * - visit https://github.com/sinricpro/esp8266-esp32-sdk/issues and check for existing issues or open a new one + */ + +// Uncomment the following line to enable serial debug output +//#define ENABLE_DEBUG + +#ifdef ENABLE_DEBUG +#define DEBUG_ESP_PORT Serial +#define NODEBUG_WEBSOCKETS +#define NDEBUG +#endif + +#include +#if defined(ESP8266) +#include +#elif defined(ESP32) || defined(ARDUINO_ARCH_RP2040) +#include +#endif + +#include "SinricPro.h" +#include "SinricProSwitch.h" +#include "ArduinoJson.h" + +#define WIFI_SSID "YOUR-WIFI-SSID" +#define WIFI_PASS "YOUR-WIFI-PASSWORD" +#define APP_KEY "YOUR-APP-KEY" // Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx" +#define APP_SECRET "YOUR-APP-SECRET" // Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx" +#define SWITCH_ID "YOUR-DEVICE-ID" // Should look like "5dc1564130xxxxxxxxxxxxxx" + +#define BAUD_RATE 115200 // Change baudrate to your need + +bool onSetDeviceSetting(const String& deviceId, const String& settingId, const String& settingValue) { + // Handle device settings. + return true; +} + +bool onSetModuleSetting(const String& id, const String& value) { + // Handle module settings. + return true; +} + +// setup function for WiFi connection +void setupWiFi() { + Serial.printf("\r\n[Wifi]: Connecting"); + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); + WiFi.setAutoReconnect(true); +#elif defined(ESP32) + WiFi.setSleep(false); + WiFi.setAutoReconnect(true); +#endif + + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while (WiFi.status() != WL_CONNECTED) { + Serial.printf("."); + delay(250); + } + Serial.printf("connected!\r\n[WiFi]: IP-Address is %s\r\n", WiFi.localIP().toString().c_str()); +} + +// setup function for SinricPro +void setupSinricPro() { + SinricProSwitch& mySwitch = SinricPro[SWITCH_ID]; + mySwitch.onSetSetting(onSetDeviceSetting); + + // setup SinricPro + SinricPro.onConnected([]() { + Serial.printf("Connected to SinricPro\r\n"); + }); + SinricPro.onDisconnected([]() { + Serial.printf("Disconnected from SinricPro\r\n"); + }); + + SinricPro.onSetSetting(onSetModuleSetting); + SinricPro.begin(APP_KEY, APP_SECRET); +} + +// main setup function +void setup() { + Serial.begin(BAUD_RATE); + Serial.printf("\r\n\r\n"); + setupWiFi(); + setupSinricPro(); +} + +void loop() { + SinricPro.handle(); +} diff --git a/library.json b/library.json index b3e0fd9..74cbc87 100644 --- a/library.json +++ b/library.json @@ -13,7 +13,7 @@ "maintainer": true } ], - "version": "3.1.0", + "version": "3.2.0", "frameworks": "arduino", "platforms": [ "espressif8266", diff --git a/library.properties b/library.properties index 361b3d8..ce30026 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=SinricPro -version=3.1.0 +version=3.2.0 author=Boris Jaeger maintainer=Boris Jaeger sentence=Library for https://sinric.pro - simple way to connect your device to alexa diff --git a/src/SinricPro.h b/src/SinricPro.h index c5c1893..567da20 100644 --- a/src/SinricPro.h +++ b/src/SinricPro.h @@ -17,6 +17,7 @@ #include "SinricProUDP.h" #include "SinricProWebsocket.h" #include "Timestamp.h" +#include "SinricProModuleCommandHandler.h" namespace SINRICPRO_NAMESPACE { /** @@ -37,6 +38,47 @@ using ConnectedCallbackHandler = std::function; */ using DisconnectedCallbackHandler = std::function; +/** + * @brief Function signature for OTA update callback. + * + * This typedef defines a function pointer type for OTA (Over-The-Air) update callbacks. + * The callback function should accept a URL string and a Version struct as parameters. + * + * @param url The URL from which the OTA update can be downloaded. + * @param major The major version number + * @param minor The minor version number + * @param patch The patch version number + * @param forceUpdate Skip the version check and apply the update. + * @return bool True if the update process started successful, false otherwise. + */ +using OTAUpdateCallbackHandler = std::function; + +/** + * @brief Function signature for setting a module setting. + * + * This callback is used to set a value for a specific setting identified by its ID. + * + * @param id The unique identifier of the setting to be set. + * @param value The new value to be assigned to the setting. + * @return bool Returns true if the setting was successfully updated, false otherwise. + */ +using SetSettingCallbackHandler = std::function; + +/** + * @typedef ReportHealthCallbackHandler + * @brief Defines a function type for reporting device health status. + * + * This typedef creates an alias for a std::function that takes a reference to a String + * and returns a boolean value. It's designed to be used as a callback for health reporting + * + * @param healthReport A reference to a String that will contain the health status information. + * The callback function should populate this string with relevant health data. + * + * @return bool Returns true if the health report was successfully handled + * false otherwise. + */ +using ReportHealthCallbackHandler = std::function; + using PongCallback = std::function; /** @@ -62,6 +104,9 @@ class SinricProClass : public SinricProInterface { void setResponseMessage(String&& message); unsigned long getTimestamp() override; Proxy operator[](const String deviceId); + void onOTAUpdate(OTAUpdateCallbackHandler cb); + void onSetSetting(SetSettingCallbackHandler cb); + void onReportHealth(ReportHealthCallbackHandler cb); protected: template @@ -78,7 +123,8 @@ class SinricProClass : public SinricProInterface { void handleReceiveQueue(); void handleSendQueue(); - void handleRequest(JsonDocument& requestMessage, interface_t Interface); + void handleDeviceRequest(JsonDocument& requestMessage, interface_t Interface); + void handleModuleRequest(JsonDocument& requestMessage, interface_t Interface); void handleResponse(JsonDocument& responseMessage); JsonDocument prepareRequest(String deviceId, const char* action); @@ -112,6 +158,8 @@ class SinricProClass : public SinricProInterface { bool _begin = false; String responseMessageStr = ""; + + SinricProModuleCommandHandler _moduleCommandHandler; }; class SinricProClass::Proxy { @@ -146,7 +194,7 @@ DeviceType& SinricProClass::getDeviceInstance(String deviceId) { DeviceType* tmp_device = (DeviceType*)getDevice(deviceId); if (tmp_device) return *tmp_device; - DEBUG_SINRIC("[SinricPro]: Device \"%s\" does not exist. Creating new device\r\n", deviceId.c_str()); + DEBUG_SINRIC("[SinricPro]: Device \"%s\" does not exist in the internal device list. creating new device\r\n", deviceId.c_str()); DeviceType& tmp_deviceInstance = add(deviceId); if (isConnected()) { @@ -277,8 +325,39 @@ void SinricProClass::handleResponse(JsonDocument& responseMessage) { #endif } -void SinricProClass::handleRequest(JsonDocument& requestMessage, interface_t Interface) { - DEBUG_SINRIC("[SinricPro.handleRequest()]: handling request\r\n"); +void SinricProClass::handleModuleRequest(JsonDocument& requestMessage, interface_t Interface) { + DEBUG_SINRIC("[SinricPro.handleModuleScopeRequest()]: handling device sope request\r\n"); +#ifndef NODEBUG_SINRIC + serializeJsonPretty(requestMessage, DEBUG_ESP_PORT); +#endif + + JsonDocument responseMessage = prepareResponse(requestMessage); + + String action = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_action] | ""; + JsonObject request_value = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_value]; + JsonObject response_value = responseMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_value]; + SinricProRequest request{ action, "", request_value, response_value}; + + bool success = _moduleCommandHandler.handleRequest(request); + + responseMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_success] = success; + responseMessage[FSTR_SINRICPRO_payload].remove(FSTR_SINRICPRO_deviceId); + if (!success) { + if (responseMessageStr.length() > 0) { + responseMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_message] = responseMessageStr; + responseMessageStr = ""; + } else { + responseMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_message] = "Module did not handle \"" + action + "\""; + } + } + + String responseString; + serializeJson(responseMessage, responseString); + sendQueue.push(new SinricProMessage(Interface, responseString.c_str())); +} + +void SinricProClass::handleDeviceRequest(JsonDocument& requestMessage, interface_t Interface) { + DEBUG_SINRIC("[SinricPro.handleDeviceRequest()]: handling device sope request\r\n"); #ifndef NODEBUG_SINRIC serializeJsonPretty(requestMessage, DEBUG_ESP_PORT); #endif @@ -345,9 +424,17 @@ void SinricProClass::handleReceiveQueue() { DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is valid. Processing message...\r\n"); extractTimestamp(jsonMessage); if (messageType == FSTR_SINRICPRO_response) handleResponse(jsonMessage); - if (messageType == FSTR_SINRICPRO_request) handleRequest(jsonMessage, rawMessage->getInterface()); + if (messageType == FSTR_SINRICPRO_request) { + String scope = jsonMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_scope] | FSTR_SINRICPRO_device; + if (strcmp(FSTR_SINRICPRO_module, scope.c_str()) == 0) { + handleModuleRequest(jsonMessage, rawMessage->getInterface()); + } else { + handleDeviceRequest(jsonMessage, rawMessage->getInterface()); + } + }; } else { - DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is invalid! Sending messsage to [dev/null] ;)\r\n"); + DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is invalid! \r\n"); + if (messageType == FSTR_SINRICPRO_request) handleDeviceRequest(jsonMessage, rawMessage->getInterface()); } delete rawMessage; } @@ -416,6 +503,50 @@ bool SinricProClass::isConnected() { return _websocketListener.isConnected(); }; +/** + * @brief Set callback function for OTA (Over-The-Air) updates. + * + * This method registers a callback function that will be called when an OTA update is available. + * The callback should handle the process of downloading and applying the update. + * + * @param cb A function pointer or lambda of type OTAUpdateCallbackHandler. + * The callback should return a boolean indicating whether the update was successful. + * + */ +void SinricProClass::onOTAUpdate(OTAUpdateCallbackHandler cb) { + _moduleCommandHandler.onOTAUpdate(cb); +} + +/** + * @brief Set callback function for setting a module setting. + * + * This method registers a callback function that will be called when a request to change + * a module setting is received. + * @return void + * @param cb A function pointer or lambda of type SetSettingCallbackHandler. + * The callback should return a boolean indicating whether the setting was successfully updated. + * + */ +void SinricProClass::onSetSetting(SetSettingCallbackHandler cb) { + _moduleCommandHandler.onSetSetting(cb); +} + +/** + * @brief Sets the callback function for reporting device health status. + * + * This method allows the user to set a custom callback function that will be called + * when the SinricPro system needs to report the device's health status. + * + * @param cb A function pointer of type ReportHealthCallbackHandler. + * This callback should populate a String with health information and return a boolean + * indicating success or failure of the health reporting process. + * @return void + * @see ReportHealthCallbackHandler for the definition of the callback function type. + */ +void SinricProClass::onReportHealth(ReportHealthCallbackHandler cb) { + _moduleCommandHandler.onReportHealth(cb); +} + /** * @brief Set callback function for websocket connected event * @@ -546,6 +677,7 @@ JsonDocument SinricProClass::prepareResponse(JsonDocument& requestMessage) { JsonObject payload = responseMessage[FSTR_SINRICPRO_payload].to(); payload[FSTR_SINRICPRO_action] = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_action]; payload[FSTR_SINRICPRO_clientId] = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_clientId]; + payload[FSTR_SINRICPRO_scope] = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_scope]; payload[FSTR_SINRICPRO_createdAt] = 0; payload[FSTR_SINRICPRO_deviceId] = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_deviceId]; if (requestMessage[FSTR_SINRICPRO_payload].containsKey(FSTR_SINRICPRO_instanceId)) payload[FSTR_SINRICPRO_instanceId] = requestMessage[FSTR_SINRICPRO_payload][FSTR_SINRICPRO_instanceId]; diff --git a/src/SinricProModuleCommandHandler.h b/src/SinricProModuleCommandHandler.h new file mode 100644 index 0000000..fd0d347 --- /dev/null +++ b/src/SinricProModuleCommandHandler.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019 Sinric. All rights reserved. + * Licensed under Creative Commons Attribution-Share Alike (CC BY-SA) + * + * This file is part of the Sinric Pro (https://github.com/sinricpro/) + */ + +#pragma once + +#include "SinricProRequest.h" + +#include "SinricProNamespace.h" +namespace SINRICPRO_NAMESPACE { + +FSTR(OTA, otaUpdateAvailable); // "otaUpdateAvailable" +FSTR(OTA, url); // "url" +FSTR(OTA, version); // "version" +FSTR(OTA, major); // "major" +FSTR(OTA, minor); // "minor" +FSTR(OTA, patch); // "patch" +FSTR(OTA, forceUpdate); // "forceUpdate" +FSTR(SETTINGS, setSetting); // "setSetting" +FSTR(SETTINGS, id); // "id" +FSTR(SETTINGS, value); // "value" +FSTR(INSIGHTS, health); // "health" +FSTR(INSIGHTS, report); // "report" + +using OTAUpdateCallbackHandler = std::function; +using SetSettingCallbackHandler = std::function; +using ReportHealthCallbackHandler = std::function; + +class SinricProModuleCommandHandler { + public: + SinricProModuleCommandHandler(); + ~SinricProModuleCommandHandler(); + + bool handleRequest(SinricProRequest &request); + void onOTAUpdate(OTAUpdateCallbackHandler callback); + void onSetSetting(SetSettingCallbackHandler callback); + void onReportHealth(ReportHealthCallbackHandler callback); + + private: + OTAUpdateCallbackHandler _otaUpdateCallbackHandler; + SetSettingCallbackHandler _setSettingCallbackHandler; + ReportHealthCallbackHandler _reportHealthCallbackHandler; +}; + +SinricProModuleCommandHandler::SinricProModuleCommandHandler() + : _otaUpdateCallbackHandler(nullptr), + _setSettingCallbackHandler(nullptr), + _reportHealthCallbackHandler(nullptr) {} + +SinricProModuleCommandHandler::~SinricProModuleCommandHandler() {} + +void SinricProModuleCommandHandler::onOTAUpdate(OTAUpdateCallbackHandler callback) { + _otaUpdateCallbackHandler = callback; +} + +void SinricProModuleCommandHandler::onSetSetting(SetSettingCallbackHandler callback) { + _setSettingCallbackHandler = callback; +} + +void SinricProModuleCommandHandler::onReportHealth(ReportHealthCallbackHandler callback) { + _reportHealthCallbackHandler = callback; +} + +bool SinricProModuleCommandHandler::handleRequest(SinricProRequest &request) { + if (strcmp(FSTR_OTA_otaUpdateAvailable, request.action.c_str()) == 0 && _otaUpdateCallbackHandler) { + String url = request.request_value[FSTR_OTA_url]; + int major = request.request_value[FSTR_OTA_version][FSTR_OTA_major]; + int minor = request.request_value[FSTR_OTA_version][FSTR_OTA_minor]; + int patch = request.request_value[FSTR_OTA_version][FSTR_OTA_patch]; + bool forceUpdate = request.request_value[FSTR_OTA_version][FSTR_OTA_forceUpdate] | false; + return _otaUpdateCallbackHandler(url, major, minor, patch, forceUpdate); + } + else if (strcmp(FSTR_SETTINGS_setSetting, request.action.c_str()) == 0 && _setSettingCallbackHandler) { + String id = request.request_value[FSTR_SETTINGS_id]; + String value = request.request_value[FSTR_SETTINGS_value]; + return _setSettingCallbackHandler(id, value); + } + else if (strcmp(FSTR_INSIGHTS_health, request.action.c_str()) == 0 && _reportHealthCallbackHandler) { + String healthReport = ""; + bool success = _reportHealthCallbackHandler(healthReport); + if (success) { + request.response_value[FSTR_INSIGHTS_report] = healthReport; + } + return success; + } + else { + DEBUG_SINRIC("[SinricProModuleCommandHandler:handleRequest]: action: %s not supported!\r\n", request.action.c_str()); + } + return false; +} + +} // SINRICPRO_NAMESPACE diff --git a/src/SinricProStrings.h b/src/SinricProStrings.h index 61836df..2f71fa6 100644 --- a/src/SinricProStrings.h +++ b/src/SinricProStrings.h @@ -38,5 +38,8 @@ FSTR(SINRICPRO, PERIODIC_POLL); // "PERIODIC_POLL" FSTR(SINRICPRO, PHYSICAL_INTERACTION); // "PHYSICAL_INTERACTION" FSTR(SINRICPRO, ALERT); // "ALERT" FSTR(SINRICPRO, OK); // "OK" +FSTR(SINRICPRO, scope); // "scope" +FSTR(SINRICPRO, module); // "module" +FSTR(SINRICPRO, device); // "device" } // SINRICPRO_NAMESPACE \ No newline at end of file diff --git a/src/SinricProVersion.h b/src/SinricProVersion.h index d392509..875d214 100644 --- a/src/SinricProVersion.h +++ b/src/SinricProVersion.h @@ -5,7 +5,7 @@ // Version Configuration #define SINRICPRO_VERSION_MAJOR 3 -#define SINRICPRO_VERSION_MINOR 1 +#define SINRICPRO_VERSION_MINOR 2 #define SINRICPRO_VERSION_REVISION 0 #define SINRICPRO_VERSION STR(SINRICPRO_VERSION_MAJOR) "." STR(SINRICPRO_VERSION_MINOR) "." STR(SINRICPRO_VERSION_REVISION) #define SINRICPRO_VERSION_STR "SinricPro (v" SINRICPRO_VERSION ")"