diff --git a/src/core/shuntingYard.cpp b/src/core/shuntingYard.cpp index d90b0a9c7..da3e6df10 100644 --- a/src/core/shuntingYard.cpp +++ b/src/core/shuntingYard.cpp @@ -380,11 +380,15 @@ std::string commands(std::string & expr, bool quotes) { if (return_code != CommandRet::OK && return_code != CommandRet::NO_VALUE) { return expr = ""; } - - std::string data = output["api_data"] | ""; - if (!isnum(data) && quotes) { - data.insert(data.begin(), '"'); - data.insert(data.end(), '"'); + std::string data; + if (output["api_data"].is()) { + data = output["api_data"].as(); + if (!isnum(data) && quotes) { + data.insert(data.begin(), '"'); + data.insert(data.end(), '"'); + } + } else { + serializeJson(output, data); } expr.replace(f, l, data); e = f + data.length(); @@ -700,7 +704,6 @@ std::string compute(const std::string & expr) { std::string cmd = expr_new.substr(f, e - f).c_str(); JsonDocument doc; if (DeserializationError::Ok == deserializeJson(doc, cmd)) { - HTTPClient http; std::string url, header_s, value_s, method_s, key_s, keys_s; // search keys lower case for (JsonPair p : doc.as()) { @@ -720,56 +723,149 @@ std::string compute(const std::string & expr) { keys_s = p.key().c_str(); } } - if (http.begin(url.c_str())) { - int httpResult = 0; - for (JsonPair p : doc[header_s].as()) { - http.addHeader(p.key().c_str(), p.value().as().c_str()); + bool content_set = false; + std::string value = doc[value_s] | ""; + std::string method = doc[method_s] | "GET"; + if (value.length()) { + method = "POST"; + } + std::string result; + int httpResult = 0; +#ifndef NO_TLS_SUPPORT + if (Helpers::toLower(url.c_str()).starts_with("https://")) { + WiFiClient * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = new ESP_SSLClient; + ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert); + ssl_client->setBufferSizes(1024, 1024); + ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds) + url.replace(0, 8, ""); + std::string host = url; + auto index = url.find_first_of('/'); + if (index != std::string::npos) { + host = url.substr(0, index); + url.replace(0, index, ""); } - std::string value = doc[value_s] | ""; - std::string method = doc[method_s] | "get"; - - // if there is data, force a POST - if (value.length() || Helpers::toLower(method) == "post") { - if (value.find_first_of('{') != std::string::npos) { - http.addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON - } - httpResult = http.POST(value.c_str()); - } else { - httpResult = http.GET(); // normal GET + /* + index = host.find_first_of('@'); + std::string auth; + if (index != std::string::npos) { + auth = base64::encode(host.substr(0, index)); + host.replace(0, index, ""); } - - if (httpResult > 0) { - std::string result = http.getString().c_str(); - std::string key = doc[key_s] | ""; - JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body - if (doc[keys_s].is()) { - keys_doc.set(doc[keys_s].as()); - } - JsonArray keys = keys_doc.as(); - - if (key.length() || !keys.isNull()) { - doc.clear(); - if (DeserializationError::Ok == deserializeJson(doc, result)) { - if (key.length()) { - result = doc[key.c_str()].as(); + */ + ssl_client->setClient(basic_client); + if (ssl_client->connect(host.c_str(), 443)) { + if (value.length() || Helpers::toLower(method) == "post") { + ssl_client->print("POST "); + ssl_client->print(url.c_str()); + ssl_client->println(" HTTP/1.1"); + ssl_client->print("Host: "); + ssl_client->println(host.c_str()); + for (JsonPair p : doc[header_s].as()) { + content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type"); + ssl_client->print(p.key().c_str()); + ssl_client->print(": "); + ssl_client->println(p.value().as().c_str()); + } + if (!content_set) { + ssl_client->print("Content-Type: "); + if (value.starts_with('{')) { + ssl_client->println(asyncsrv::T_application_json); } else { - JsonVariant json = doc.as(); - for (JsonVariant keys_key : keys) { - if (keys_key.is() && json.is()) { - json = json[keys_key.as()].as(); - } else if (keys_key.is() && json.is()) { - json = json[keys_key.as()].as(); - } else { - break; // type mismatch - } - } - result = json.as(); + ssl_client->println(asyncsrv::T_text_plain); } } + ssl_client->print("Content-Length: "); + ssl_client->println(value.length()); + ssl_client->println("Connection: close"); + ssl_client->print("\r\n"); + ssl_client->print(value.c_str()); + } else { + ssl_client->print("GET "); + ssl_client->print(url.c_str()); + ssl_client->println(" HTTP/1.1"); + ssl_client->print("Host: "); + ssl_client->println(host.c_str()); + for (JsonPair p : doc[header_s].as()) { + ssl_client->print(p.key().c_str()); + ssl_client->print(": "); + ssl_client->println(p.value().as().c_str()); + } + ssl_client->println("Connection: close"); + } + auto ms = millis(); + while (!ssl_client->available() && millis() - ms < 3000) { + delay(0); + } + while (ssl_client->available()) { + result += (char)ssl_client->read(); + } + ssl_client->stop(); + index = result.find_first_of(' '); + if (index != std::string::npos) { + httpResult = stoi(result.substr(index + 1, 3)); + } + index = result.find("\r\n\r\n"); + if (index != std::string::npos) { + result.replace(0, index + 4, ""); } - expr_new.replace(f, e - f, result.c_str()); } - http.end(); + delete ssl_client; + delete basic_client; + } else +#endif + if (Helpers::toLower(url.c_str()).starts_with("http://")) { + HTTPClient * http = new HTTPClient; + if (http->begin(url.c_str())) { + for (JsonPair p : doc[header_s].as()) { + http->addHeader(p.key().c_str(), p.value().as().c_str()); + content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type"); + } + if (value.length() || Helpers::toLower(method) == "post") { + if (!content_set) { + http->addHeader("Content-Type", value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain); + } + httpResult = http->POST(value.c_str()); + } else { + httpResult = http->GET(); // normal GET + } + if (httpResult > 0) { + result = http->getString().c_str(); + } + } + http->end(); + delete http; + } + if (httpResult == 200) { + std::string key = doc[key_s] | ""; + JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body + if (doc[keys_s].is()) { + keys_doc.set(doc[keys_s].as()); + } + JsonArray keys = keys_doc.as(); + if (key.length() || !keys.isNull()) { + doc.clear(); + if (DeserializationError::Ok == deserializeJson(doc, result)) { + if (key.length()) { + result = doc[key.c_str()].as(); + } else { + JsonVariant json = doc.as(); + for (JsonVariant keys_key : keys) { + if (keys_key.is() && json.is()) { + json = json[keys_key.as()].as(); + } else if (keys_key.is() && json.is()) { + json = json[keys_key.as()].as(); + } else { + break; // type mismatch + } + } + result = json.as(); + } + } + } + expr_new.replace(f, e - f, result); + } else if (httpResult != 0) { + EMSESP::logger().warning("URL command failed with https code: %d, response: %s", httpResult, result.c_str()); } } f = expr_new.find_first_of('{', e); diff --git a/src/core/system.cpp b/src/core/system.cpp index 1dcc9465a..19d037877 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -16,1711 +16,1423 @@ * along with this program. If not, see . */ - #include "system.h" - #include "emsesp.h" // for send_raw_telegram() command - - #ifndef EMSESP_STANDALONE - #include "esp_image_format.h" - #include "esp_ota_ops.h" - #include "esp_partition.h" - #include - #include "esp_efuse.h" - #include - #include - #endif - - #include - #include - - #include - - #if defined(EMSESP_TEST) - #include "../test/test.h" - #endif - - #ifndef NO_TLS_SUPPORT - #define ENABLE_SMTP - #define USE_ESP_SSLCLIENT - #define READYCLIENT_SSL_CLIENT ESP_SSLClient - #define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient - #include - #include - #endif - - namespace emsesp { - - // Languages supported. Note: the order is important - // and must match locale_translations.h and common.h - #if defined(EMSESP_TEST) - // in Test mode use two languages (en & de) to save flash memory needed for the tests - const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; - #elif defined(EMSESP_EN_ONLY) - // EN only - const char * const languages[] = {EMSESP_LOCALE_EN}; - #elif defined(EMSESP_DE_ONLY) - // EN + DE - const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; - #else - const char * const languages[] = {EMSESP_LOCALE_EN, - EMSESP_LOCALE_DE, - EMSESP_LOCALE_NL, - EMSESP_LOCALE_SV, - EMSESP_LOCALE_PL, - EMSESP_LOCALE_NO, - EMSESP_LOCALE_FR, - EMSESP_LOCALE_TR, - EMSESP_LOCALE_IT, - EMSESP_LOCALE_SK, - EMSESP_LOCALE_CZ}; - #endif - - static constexpr uint8_t NUM_LANGUAGES = sizeof(languages) / sizeof(const char *); - - #ifndef EMSESP_STANDALONE - uuid::syslog::SyslogService System::syslog_; - #endif - - uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN}; - - // init statics - PButton System::myPButton_; - bool System::test_set_all_active_ = false; - uint32_t System::max_alloc_mem_; - uint32_t System::heap_mem_; - - // LED flash timer - uint8_t System::led_flash_gpio_ = 0; - uint8_t System::led_flash_type_ = 0; - uint32_t System::led_flash_start_time_ = 0; - uint32_t System::led_flash_duration_ = 0; - bool System::led_flash_timer_ = false; - - // GPIOs - std::vector> System::valid_system_gpios_; - std::vector> System::used_gpios_; - - // find the index of the language - // 0 = EN, 1 = DE, etc... - uint8_t System::language_index() { - for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { - if (languages[i] == locale()) { - return i; - } - } - return 0; // EN only - } - - // send raw to ems - bool System::command_send(const char * value, const int8_t id) { - return EMSESP::txservice_.send_raw(value); // ignore id - } - - bool System::command_sendmail(const char * value, const int8_t id) { - bool enabled = false; - bool ssl, starttls; - uint16_t port; - String server, login, pass, sender, recp, subject; - EMSESP::webSettingsService.read([&](WebSettings & settings) { - enabled = settings.email_enabled; - ssl = settings.email_ssl; - starttls = settings.email_starttls; - server = settings.email_server; - port = settings.email_port; - login = settings.email_login; - pass = settings.email_pass; - sender = settings.email_sender; - recp = settings.email_recp; - subject = settings.email_subject; - }); - if (!enabled) { - return false; - } - LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value); - // LOG_DEBUG("Command sendmail port %d called with '%s'", port, value); - bool success = false; - - #ifndef NO_TLS_SUPPORT - WiFiClient * basic_client; - ESP_SSLClient * ssl_client; - ReadyClient * r_client; // rClient(ssl_client); - SMTPClient * smtp; // smtp(rClient); - basic_client = new WiFiClient; - ssl_client = new ESP_SSLClient; - r_client = new ReadyClient(*ssl_client); - smtp = new SMTPClient(*r_client); - - ssl_client->setClient(basic_client); - ssl_client->setInsecure(); - ssl_client->setBufferSizes(1024, 1024); - r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text); - - // smtp->connect(server, port, sendmailCallback); - smtp->connect(server, port); - if (!smtp->isConnected()) { - LOG_ERROR("Sendmail connection error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - return false; - } - - // LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str()); - smtp->authenticate(login, pass, readymail_auth_password); - if (!smtp->isAuthenticated()) { - LOG_ERROR("Sendmail authenticate error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - return false; - } - JsonDocument doc; - String body = value; - if (body.length()) { - auto error = deserializeJson(doc, (const char *)value); - if (!error && doc.as().size() >= 0) { - subject = doc["subject"] | subject; - recp = doc["to"] | recp; - sender = doc["from"] | sender; - body = doc["body"] | body; - } - } - - SMTPMessage & msg = smtp->getMessage(); - msg.headers.add(rfc822_subject, subject); - msg.headers.add(rfc822_from, sender); - msg.headers.add(rfc822_to, recp); - - // Use addCustom to add custom header e.g. Importance and Priority. - // msg.headers.addCustom("Importance", PRIORITY); - // msg.headers.addCustom("X-MSMail-Priority", PRIORITY); - // msg.headers.addCustom("X-Priority", PRIORITY_NUM); - - msg.text.body(body); - - // bodyText.replace("\r\n", "
\r\n"); - // msg.html.body("
" + bodyText + "
"); - // msg.html.transferEncoding("base64"); - - // With embedFile function, the html message will send as attachment. - // if (EMBED_MESSAGE) - // msg.html.embedFile(true, "msg.html", embed_message_type_attachment); - - msg.timestamp = time(nullptr); - - success = smtp->send(msg); - - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - #endif - return success; - } - - // return string of languages and count - std::string System::languages_string() { - std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; - for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { - languages_string += languages[i]; - if (i != NUM_LANGUAGES - 1) { - languages_string += ","; - } - } - languages_string += ")"; - return languages_string; - } - - // returns last response from MQTT - bool System::command_response(const char * value, const int8_t id, JsonObject output) { - JsonDocument doc; - if (DeserializationError::Ok == deserializeJson(doc, Mqtt::get_response())) { - for (JsonPair p : doc.as()) { - output[p.key()] = p.value(); - } - } else { - output["response"] = Mqtt::get_response(); - } - return true; - } - - // fetch device values - bool System::command_fetch(const char * value, const int8_t id) { - std::string value_s; - if (Helpers::value2string(value, value_s)) { - if (value_s == "all") { - LOG_INFO("Requesting data from EMS devices"); - EMSESP::fetch_device_values(); - } else if (value_s == F_(boiler)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::BOILER); - } else if (value_s == F_(thermostat)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::THERMOSTAT); - } else if (value_s == F_(solar)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::SOLAR); - } else if (value_s == F_(mixer)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::MIXER); - } - } else { - EMSESP::fetch_device_values(); // default if no name or id is given - } - - return true; // always true - } - - // mqtt publish - bool System::command_publish(const char * value, const int8_t id) { - std::string value_s; - if (Helpers::value2string(value, value_s)) { - if (value_s == "ha") { - EMSESP::publish_all(true); // includes HA - LOG_INFO("Publishing all data to MQTT, including HA configs"); - return true; - } else if (value_s == (F_(boiler))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER); - return true; - } else if (value_s == (F_(thermostat))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT); - return true; - } else if (value_s == (F_(solar))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR); - return true; - } else if (value_s == (F_(mixer))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER); - return true; - } else if (value_s == (F_(water))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::WATER); - return true; - } else if (value_s == "other") { - EMSESP::publish_other_values(); // switch and heat pump - return true; - } else if ((value_s == (F_(temperaturesensor))) || (value_s == (F_(analogsensor)))) { - EMSESP::publish_sensor_values(true); - return true; - } - } - - LOG_INFO("Publishing all data to MQTT"); - EMSESP::publish_all(); - - return true; - } - - // syslog level - // commenting this out - don't see the point on having an API service to change the syslog level - /* - bool System::command_syslog_level(const char * value, const int8_t id) { - uint8_t s = 0xff; - if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { - bool changed = false; - EMSESP::webSettingsService.update( - [&](WebSettings & settings) { - if (settings.syslog_level != (int8_t)s - 1) { - settings.syslog_level = (int8_t)s - 1; - changed = true; - } - return StateUpdateResult::CHANGED; - }); - if (changed) { - EMSESP::system_.syslog_init(); - } - return true; - } - return false; - } - */ - - // send message - to system log and MQTT - bool System::command_message(const char * value, const int8_t id, JsonObject output) { - if (value == nullptr || value[0] == '\0') { - LOG_WARNING("Message is empty"); - return false; // must have a string value - } - - EMSESP::webSchedulerService.computed_value.clear(); - EMSESP::webSchedulerService.raw_value = value; - for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) { - delay(1); - } - - if (EMSESP::webSchedulerService.computed_value.empty()) { - LOG_WARNING("Message result is empty"); - return false; - } - - LOG_INFO("Message: %s", EMSESP::webSchedulerService.computed_value.c_str()); // send to log - Mqtt::queue_publish(F_(message), EMSESP::webSchedulerService.computed_value); // send to MQTT if enabled - output["api_data"] = EMSESP::webSchedulerService.computed_value; // send to API - - return true; - } - - // watch - bool System::command_watch(const char * value, const int8_t id) { - uint8_t w = 0xff; - uint16_t i = Helpers::hextoint(value); - if (Helpers::value2enum(value, w, FL_(list_watch))) { - if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { - EMSESP::watch_id(0); - } - if (Mqtt::publish_single() && w != EMSESP::watch()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); - } else { - Mqtt::queue_publish("system_data/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); - } - } - EMSESP::watch(w); - return true; - } else if (i) { - if (Mqtt::publish_single() && i != EMSESP::watch_id()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/watch", Helpers::hextoa(i)); - } else { - Mqtt::queue_publish("system_data/watch", Helpers::hextoa(i)); - } - } - EMSESP::watch_id(i); - if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { - EMSESP::watch(EMSESP::Watch::WATCH_ON); - } - return true; - } - return false; - } - - void System::store_nvs_values() { - if (Command::find_command(EMSdevice::DeviceType::BOILER, 0, "nompower", 0) != nullptr) { - Command::call(EMSdevice::DeviceType::BOILER, "nompower", "-1"); // trigger a write - } - EMSESP::analogsensor_.store_counters(); - EMSESP::nvs_.end(); - } - - // Build up a list of all partitions and their version info - void System::get_partition_info() { - partition_info_.clear(); // clear existing data - - #ifdef EMSESP_STANDALONE - // dummy data for standalone mode - version, size, install_date - partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""}; - partition_info_["app1"] = {"", 0, ""}; - partition_info_["factory"] = {"", 0, ""}; - partition_info_["boot"] = {"", 0, ""}; - #else - - auto current_partition = (const char *)esp_ota_get_running_partition()->label; - - // update the current version and partition name in NVS if not already set - if (EMSESP::nvs_.getString(current_partition) != EMSESP_APP_VERSION || emsesp::EMSESP::nvs_.getBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true)) { - EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, false); - EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); - char c[20]; - snprintf(c, sizeof(c), "d_%s", current_partition); - auto t = time(nullptr); - // write timestamp always with new version, if clock is not set, this will be updated with ntp - EMSESP::nvs_.putULong(c, t); - } - - // Loop through all available partitions and update map with the version info pulled from NVS - // Partitions can be app0, app1, factory, boot - esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); - uint64_t buffer; - - while (it != nullptr) { - bool is_valid = true; - const esp_partition_t * part = esp_partition_get(it); - - if (part->label != nullptr && part->label[0] != '\0') { - // check if partition is valid and not empty - esp_partition_read(part, 0, &buffer, 8); - if (buffer == 0xFFFFFFFFFFFFFFFF) { - is_valid = false; // skip this partition - } - } - - // get the version from the NVS store, and add to map - if (is_valid) { - PartitionInfo p_info; - // if there is an entry for this partition in NVS, get it's version from NVS - p_info.version = EMSESP::nvs_.getString(part->label, "").c_str(); - char c[20]; - snprintf(c, sizeof(c), "d_%s", (const char *)part->label); - time_t d = EMSESP::nvs_.getULong(c, 0); - char time_string[25]; - strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); - p_info.install_date = d > 1500000000L ? time_string : ""; - - esp_image_metadata_t meta = {}; - esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; - if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { - p_info.size = meta.image_len / 1024; // actual firmware size in KB - } else { - p_info.size = 0; - } - - partition_info_[part->label] = p_info; - } - - it = esp_partition_next(it); // loop to next partition - } - esp_partition_iterator_release(it); - #endif - } - - // set NTP install time/date for the current partition - // assumes NTP is connected and working - void System::set_partition_install_date() { - #ifndef EMSESP_STANDALONE - auto current_partition = (const char *)esp_ota_get_running_partition()->label; - if (current_partition == nullptr) { - return; // fail-safe - } - - char c[20]; - snprintf(c, sizeof(c), "d_%s", current_partition); - time_t d = EMSESP::nvs_.getULong(c, 0); - if (d < 1500000000L) { - LOG_DEBUG("Setting the install date in partition %s", current_partition); - auto t = time(nullptr) - uuid::get_uptime_sec(); - EMSESP::nvs_.putULong(c, t); - } - #endif - } - - // sets the partition to use on the next restart - bool System::set_partition(const char * partitionname) { - #ifdef EMSESP_STANDALONE - return true; - #else - if (partitionname == nullptr) { - return false; - } - - // Find the partition by label - esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, partitionname); - if (it == nullptr) { - return false; // partition not found - } - - const esp_partition_t * partition = esp_partition_get(it); - esp_partition_iterator_release(it); - - if (partition == nullptr) { - return false; - } - - // Set the boot partition - esp_err_t err = esp_ota_set_boot_partition(partition); - if (err != ESP_OK) { - return false; - } - - // initiate the restart - EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); - return true; - #endif - } - - // restart EMS-ESP - // app0 or app1, or boot/factory on 16MB boards - void System::system_restart(const char * partitionname) { - // see if we are forcing a partition to use - if (partitionname != nullptr) { - #ifndef EMSESP_STANDALONE - // Factory partition - label will be "factory" - const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); - if (partition && !strcmp(partition->label, partitionname)) { - esp_ota_set_boot_partition(partition); - } else - // try and find the partition by name - if (strcmp(esp_ota_get_running_partition()->label, partitionname)) { - // not found, get next one in cycle - partition = esp_ota_get_next_update_partition(nullptr); - if (!partition) { - LOG_ERROR("Partition '%s' not found", partitionname); - return; - } - if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { - partition = esp_ota_get_next_update_partition(partition); - if (!partition || strcmp(partition->label, partitionname)) { - LOG_ERROR("Partition '%s' not found", partitionname); - return; - } - } - // error if partition is empty - uint64_t buffer; - esp_partition_read(partition, 0, &buffer, 8); - if (buffer == 0xFFFFFFFFFFFFFFFF) { - LOG_ERROR("Partition '%s' is empty, not bootable", partition->label); - return; - } - // set the boot partition - esp_ota_set_boot_partition(partition); - } - #endif - LOG_INFO("Restarting EMS-ESP from %s partition", partitionname); - } else { - LOG_INFO("Restarting EMS-ESP..."); - } - - store_nvs_values(); // save any NVS values - - // flush all the log - EMSESP::webLogService.loop(); // dump all to web log - for (int i = 0; i < 10; i++) { - Shell::loop_all(); - delay(10); // give telnet TCP stack time to transmit - } - Serial.flush(); // wait for hardware TX buffer to drain - - Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 - EMSuart::stop(); // stop UART so there is no interference - #ifndef EMSESP_STANDALONE - delay(1000); // wait 1 second - ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called - #endif - } - - // saves all settings - void System::wifi_reconnect() { - EMSESP::esp32React.getNetworkSettingsService()->read( - [](NetworkSettings & networkSettings) { LOG_INFO("WiFi reconnecting to SSID '%s'...", networkSettings.ssid.c_str()); }); - delay(500); // wait - EMSESP::webSettingsService.save(); // save local settings - EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password - } - - void System::syslog_init() { - EMSESP::webSettingsService.read([&](WebSettings & settings) { - syslog_enabled_ = settings.syslog_enabled; - syslog_level_ = settings.syslog_level; - syslog_mark_interval_ = settings.syslog_mark_interval; - syslog_host_ = settings.syslog_host; - syslog_port_ = settings.syslog_port; - }); - #ifndef EMSESP_STANDALONE - if (syslog_enabled_) { - // start & configure syslog - syslog_.maximum_log_messages(10); - syslog_.log_level((uuid::log::Level)syslog_level_); - syslog_.mark_interval(syslog_mark_interval_); - syslog_.destination(syslog_host_.c_str(), syslog_port_); - syslog_.hostname(hostname()); - EMSESP::logger().info("Starting Syslog service"); - } else if (syslog_.started()) { - // in case service is still running, this flushes the queue - // https://github.com/emsesp/EMS-ESP/issues/496 - EMSESP::logger().info("Stopping Syslog"); - syslog_.loop(); - syslog_.log_level(uuid::log::Level::OFF); // stop server - syslog_.mark_interval(0); - // syslog_.destination(""); - } - if (Mqtt::publish_single()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); - if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { - Mqtt::queue_publish("system/watch", - EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); - } else { - Mqtt::queue_publish("system/watch", Helpers::hextoa(EMSESP::watch_id())); - } - - } else { - Mqtt::queue_publish("system_data/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); - if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { - Mqtt::queue_publish("system_data/watch", - EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); - } else { - Mqtt::queue_publish("system_data/watch", Helpers::hextoa(EMSESP::watch_id())); - } - } - } - #endif - } - - // read specific major system settings to store locally for faster access - void System::store_settings(WebSettings & settings) { - version_ = settings.version; - - rx_gpio_ = settings.rx_gpio; - tx_gpio_ = settings.tx_gpio; - pbutton_gpio_ = settings.pbutton_gpio; - dallas_gpio_ = settings.dallas_gpio; - led_gpio_ = settings.led_gpio; - - analog_enabled_ = settings.analog_enabled; - low_clock_ = settings.low_clock; - hide_led_ = settings.hide_led; - led_type_ = settings.led_type; - board_profile_ = settings.board_profile; - telnet_enabled_ = settings.telnet_enabled; - - tx_mode_ = settings.tx_mode; - syslog_enabled_ = settings.syslog_enabled; - syslog_level_ = settings.syslog_level; - syslog_mark_interval_ = settings.syslog_mark_interval; - syslog_host_ = settings.syslog_host; - syslog_port_ = settings.syslog_port; - - fahrenheit_ = settings.fahrenheit; - bool_format_ = settings.bool_format; - bool_dashboard_ = settings.bool_dashboard; - enum_format_ = settings.enum_format; - readonly_mode_ = settings.readonly_mode; - - phy_type_ = settings.phy_type; - eth_power_ = settings.eth_power; - eth_phy_addr_ = settings.eth_phy_addr; - eth_clock_mode_ = settings.eth_clock_mode; - - locale_ = settings.locale; - developer_mode_ = settings.developer_mode; - // start services - if (settings.modbus_enabled) { - if (EMSESP::modbus_ == nullptr) { - EMSESP::modbus_ = new Modbus; - EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); - } else if (settings.modbus_port != modbus_port_ || settings.modbus_max_clients != modbus_max_clients_ || settings.modbus_timeout != modbus_timeout_) { - EMSESP::modbus_->stop(); - EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); - } - } else if (EMSESP::modbus_ != nullptr) { - EMSESP::modbus_->stop(); - delete EMSESP::modbus_; - EMSESP::modbus_ = nullptr; - } - modbus_enabled_ = settings.modbus_enabled; - modbus_port_ = settings.modbus_port; - modbus_max_clients_ = settings.modbus_max_clients; - modbus_timeout_ = settings.modbus_timeout; - } - - // Starts up core services - void System::start() { - get_partition_info(); // get the partition info - - #ifndef EMSESP_STANDALONE - // disable bluetooth module - // periph_module_disable(PERIPH_BT_MODULE); - if (low_clock_) { - #if CONFIG_IDF_TARGET_ESP32C3 - setCpuFrequencyMhz(80); - #else - setCpuFrequencyMhz(160); - #endif - } - - // get current memory values - fstotal_ = LittleFS.totalBytes() / 1024; // read only once, it takes 500 ms to read - appused_ = ESP.getSketchSize() / 1024; - appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; - refreshHeapMem(); // refresh free heap and max alloc heap - #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); - temperature_sensor_install(&temp_sensor_config, &temperature_handle_); - temperature_sensor_enable(temperature_handle_); - temperature_sensor_get_celsius(temperature_handle_, &temperature_); - #endif - #endif - - EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { - hostname(networkSettings.hostname.c_str()); // sets the hostname - }); - - commands_init(); // console & api commands - led_init(); // init LED - button_init(); // button - network_init(); // network - uart_init(); // start UART - syslog_init(); // start syslog - } - - // button single click - void System::button_OnClick(PButton & b) { - LOG_NOTICE("Button pressed - single click"); - - #if defined(EMSESP_TEST) - #ifndef EMSESP_STANDALONE - // show filesystem - Test::listDir(LittleFS, "/", 3); - #endif - #endif - } - - // button double click - void System::button_OnDblClick(PButton & b) { - LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); - // set AP mode to always so will join AP if wifi ssid fails to connect - EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { - apSettings.provisionMode = AP_MODE_ALWAYS; - return StateUpdateResult::CHANGED; - }); - // remove SSID from network settings - EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) { - networkSettings.ssid = ""; - return StateUpdateResult::CHANGED; - }); - EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password - } - - // LED flash every 100ms - void System::led_flash() { - static bool led_flash_state_ = false; - static uint32_t last_toggle_time_ = 0; - uint32_t current_time = uuid::get_uptime(); - - if (current_time - last_toggle_time_ >= 100) { // every 100ms - led_flash_state_ = !led_flash_state_; - last_toggle_time_ = current_time; - - if (led_flash_type_) { - uint8_t intensity = led_flash_state_ ? RGB_LED_BRIGHTNESS : 0; - EMSESP_RGB_WRITE(led_flash_gpio_, intensity, intensity, 0); // RGB LED - Yellow - } else { - digitalWrite(led_flash_gpio_, led_flash_state_ ? LED_ON : !LED_ON); // Standard LED - } - } - - // after duration, turn off the LED - if (current_time - led_flash_start_time_ >= led_flash_duration_) { - if (led_flash_type_) { - EMSESP_RGB_WRITE(led_flash_gpio_, 0, 0, 0); - } else { - digitalWrite(led_flash_gpio_, !LED_ON); - } - led_flash_timer_ = false; - command_format(nullptr, 0); // Execute format operation - } - } - - // Start the LED flash timer - duration in seconds - void System::start_led_flash(uint8_t duration) { - // Don't start if already running - if (led_flash_timer_) { - return; - } - - // Get LED settings - EMSESP::webSettingsService.read([&](WebSettings & settings) { - led_flash_type_ = settings.led_type; - led_flash_gpio_ = settings.led_gpio; - }); - - // Reset counter and state - led_flash_start_time_ = uuid::get_uptime(); // current time - led_flash_duration_ = duration * 1000; // duration in milliseconds - led_flash_timer_ = true; // it's active - } - - // button long press - void System::button_OnLongPress(PButton & b) { - LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); - EMSESP::system_.system_restart("boot"); - } - - // button indefinite press - void System::button_OnVLongPress(PButton & b) { - LOG_NOTICE("Button pressed - very long press - perform factory reset"); - start_led_flash(5); // Start LED flash timer for 5 seconds - } - - // push button - void System::button_init() { - #ifndef EMSESP_STANDALONE - if (!myPButton_.init(pbutton_gpio_, HIGH)) { - LOG_WARNING("Multi-functional button not detected"); - return; - } - LOG_DEBUG("Multi-functional button enabled"); - - myPButton_.onClick(BUTTON_Debounce, button_OnClick); - myPButton_.onDblClick(BUTTON_DblClickDelay, button_OnDblClick); - myPButton_.onLongPress(BUTTON_LongPressDelay, button_OnLongPress); - myPButton_.onVLongPress(BUTTON_VLongPressDelay, button_OnVLongPress); - #endif - } - - // set the LED to on or off when in normal operating mode - void System::led_init() { - // disabled old led port before setting new one - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - - if ((led_gpio_)) { // 0 means disabled - if (led_type_) { - // rgb LED WS2812B, use Neopixel - EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0); - } else { - pinMode(led_gpio_, OUTPUT); - digitalWrite(led_gpio_, !LED_ON); // start with LED off - } - } else { - LOG_INFO("LED disabled"); - } - } - - void System::uart_init() { - EMSuart::stop(); - EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked - EMSESP::txservice_.start(); // reset counters and send devices request - } - - // checks system health and handles LED flashing wizardry - // returns true if the LED flash is active - bool System::loop() { - // check if we're supposed to do a reset/restart - if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED) { - system_restart(); - } - - // if LED flashing is active, run the LED flash - if (led_flash_timer_) { - led_flash(); - return true; // is active - } - - led_monitor(); // check status and report back using the LED - myPButton_.check(); // check button press - system_check(); // check system health - - // syslog - #ifndef EMSESP_STANDALONE - if (syslog_enabled_) { - syslog_.loop(); - } - #endif - - send_info_mqtt(); - - return false; // LED flashing is not active - } - - // send MQTT info topic appended with the version information as JSON, as a retained flag - // this is only done once when the connection is established - void System::send_info_mqtt() { - static uint8_t _connection = 0; - uint8_t connection = (ethernet_connected() ? 1 : 0) + ((WiFi.status() == WL_CONNECTED) ? 2 : 0) + (ntp_connected_ ? 4 : 0) + (has_ipv6_ ? 8 : 0); - // check if connection status has changed - if (!Mqtt::connected() || connection == _connection) { - return; - } - _connection = connection; - JsonDocument doc; - // doc["event"] = "connected"; - doc["version"] = EMSESP_APP_VERSION; - - // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) - // https://github.com/emsesp/EMS-ESP32/issues/751 - if (ntp_connected_) { - char time_string[25]; - time_t now = time(nullptr) - uuid::get_uptime_sec(); - strftime(time_string, 25, "%FT%T%z", localtime(&now)); - doc["bootTime"] = time_string; - } - - #ifndef EMSESP_STANDALONE - if (EMSESP::system_.ethernet_connected()) { - doc["network"] = "ethernet"; - doc["hostname"] = ETH.getHostname(); - /* - doc["MAC"] = ETH.macAddress(); - doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); - doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP()); - doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP()); - if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { - doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); - } - */ - - } else if (WiFi.status() == WL_CONNECTED) { - doc["network"] = "wifi"; - doc["hostname"] = WiFi.getHostname(); - doc["SSID"] = WiFi.SSID(); - doc["BSSID"] = WiFi.BSSIDstr(); - doc["MAC"] = WiFi.macAddress(); - doc["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask()); - doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); - doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); - - if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { - doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); - } - } - #endif - Mqtt::queue_publish_retain(F_(info), doc.as()); // topic called "info" and it's Retained - } - - // create the json for heartbeat - void System::heartbeat_json(JsonObject output) { - switch (EMSESP::bus_status()) { - case EMSESP::BUS_STATUS_OFFLINE: - output["bus_status"] = "connecting"; // EMS-ESP is booting... - break; - case EMSESP::BUS_STATUS_TX_ERRORS: - output["bus_status"] = "txerror"; - break; - case EMSESP::BUS_STATUS_CONNECTED: - output["bus_status"] = "connected"; - break; - default: - output["bus_status"] = "disconnected"; - break; - } - - output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); - output["uptime_sec"] = uuid::get_uptime_sec(); - - output["rxreceived"] = EMSESP::rxservice_.telegram_count(); - output["rxfails"] = EMSESP::rxservice_.telegram_error_count(); - output["txreads"] = EMSESP::txservice_.telegram_read_count(); - output["txwrites"] = EMSESP::txservice_.telegram_write_count(); - output["txfails"] = EMSESP::txservice_.telegram_read_fail_count() + EMSESP::txservice_.telegram_write_fail_count(); - - if (Mqtt::enabled()) { - output["mqttcount"] = Mqtt::publish_count(); - output["mqttfails"] = Mqtt::publish_fails(); - output["mqttreconnects"] = Mqtt::connect_count(); - } - output["apicalls"] = WebAPIService::api_count(); // + WebAPIService::api_fails(); - output["apifails"] = WebAPIService::api_fails(); - - if (EMSESP::sensor_enabled() || EMSESP::analog_enabled()) { - output["sensorreads"] = EMSESP::temperaturesensor_.reads() + EMSESP::analogsensor_.reads(); - output["sensorfails"] = EMSESP::temperaturesensor_.fails() + EMSESP::analogsensor_.fails(); - } - - #ifndef EMSESP_STANDALONE - output["freemem"] = getHeapMem(); - output["max_alloc"] = getMaxAllocMem(); - #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - output["temperature"] = (int)temperature_; - #endif - #endif - - #ifndef EMSESP_STANDALONE - if (!ethernet_connected_) { - int8_t rssi = WiFi.RSSI(); - output["rssi"] = rssi; - output["wifistrength"] = wifi_quality(rssi); - output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects(); - } - #endif - } - - // send periodic MQTT message with system information - void System::send_heartbeat() { - refreshHeapMem(); // refresh free heap and max alloc heap - - JsonDocument doc; - JsonObject json = doc.to(); - - heartbeat_json(json); - Mqtt::queue_publish(F_(heartbeat), json); // send to MQTT with retain off. This will add to MQTT queue. - } - - // initializes network - void System::network_init() { - last_system_check_ = 0; // force the LED to go from fast flash to pulse - - #if CONFIG_IDF_TARGET_ESP32 - bool disableEth; - EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { disableEth = settings.ssid.length() > 0; }); - - // no ethernet present or disabled - if (phy_type_ == PHY_type::PHY_TYPE_NONE || disableEth) { - return; - } // no ethernet present - - // configure Ethernet - int mdc = 23; // Pin# of the I²C clock signal for the Ethernet PHY - hardcoded - int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded - uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) - int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) - eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 - : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 - : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) - // clock mode: - // ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0 - // ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0 - // ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16 - // ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock - auto clock_mode = (eth_clock_mode_t)eth_clock_mode_; - - // reset power and add a delay as ETH doesn't not always start up correctly after a warm boot - if (eth_power_ != -1) { - pinMode(eth_power_, OUTPUT); - digitalWrite(eth_power_, LOW); - delay(500); - digitalWrite(eth_power_, HIGH); - } - eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); - #endif - } - - // check health of system, done every 5 seconds - void System::system_check() { - uint32_t current_uptime = uuid::get_uptime(); - if (!last_system_check_ || ((uint32_t)(current_uptime - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) { - last_system_check_ = current_uptime; - - #ifndef EMSESP_STANDALONE - #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - temperature_sensor_get_celsius(temperature_handle_, &temperature_); - #endif - #endif - - #ifdef EMSESP_PINGTEST - static uint64_t ping_count = 0; - LOG_NOTICE("Ping test, #%d", ping_count++); - #endif - - // check if we have a valid network connection - if (!ethernet_connected() && (WiFi.status() != WL_CONNECTED)) { - healthcheck_ |= HEALTHCHECK_NO_NETWORK; - } else { - healthcheck_ &= ~HEALTHCHECK_NO_NETWORK; - } - - // check if we have a bus connection - if (!EMSbus::bus_connected()) { - healthcheck_ |= HEALTHCHECK_NO_BUS; - } else { - healthcheck_ &= ~HEALTHCHECK_NO_BUS; - } - - // see if the healthcheck state has changed - static uint8_t last_healthcheck_ = 0; - if (healthcheck_ != last_healthcheck_) { - last_healthcheck_ = healthcheck_; - - EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected - - // see if we're better now - if (healthcheck_ == 0) { - // everything is healthy, show LED permanently on or off depending on setting - // Green on RGB LED, on/off on standard LED - if (led_gpio_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) - : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // Green - } - } else { - // turn off LED so we're ready for the warning flashes - if (led_gpio_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - } - } - } - } - } - - // commands - takes static function pointers - // can be called via Console using 'call system ' - void System::commands_init() { - Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); - #if defined(EMSESP_TEST) - Command::add(EMSdevice::DeviceType::SYSTEM, ("test"), System::command_test, FL_(test_cmd)); - #endif - - // these commands will return data in JSON format - Command::add(EMSdevice::DeviceType::SYSTEM, F("response"), System::command_response, FL_(commands_response)); - - // MQTT subscribe "ems-esp/system/#" - Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback - } - - // uses LED to show system health - void System::led_monitor() { - // if button is pressed, show LED (yellow on RGB LED, on/off on standard LED) - static bool button_busy_ = false; - if (button_busy_ != myPButton_.button_busy()) { - button_busy_ = myPButton_.button_busy(); - if (led_type_) { - EMSESP_RGB_WRITE(led_gpio_, button_busy_ ? RGB_LED_BRIGHTNESS : 0, button_busy_ ? RGB_LED_BRIGHTNESS : 0, 0); // Yellow - } else { - digitalWrite(led_gpio_, button_busy_ ? LED_ON : !LED_ON); - } - } - - // we only need to run the LED healthcheck if there are errors - // skip if we're in the led_flash_timer or if a button has been pressed - if (!healthcheck_ || !led_gpio_ || button_busy_ || led_flash_timer_) { - return; // all good - } - - static uint32_t led_long_timer_ = 1; // 1 will kick it off immediately - static uint32_t led_short_timer_ = 0; - static uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer - - auto current_time = uuid::get_uptime(); - - // first long pause before we start flashing - if (led_long_timer_ && (uint32_t)(current_time - led_long_timer_) >= HEALTHCHECK_LED_LONG_DUARATION) { - led_short_timer_ = current_time; // start the short timer - led_long_timer_ = 0; // stop long timer - led_flash_step_ = 1; // enable the short flash timer - } - - // the flash timer which starts after the long pause - if (led_flash_step_ && (uint32_t)(current_time - led_short_timer_) >= HEALTHCHECK_LED_FLASH_DUARATION) { - led_long_timer_ = 0; // stop the long timer - led_short_timer_ = current_time; - static bool led_on_ = false; - - if (++led_flash_step_ == 8) { - // reset the whole sequence - led_long_timer_ = uuid::get_uptime(); - led_flash_step_ = 0; - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); // LED off - } else if (led_flash_step_ % 2) { - // handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED - // 1 flash (blue) is the EMS bus is not connected - // 2 flashes (red, red) if the network (wifi or ethernet) is not connected - // 3 flashes (red, red, blue) is both the bus and the network are not connected - bool no_network = (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK; - bool no_bus = (healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS; - - if (led_type_) { - if (led_flash_step_ == 3) { - if (no_network) { - EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red - } else if (no_bus) { - EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue - } - } - if (led_flash_step_ == 5 && no_network) { - EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red - } - if ((led_flash_step_ == 7) && no_network && no_bus) { - EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue - } - } else { - if ((led_flash_step_ == 3) && (no_network || no_bus)) { - led_on_ = true; - } - - if ((led_flash_step_ == 5) && no_network) { - led_on_ = true; - } - - if ((led_flash_step_ == 7) && no_network && no_bus) { - led_on_ = true; - } - - if (led_on_) { - digitalWrite(led_gpio_, LED_ON); // LED on - } - } - } else { - // turn the led off after the flash, on even number count - if (led_on_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - led_on_ = false; - } - } - } - } - - // Return the quality (Received Signal Strength Indicator) of the WiFi network as a % - // High quality: 90% ~= -55dBm - // Medium quality: 50% ~= -75dBm - // Low quality: 30% ~= -85dBm - // Unusable quality: 8% ~= -96dBm - int8_t System::wifi_quality(int8_t dBm) { - if (dBm <= -100) { - return 0; - } - - if (dBm >= -50) { - return 100; - } - return 2 * (dBm + 100); - } - - // print users to console - void System::show_users(uuid::console::Shell & shell) { - if (!shell.has_flags(CommandFlags::ADMIN)) { - shell.printfln("Unauthorized. You need to be an admin to view users."); - return; - } - - shell.printfln("Users:"); - - #ifndef EMSESP_STANDALONE - EMSESP::esp32React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { - for (const User & user : securitySettings.users) { - shell.printfln(" username: %s, password: %s, is_admin: %s", user.username.c_str(), user.password.c_str(), user.admin ? ("yes") : ("no")); - } - }); - #endif - - shell.println(); - } - - // shell command 'show system' - void System::show_system(uuid::console::Shell & shell) { - refreshHeapMem(); // refresh free heap and max alloc heap - - shell.println(); - shell.println("System:"); - shell.printfln(" Version: %s", EMSESP_APP_VERSION); - #ifndef EMSESP_STANDALONE - shell.printfln(" Platform: %s (%s)", EMSESP_PLATFORM, ESP.getChipModel()); - shell.printfln(" Model: %s", getBBQKeesGatewayDetails().c_str()); - #endif - shell.printfln(" Language: %s", locale().c_str()); - shell.printfln(" Board profile: %s", board_profile().c_str()); - shell.printfln(" Uptime: %s", uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); - #ifndef EMSESP_STANDALONE - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/misc_system_api.html - unsigned char mac_base[6] = {0}; - esp_efuse_mac_get_default(mac_base); - esp_read_mac(mac_base, ESP_MAC_WIFI_STA); - shell.printfln(" Base MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]); - - shell.printfln(" SDK version: %s", ESP.getSdkVersion()); - shell.printfln(" CPU frequency: %lu MHz", ESP.getCpuFreqMHz()); - #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - shell.printfln(" CPU temperature: %d °C", (int)temperature()); - #endif - shell.printfln(" Free heap/Max alloc: %lu KB / %lu KB", getHeapMem(), getMaxAllocMem()); - shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree()); - uint32_t FSused = LittleFS.usedBytes() / 1024; - shell.printfln(" FS used/free: %lu KB / %lu KB", FSused, FStotal() - FSused); - shell.printfln(" Flash size: %lu KB", ESP.getFlashChipSize() / 1024); - if (PSram()) { - shell.printfln(" PSRAM size/free: %lu KB / %lu KB", PSram(), ESP.getFreePsram() / 1024); - } else { - shell.printfln(" PSRAM: not available"); - } - // GPIOs - shell.println(" GPIOs:"); - shell.printf(" allowed:"); - for (const auto & gpio : valid_system_gpios_) { - shell.printf(" %d", gpio); - } - shell.printfln(" [total %d]", valid_system_gpios_.size()); - shell.printf(" in use:"); - auto sorted_gpios = used_gpios_; - std::sort(sorted_gpios.begin(), sorted_gpios.end(), [](const GpioUsage & a, const GpioUsage & b) { return a.pin < b.pin; }); - for (const auto & gpio : sorted_gpios) { - shell.printf(" %d(%s)", gpio.pin, gpio.source.c_str()); - } - shell.printfln(" [total %d]", used_gpios_.size()); - auto available = available_gpios(); - shell.printf(" available:"); - for (const auto & gpio : available) { - shell.printf(" %d", gpio); - } - shell.printfln(" [total %d]", available.size()); - // List all partitions and their version info - shell.println(" Partitions:"); - for (const auto & partition : partition_info_) { - if (partition.second.version.empty()) { - continue; // no version, empty string - } - shell.printfln(" %s: v%s (%d KB%s) %s", - partition.first.c_str(), - partition.second.version.c_str(), - partition.second.size, - partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(), - (strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : ""); - } - - shell.println(); - shell.println("Network:"); - switch (WiFi.status()) { - case WL_IDLE_STATUS: - shell.printfln(" Status: Idle"); - break; - - case WL_NO_SSID_AVAIL: - shell.printfln(" Status: Network not found"); - break; - - case WL_SCAN_COMPLETED: - shell.printfln(" Status: Network scan complete"); - break; - - case WL_CONNECTED: - shell.printfln(" Status: WiFi connected"); - shell.printfln(" SSID: %s", WiFi.SSID().c_str()); - shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); - shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); - char result[10]; - shell.printfln(" TxPower: %s dBm", Helpers::render_value(result, (double)(WiFi.getTxPower() / 4), 1)); - shell.printfln(" MAC address: %s", WiFi.macAddress().c_str()); - shell.printfln(" Hostname: %s", WiFi.getHostname()); - shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); - shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); - shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); - if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); - } - break; - - case WL_CONNECT_FAILED: - shell.printfln(" WiFi Network: Connection failed"); - break; - - case WL_CONNECTION_LOST: - shell.printfln(" WiFi Network: Connection lost"); - break; - - case WL_DISCONNECTED: - shell.printfln(" WiFi Network: Disconnected"); - break; - - // case WL_NO_SHIELD: - default: - shell.printfln(" WiFi MAC address: %s", WiFi.macAddress().c_str()); - shell.printfln(" WiFi Network: not connected"); - break; - } - - // show Ethernet if connected - if (ethernet_connected_) { - shell.println(); - shell.printfln(" Ethernet Status: connected"); - shell.printfln(" Ethernet MAC address: %s", ETH.macAddress().c_str()); - shell.printfln(" Hostname: %s", ETH.getHostname()); - shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); - shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); - shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); - if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); - } - } - shell.println(); - - shell.println("Syslog:"); - if (!syslog_enabled_) { - shell.printfln(" Syslog: disabled"); - } else { - shell.printfln(" Syslog: %s", syslog_.started() ? "started" : "stopped"); - shell.print(" "); - shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : F_(unset)); - shell.printfln(" IP: %s", uuid::printable_to_string(syslog_.ip()).c_str()); - shell.print(" "); - shell.printfln(F_(port_fmt), syslog_port_); - shell.print(" "); - shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(syslog_level_))); - shell.print(" "); - shell.printfln(F_(mark_interval_fmt), syslog_mark_interval_); - shell.printfln(" Queued: %d", syslog_.queued()); - } - - shell.println(); - #endif - } - - // see if there is a restore of an older settings file that needs to be applied - // note there can be only one file at a time - bool System::check_restore() { - bool reboot_required = false; // true if we need to reboot - - #ifndef EMSESP_STANDALONE - File new_file = LittleFS.open(TEMP_FILENAME_PATH); - if (new_file) { - JsonDocument jsonDocument; - DeserializationError error = deserializeJson(jsonDocument, new_file); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject input = jsonDocument.as(); - // see what type of file it is, either settings or customization. anything else is ignored - std::string settings_type = input["type"]; - LOG_INFO("Restoring '%s' settings...", settings_type.c_str()); - - // system backup, which is a consolidated json object with all the settings files - if (settings_type == "systembackup") { - reboot_required = true; - JsonArray sections = input["systembackup"].as(); - for (JsonObject section : sections) { - std::string section_type = section["type"]; - LOG_DEBUG("Restoring '%s' section...", section_type.c_str()); - if (section_type == "settings") { - saveSettings(NETWORK_SETTINGS_FILE, section); - saveSettings(AP_SETTINGS_FILE, section); - saveSettings(MQTT_SETTINGS_FILE, section); - saveSettings(NTP_SETTINGS_FILE, section); - saveSettings(SECURITY_SETTINGS_FILE, section); - saveSettings(EMSESP_SETTINGS_FILE, section); - } - if (section_type == "schedule") { - saveSettings(EMSESP_SCHEDULER_FILE, section); - } - if (section_type == "customizations") { - saveSettings(EMSESP_CUSTOMIZATION_FILE, section); - } - if (section_type == "entities") { - saveSettings(EMSESP_CUSTOMENTITY_FILE, section); - } - if (section_type == "modules") { - saveSettings(EMSESP_MODULES_FILE, section); - } - if (section_type == "customSupport") { - // it's a custom support, extract json and write to /config/customSupport.json file - File customSupportFile = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "w"); - if (customSupportFile) { - serializeJson(section, customSupportFile); - customSupportFile.close(); - LOG_INFO("Custom support file updated"); - } else { - LOG_ERROR("Failed to save custom support file"); - } - } - - if (section_type == "nvs") { - // Restore NVS values - JsonArray nvs_entries = section["nvs"].as(); - for (JsonObject entry : nvs_entries) { - std::string key = entry["key"] | ""; - int type = entry["type"] | NVS_TYPE_ANY; - - switch (type) { - case NVS_TYPE_I8: - if (entry["value"].is()) { - int8_t v = entry["value"]; - EMSESP::nvs_.putChar(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_U8: - if (entry["value"].is()) { - uint8_t v = entry["value"]; - EMSESP::nvs_.putUChar(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_I32: - if (entry["value"].is()) { - int32_t v = entry["value"]; - EMSESP::nvs_.putInt(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_U32: - if (entry["value"].is()) { - uint32_t v = entry["value"]; - EMSESP::nvs_.putUInt(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_I64: - if (entry["value"].is()) { - int64_t v = entry["value"]; - EMSESP::nvs_.putLong64(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_U64: - if (entry["value"].is()) { - uint64_t v = entry["value"]; - EMSESP::nvs_.putULong64(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_BLOB: - // used for double values - if (entry["value"].is()) { - double v = entry["value"]; - EMSESP::nvs_.putDouble(key.c_str(), v); - LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); - } - break; - case NVS_TYPE_STR: - case NVS_TYPE_ANY: - default: - if (entry["value"].is()) { - std::string v = entry["value"]; - EMSESP::nvs_.putString(key.c_str(), v.c_str()); - LOG_DEBUG("Restored NVS value: %s = %s", key.c_str(), v.c_str()); - } - break; - } - } - } - } - } - - // It's a single settings file. Parse each section separately. If it's system related it will require a reboot - else if (settings_type == "settings") { - reboot_required = saveSettings(NETWORK_SETTINGS_FILE, input); - reboot_required |= saveSettings(AP_SETTINGS_FILE, input); - reboot_required |= saveSettings(MQTT_SETTINGS_FILE, input); - reboot_required |= saveSettings(NTP_SETTINGS_FILE, input); - reboot_required |= saveSettings(SECURITY_SETTINGS_FILE, input); - reboot_required |= saveSettings(EMSESP_SETTINGS_FILE, input); - } else if (settings_type == "customizations") { - saveSettings(EMSESP_CUSTOMIZATION_FILE, input); - } else if (settings_type == "schedule") { - saveSettings(EMSESP_SCHEDULER_FILE, input); - } else if (settings_type == "entities") { - saveSettings(EMSESP_CUSTOMENTITY_FILE, input); - } else if (settings_type == "customSupport") { - // it's a custom support file - save it to /config - new_file.close(); - if (LittleFS.rename(TEMP_FILENAME_PATH, EMSESP_CUSTOMSUPPORT_FILE)) { - LOG_INFO("Custom support file stored"); - return false; // no need to reboot - } else { - LOG_ERROR("Failed to save custom support file"); - } - } else { - LOG_ERROR("Unrecognized file uploaded"); - } - } else { - LOG_ERROR("Unrecognized file uploaded, not json."); - } - - // close (just in case) and remove the temp file - new_file.close(); - LittleFS.remove(TEMP_FILENAME_PATH); - } - #endif - - return reboot_required; - } - - // handle upgrades from previous versions - // this function will not be called on a clean install, with no settings files yet created - // returns true if we need a reboot - bool System::check_upgrade() { - bool missing_version = true; - std::string settingsVersion; - - // fetch current version from settings file - EMSESP::webSettingsService.read([&](WebSettings const & settings) { settingsVersion = settings.version.c_str(); }); - - // see if we're missing a version, will be < 3.5.0b13 from Dec 23 2022 - missing_version = (settingsVersion.empty() || (settingsVersion.length() < 5)); - if (missing_version) { - LOG_WARNING("No version information found. Assuming version 3.5.0"); - settingsVersion = "3.5.0"; // this was the last stable version without version info - } - - version::Semver200_version settings_version(settingsVersion); - version::Semver200_version this_version(EMSESP_APP_VERSION); - - std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease()); - std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease()); - bool save_version = true; - bool reboot_required = false; - - LOG_DEBUG("Checking for version upgrades from v%d.%d.%d%s", - settings_version.major(), - settings_version.minor(), - settings_version.patch(), - settings_version_type.c_str()); - - // compare versions - if (this_version > settings_version) { - // we need to do an upgrade - if (missing_version) { - LOG_NOTICE("Upgrading to version %d.%d.%d%s", this_version.major(), this_version.minor(), this_version.patch(), this_version_type.c_str()); - } else { - LOG_NOTICE("Upgrading from version %d.%d.%d%s to %d.%d.%d%s", - settings_version.major(), - settings_version.minor(), - settings_version.patch(), - settings_version_type.c_str(), - this_version.major(), - this_version.minor(), - this_version.patch(), - this_version_type.c_str()); - } - - // if we're coming from 3.4.4 or 3.5.0b14 which had no version stored then we need to apply new settings - if (missing_version) { - LOG_INFO("Upgrade: Setting MQTT Entity ID format to older v3.4 format (0)"); - EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & mqttSettings) { - mqttSettings.entity_format = Mqtt::entityFormat::SINGLE_LONG; // use old Entity ID format from v3.4 - return StateUpdateResult::CHANGED; - }); - } else if (settings_version.major() == 3 && settings_version.minor() <= 6) { - EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & mqttSettings) { - if (mqttSettings.entity_format == 1) { - mqttSettings.entity_format = Mqtt::entityFormat::SINGLE_OLD; // use old Entity ID format from v3.6 - LOG_INFO("Upgrade: Setting MQTT Entity ID format to v3.6 format (3)"); - return StateUpdateResult::CHANGED; - } else if (mqttSettings.entity_format == 2) { - mqttSettings.entity_format = Mqtt::entityFormat::MULTI_OLD; // use old Entity ID format from v3.6 - LOG_INFO("Upgrade: Setting MQTT Entity ID format to v3.6 format (4)"); - return StateUpdateResult::CHANGED; - } - return StateUpdateResult::UNCHANGED; - }); - } - - // changes pre < v3.7.0 - if (settings_version.major() == 3 && settings_version.minor() < 7) { - // network changes - // 1) WiFi Tx Power is now using the value * 4 (was 20) - // 2) WiFi sleep is now off by default (was on) - EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) { - auto changed = StateUpdateResult::UNCHANGED; - if (networkSettings.tx_power == 20) { - networkSettings.tx_power = WIFI_POWER_19_5dBm; // use 19.5 as we don't have 20 anymore - LOG_INFO("Upgrade: Setting WiFi TX Power to Auto"); - changed = StateUpdateResult::CHANGED; - } - if (networkSettings.nosleep != true) { - networkSettings.nosleep = true; - LOG_INFO("Upgrade: Disabling WiFi nosleep"); - changed = StateUpdateResult::CHANGED; - } - return changed; - }); - } - - // changes to application settings - EMSESP::webSettingsService.update([&](WebSettings & settings) { - // force web buffer to 25 for those boards without psram - if ((EMSESP::system_.PSram() == 0) && (settings.weblog_buffer != 25)) { - settings.weblog_buffer = 25; - return StateUpdateResult::CHANGED; - } - return StateUpdateResult::UNCHANGED; - }); - } else if (this_version < settings_version) { - // downgrading - LOG_NOTICE("Downgrading from version %d.%d.%d%s to version %d.%d.%d%s", - settings_version.major(), - settings_version.minor(), - settings_version.patch(), - settings_version_type.c_str(), - this_version.major(), - this_version.minor(), - this_version.patch(), - this_version_type.c_str()); - } else { - save_version = false; // same version, do nothing - } - - // if we did a change, set the new version and save it, no need to reboot - if (save_version) { - EMSESP::webSettingsService.update([&](WebSettings & settings) { - settings.version = EMSESP_APP_VERSION; - LOG_DEBUG("Upgrade: Setting version to %s", EMSESP_APP_VERSION); - return StateUpdateResult::CHANGED; - }); - } - - if (reboot_required) { - LOG_INFO("Upgrade: Rebooting to apply changes"); - return true; // need reboot - } - - return false; // no reboot required - } - +#include "system.h" +#include "emsesp.h" // for send_raw_telegram() command + +#ifndef EMSESP_STANDALONE +#include "esp_image_format.h" +#include "esp_ota_ops.h" +#include "esp_partition.h" +#include +#include "esp_efuse.h" +#include +#include +#endif + +#include +#include + +#include + +#if defined(EMSESP_TEST) +#include "../test/test.h" +#endif + +#ifndef NO_TLS_SUPPORT +#define ENABLE_SMTP +#define USE_ESP_SSLCLIENT +#define READYCLIENT_SSL_CLIENT ESP_SSLClient +#define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient +#include +#include +#endif + +namespace emsesp { + +// Languages supported. Note: the order is important +// and must match locale_translations.h and common.h +#if defined(EMSESP_TEST) +// in Test mode use two languages (en & de) to save flash memory needed for the tests +const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; +#elif defined(EMSESP_EN_ONLY) +// EN only +const char * const languages[] = {EMSESP_LOCALE_EN}; +#elif defined(EMSESP_DE_ONLY) +// EN + DE +const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; +#else +const char * const languages[] = {EMSESP_LOCALE_EN, + EMSESP_LOCALE_DE, + EMSESP_LOCALE_NL, + EMSESP_LOCALE_SV, + EMSESP_LOCALE_PL, + EMSESP_LOCALE_NO, + EMSESP_LOCALE_FR, + EMSESP_LOCALE_TR, + EMSESP_LOCALE_IT, + EMSESP_LOCALE_SK, + EMSESP_LOCALE_CZ}; +#endif + +static constexpr uint8_t NUM_LANGUAGES = sizeof(languages) / sizeof(const char *); + +#ifndef EMSESP_STANDALONE +uuid::syslog::SyslogService System::syslog_; +#endif + +uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN}; + +// init statics +PButton System::myPButton_; +bool System::test_set_all_active_ = false; +uint32_t System::max_alloc_mem_; +uint32_t System::heap_mem_; + +// LED flash timer +uint8_t System::led_flash_gpio_ = 0; +uint8_t System::led_flash_type_ = 0; +uint32_t System::led_flash_start_time_ = 0; +uint32_t System::led_flash_duration_ = 0; +bool System::led_flash_timer_ = false; + +// GPIOs +std::vector> System::valid_system_gpios_; +std::vector> System::used_gpios_; + +// find the index of the language +// 0 = EN, 1 = DE, etc... +uint8_t System::language_index() { + for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { + if (languages[i] == locale()) { + return i; + } + } + return 0; // EN only +} + +// send raw to ems +bool System::command_send(const char * value, const int8_t id) { + return EMSESP::txservice_.send_raw(value); // ignore id +} + +bool System::command_sendmail(const char * value, const int8_t id) { + bool enabled = false; + bool ssl, starttls; + uint16_t port; + String server, login, pass, sender, recp, subject; + EMSESP::webSettingsService.read([&](WebSettings & settings) { + enabled = settings.email_enabled; + ssl = settings.email_ssl; + starttls = settings.email_starttls; + server = settings.email_server; + port = settings.email_port; + login = settings.email_login; + pass = settings.email_pass; + sender = settings.email_sender; + recp = settings.email_recp; + subject = settings.email_subject; + }); + if (!enabled) { + return false; + } + LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value); + // LOG_DEBUG("Command sendmail port %d called with '%s'", port, value); + bool success = false; + +#ifndef NO_TLS_SUPPORT + WiFiClient * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = new ESP_SSLClient; + ReadyClient * r_client = new ReadyClient(*ssl_client); + SMTPClient * smtp = new SMTPClient(*r_client); + + ssl_client->setClient(basic_client); + ssl_client->setInsecure(); + ssl_client->setBufferSizes(1024, 1024); + r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text); + + // smtp->connect(server, port, sendmailCallback); + smtp->connect(server, port); + if (!smtp->isConnected()) { + LOG_ERROR("Sendmail connection error"); + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + return false; + } + + // LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str()); + smtp->authenticate(login, pass, readymail_auth_password); + if (!smtp->isAuthenticated()) { + LOG_ERROR("Sendmail authenticate error"); + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + return false; + } + JsonDocument doc; + String body = value; + if (body.length()) { + auto error = deserializeJson(doc, (const char *)value); + if (!error && doc.as().size() >= 0) { + subject = doc["subject"] | subject; + recp = doc["to"] | recp; + sender = doc["from"] | sender; + body = doc["body"] | body; + } + } + + SMTPMessage & msg = smtp->getMessage(); + msg.headers.add(rfc822_subject, subject); + msg.headers.add(rfc822_from, sender); + msg.headers.add(rfc822_to, recp); + + // Use addCustom to add custom header e.g. Importance and Priority. + // msg.headers.addCustom("Importance", PRIORITY); + // msg.headers.addCustom("X-MSMail-Priority", PRIORITY); + // msg.headers.addCustom("X-Priority", PRIORITY_NUM); + EMSESP::webSchedulerService.computed_value.clear(); + EMSESP::webSchedulerService.raw_value = body.c_str(); + for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) { + delay(1); + } + if (!EMSESP::webSchedulerService.computed_value.empty()) { + body = EMSESP::webSchedulerService.computed_value.c_str(); + EMSESP::webSchedulerService.computed_value.clear(); + EMSESP::webSchedulerService.computed_value.shrink_to_fit(); // free allocated memory + } + msg.text.body(body); + + // bodyText.replace("\r\n", "
\r\n"); + // msg.html.body("
" + bodyText + "
"); + // msg.html.transferEncoding("base64"); + + // With embedFile function, the html message will send as attachment. + // if (EMBED_MESSAGE) + // msg.html.embedFile(true, "msg.html", embed_message_type_attachment); + + msg.timestamp = time(nullptr); + + success = smtp->send(msg); + + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; +#endif + return success; +} + +// return string of languages and count +std::string System::languages_string() { + std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; + for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { + languages_string += languages[i]; + if (i != NUM_LANGUAGES - 1) { + languages_string += ","; + } + } + languages_string += ")"; + return languages_string; +} + +// returns last response from MQTT +bool System::command_response(const char * value, const int8_t id, JsonObject output) { + JsonDocument doc; + if (DeserializationError::Ok == deserializeJson(doc, Mqtt::get_response())) { + for (JsonPair p : doc.as()) { + output[p.key()] = p.value(); + } + } else { + output["response"] = Mqtt::get_response(); + } + return true; +} + +// fetch device values +bool System::command_fetch(const char * value, const int8_t id) { + std::string value_s; + if (Helpers::value2string(value, value_s)) { + if (value_s == "all") { + LOG_INFO("Requesting data from EMS devices"); + EMSESP::fetch_device_values(); + } else if (value_s == F_(boiler)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::BOILER); + } else if (value_s == F_(thermostat)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::THERMOSTAT); + } else if (value_s == F_(solar)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::SOLAR); + } else if (value_s == F_(mixer)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::MIXER); + } + } else { + EMSESP::fetch_device_values(); // default if no name or id is given + } + + return true; // always true +} + +// mqtt publish +bool System::command_publish(const char * value, const int8_t id) { + std::string value_s; + if (Helpers::value2string(value, value_s)) { + if (value_s == "ha") { + EMSESP::publish_all(true); // includes HA + LOG_INFO("Publishing all data to MQTT, including HA configs"); + return true; + } else if (value_s == (F_(boiler))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER); + return true; + } else if (value_s == (F_(thermostat))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT); + return true; + } else if (value_s == (F_(solar))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR); + return true; + } else if (value_s == (F_(mixer))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER); + return true; + } else if (value_s == (F_(water))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::WATER); + return true; + } else if (value_s == "other") { + EMSESP::publish_other_values(); // switch and heat pump + return true; + } else if ((value_s == (F_(temperaturesensor))) || (value_s == (F_(analogsensor)))) { + EMSESP::publish_sensor_values(true); + return true; + } + } + + LOG_INFO("Publishing all data to MQTT"); + EMSESP::publish_all(); + + return true; +} + +// syslog level +// commenting this out - don't see the point on having an API service to change the syslog level +/* +bool System::command_syslog_level(const char * value, const int8_t id) { + uint8_t s = 0xff; + if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { + bool changed = false; + EMSESP::webSettingsService.update( + [&](WebSettings & settings) { + if (settings.syslog_level != (int8_t)s - 1) { + settings.syslog_level = (int8_t)s - 1; + changed = true; + } + return StateUpdateResult::CHANGED; + }); + if (changed) { + EMSESP::system_.syslog_init(); + } + return true; + } + return false; +} +*/ + +// send message - to system log and MQTT +bool System::command_message(const char * value, const int8_t id, JsonObject output) { + if (value == nullptr || value[0] == '\0') { + LOG_WARNING("Message is empty"); + return false; // must have a string value + } + + EMSESP::webSchedulerService.computed_value.clear(); + EMSESP::webSchedulerService.raw_value = value; + for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) { + delay(1); + } + + if (EMSESP::webSchedulerService.computed_value.empty()) { + LOG_WARNING("Message result is empty"); + return false; + } + + LOG_INFO("Message: %s", EMSESP::webSchedulerService.computed_value.c_str()); // send to log + Mqtt::queue_publish(F_(message), EMSESP::webSchedulerService.computed_value); // send to MQTT if enabled + output["api_data"] = EMSESP::webSchedulerService.computed_value; // send to API + EMSESP::webSchedulerService.computed_value.clear(); + EMSESP::webSchedulerService.computed_value.shrink_to_fit(); + return true; +} + +// watch +bool System::command_watch(const char * value, const int8_t id) { + uint8_t w = 0xff; + uint16_t i = Helpers::hextoint(value); + if (Helpers::value2enum(value, w, FL_(list_watch))) { + if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { + EMSESP::watch_id(0); + } + if (Mqtt::publish_single() && w != EMSESP::watch()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); + } else { + Mqtt::queue_publish("system_data/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); + } + } + EMSESP::watch(w); + return true; + } else if (i) { + if (Mqtt::publish_single() && i != EMSESP::watch_id()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/watch", Helpers::hextoa(i)); + } else { + Mqtt::queue_publish("system_data/watch", Helpers::hextoa(i)); + } + } + EMSESP::watch_id(i); + if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { + EMSESP::watch(EMSESP::Watch::WATCH_ON); + } + return true; + } + return false; +} + +void System::store_nvs_values() { + if (Command::find_command(EMSdevice::DeviceType::BOILER, 0, "nompower", 0) != nullptr) { + Command::call(EMSdevice::DeviceType::BOILER, "nompower", "-1"); // trigger a write + } + EMSESP::analogsensor_.store_counters(); + EMSESP::nvs_.end(); +} + +// Build up a list of all partitions and their version info +void System::get_partition_info() { + partition_info_.clear(); // clear existing data + +#ifdef EMSESP_STANDALONE + // dummy data for standalone mode - version, size, install_date + partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""}; + partition_info_["app1"] = {"", 0, ""}; + partition_info_["factory"] = {"", 0, ""}; + partition_info_["boot"] = {"", 0, ""}; +#else + + auto current_partition = (const char *)esp_ota_get_running_partition()->label; + + // update the current version and partition name in NVS if not already set + if (EMSESP::nvs_.getString(current_partition) != EMSESP_APP_VERSION || emsesp::EMSESP::nvs_.getBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true)) { + EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, false); + EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); + char c[20]; + snprintf(c, sizeof(c), "d_%s", current_partition); + auto t = time(nullptr); + // write timestamp always with new version, if clock is not set, this will be updated with ntp + EMSESP::nvs_.putULong(c, t); + } + + // Loop through all available partitions and update map with the version info pulled from NVS + // Partitions can be app0, app1, factory, boot + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + uint64_t buffer; + + while (it != nullptr) { + bool is_valid = true; + const esp_partition_t * part = esp_partition_get(it); + + if (part->label != nullptr && part->label[0] != '\0') { + // check if partition is valid and not empty + esp_partition_read(part, 0, &buffer, 8); + if (buffer == 0xFFFFFFFFFFFFFFFF) { + is_valid = false; // skip this partition + } + } + + // get the version from the NVS store, and add to map + if (is_valid) { + PartitionInfo p_info; + // if there is an entry for this partition in NVS, get it's version from NVS + p_info.version = EMSESP::nvs_.getString(part->label, "").c_str(); + char c[20]; + snprintf(c, sizeof(c), "d_%s", (const char *)part->label); + time_t d = EMSESP::nvs_.getULong(c, 0); + char time_string[25]; + strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); + p_info.install_date = d > 1500000000L ? time_string : ""; + + esp_image_metadata_t meta = {}; + esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { + p_info.size = meta.image_len / 1024; // actual firmware size in KB + } else { + p_info.size = 0; + } + + partition_info_[part->label] = p_info; + } + + it = esp_partition_next(it); // loop to next partition + } + esp_partition_iterator_release(it); +#endif +} + +// set NTP install time/date for the current partition +// assumes NTP is connected and working +void System::set_partition_install_date() { +#ifndef EMSESP_STANDALONE + auto current_partition = (const char *)esp_ota_get_running_partition()->label; + if (current_partition == nullptr) { + return; // fail-safe + } + + char c[20]; + snprintf(c, sizeof(c), "d_%s", current_partition); + time_t d = EMSESP::nvs_.getULong(c, 0); + if (d < 1500000000L) { + LOG_DEBUG("Setting the install date in partition %s", current_partition); + auto t = time(nullptr) - uuid::get_uptime_sec(); + EMSESP::nvs_.putULong(c, t); + } +#endif +} + +// sets the partition to use on the next restart +bool System::set_partition(const char * partitionname) { +#ifdef EMSESP_STANDALONE + return true; +#else + if (partitionname == nullptr) { + return false; + } + + // Find the partition by label + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, partitionname); + if (it == nullptr) { + return false; // partition not found + } + + const esp_partition_t * partition = esp_partition_get(it); + esp_partition_iterator_release(it); + + if (partition == nullptr) { + return false; + } + + // Set the boot partition + esp_err_t err = esp_ota_set_boot_partition(partition); + if (err != ESP_OK) { + return false; + } + + // initiate the restart + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); + return true; +#endif +} + +// restart EMS-ESP +// app0 or app1, or boot/factory on 16MB boards +void System::system_restart(const char * partitionname) { + // see if we are forcing a partition to use + if (partitionname != nullptr) { +#ifndef EMSESP_STANDALONE + // Factory partition - label will be "factory" + const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); + if (partition && !strcmp(partition->label, partitionname)) { + esp_ota_set_boot_partition(partition); + } else + // try and find the partition by name + if (strcmp(esp_ota_get_running_partition()->label, partitionname)) { + // not found, get next one in cycle + partition = esp_ota_get_next_update_partition(nullptr); + if (!partition) { + LOG_ERROR("Partition '%s' not found", partitionname); + return; + } + if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { + partition = esp_ota_get_next_update_partition(partition); + if (!partition || strcmp(partition->label, partitionname)) { + LOG_ERROR("Partition '%s' not found", partitionname); + return; + } + } + // error if partition is empty + uint64_t buffer; + esp_partition_read(partition, 0, &buffer, 8); + if (buffer == 0xFFFFFFFFFFFFFFFF) { + LOG_ERROR("Partition '%s' is empty, not bootable", partition->label); + return; + } + // set the boot partition + esp_ota_set_boot_partition(partition); + } +#endif + LOG_INFO("Restarting EMS-ESP from %s partition", partitionname); + } else { + LOG_INFO("Restarting EMS-ESP..."); + } + + store_nvs_values(); // save any NVS values + + // flush all the log + EMSESP::webLogService.loop(); // dump all to web log + for (int i = 0; i < 10; i++) { + Shell::loop_all(); + delay(10); // give telnet TCP stack time to transmit + } + Serial.flush(); // wait for hardware TX buffer to drain + + Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 + EMSuart::stop(); // stop UART so there is no interference +#ifndef EMSESP_STANDALONE + delay(1000); // wait 1 second + ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called +#endif +} + +// saves all settings +void System::wifi_reconnect() { + EMSESP::esp32React.getNetworkSettingsService()->read( + [](NetworkSettings & networkSettings) { LOG_INFO("WiFi reconnecting to SSID '%s'...", networkSettings.ssid.c_str()); }); + delay(500); // wait + EMSESP::webSettingsService.save(); // save local settings + EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password +} + +void System::syslog_init() { + EMSESP::webSettingsService.read([&](WebSettings & settings) { + syslog_enabled_ = settings.syslog_enabled; + syslog_level_ = settings.syslog_level; + syslog_mark_interval_ = settings.syslog_mark_interval; + syslog_host_ = settings.syslog_host; + syslog_port_ = settings.syslog_port; + }); +#ifndef EMSESP_STANDALONE + if (syslog_enabled_) { + // start & configure syslog + syslog_.maximum_log_messages(10); + syslog_.log_level((uuid::log::Level)syslog_level_); + syslog_.mark_interval(syslog_mark_interval_); + syslog_.destination(syslog_host_.c_str(), syslog_port_); + syslog_.hostname(hostname()); + EMSESP::logger().info("Starting Syslog service"); + } else if (syslog_.started()) { + // in case service is still running, this flushes the queue + // https://github.com/emsesp/EMS-ESP/issues/496 + EMSESP::logger().info("Stopping Syslog"); + syslog_.loop(); + syslog_.log_level(uuid::log::Level::OFF); // stop server + syslog_.mark_interval(0); + // syslog_.destination(""); + } + if (Mqtt::publish_single()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); + if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { + Mqtt::queue_publish("system/watch", + EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); + } else { + Mqtt::queue_publish("system/watch", Helpers::hextoa(EMSESP::watch_id())); + } + + } else { + Mqtt::queue_publish("system_data/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); + if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { + Mqtt::queue_publish("system_data/watch", + EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); + } else { + Mqtt::queue_publish("system_data/watch", Helpers::hextoa(EMSESP::watch_id())); + } + } + } +#endif +} + +// read specific major system settings to store locally for faster access +void System::store_settings(WebSettings & settings) { + version_ = settings.version; + + rx_gpio_ = settings.rx_gpio; + tx_gpio_ = settings.tx_gpio; + pbutton_gpio_ = settings.pbutton_gpio; + dallas_gpio_ = settings.dallas_gpio; + led_gpio_ = settings.led_gpio; + + analog_enabled_ = settings.analog_enabled; + low_clock_ = settings.low_clock; + hide_led_ = settings.hide_led; + led_type_ = settings.led_type; + board_profile_ = settings.board_profile; + telnet_enabled_ = settings.telnet_enabled; + + tx_mode_ = settings.tx_mode; + syslog_enabled_ = settings.syslog_enabled; + syslog_level_ = settings.syslog_level; + syslog_mark_interval_ = settings.syslog_mark_interval; + syslog_host_ = settings.syslog_host; + syslog_port_ = settings.syslog_port; + + fahrenheit_ = settings.fahrenheit; + bool_format_ = settings.bool_format; + bool_dashboard_ = settings.bool_dashboard; + enum_format_ = settings.enum_format; + readonly_mode_ = settings.readonly_mode; + + phy_type_ = settings.phy_type; + eth_power_ = settings.eth_power; + eth_phy_addr_ = settings.eth_phy_addr; + eth_clock_mode_ = settings.eth_clock_mode; + + locale_ = settings.locale; + developer_mode_ = settings.developer_mode; + // start services + if (settings.modbus_enabled) { + if (EMSESP::modbus_ == nullptr) { + EMSESP::modbus_ = new Modbus; + EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); + } else if (settings.modbus_port != modbus_port_ || settings.modbus_max_clients != modbus_max_clients_ || settings.modbus_timeout != modbus_timeout_) { + EMSESP::modbus_->stop(); + EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); + } + } else if (EMSESP::modbus_ != nullptr) { + EMSESP::modbus_->stop(); + delete EMSESP::modbus_; + EMSESP::modbus_ = nullptr; + } + modbus_enabled_ = settings.modbus_enabled; + modbus_port_ = settings.modbus_port; + modbus_max_clients_ = settings.modbus_max_clients; + modbus_timeout_ = settings.modbus_timeout; +} + +// Starts up core services +void System::start() { + get_partition_info(); // get the partition info + +#ifndef EMSESP_STANDALONE + // disable bluetooth module + // periph_module_disable(PERIPH_BT_MODULE); + if (low_clock_) { +#if CONFIG_IDF_TARGET_ESP32C3 + setCpuFrequencyMhz(80); +#else + setCpuFrequencyMhz(160); +#endif + } + + // get current memory values + fstotal_ = LittleFS.totalBytes() / 1024; // read only once, it takes 500 ms to read + appused_ = ESP.getSketchSize() / 1024; + appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; + refreshHeapMem(); // refresh free heap and max alloc heap +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); + temperature_sensor_install(&temp_sensor_config, &temperature_handle_); + temperature_sensor_enable(temperature_handle_); + temperature_sensor_get_celsius(temperature_handle_, &temperature_); +#endif +#endif + + EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { + hostname(networkSettings.hostname.c_str()); // sets the hostname + }); + + commands_init(); // console & api commands + led_init(); // init LED + button_init(); // button + network_init(); // network + uart_init(); // start UART + syslog_init(); // start syslog +} + +// button single click +void System::button_OnClick(PButton & b) { + LOG_NOTICE("Button pressed - single click"); + +#if defined(EMSESP_TEST) +#ifndef EMSESP_STANDALONE + // show filesystem + Test::listDir(LittleFS, "/", 3); +#endif +#endif +} + +// button double click +void System::button_OnDblClick(PButton & b) { + LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); + // set AP mode to always so will join AP if wifi ssid fails to connect + EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { + apSettings.provisionMode = AP_MODE_ALWAYS; + return StateUpdateResult::CHANGED; + }); + // remove SSID from network settings + EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) { + networkSettings.ssid = ""; + return StateUpdateResult::CHANGED; + }); + EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password +} + +// LED flash every 100ms +void System::led_flash() { + static bool led_flash_state_ = false; + static uint32_t last_toggle_time_ = 0; + uint32_t current_time = uuid::get_uptime(); + + if (current_time - last_toggle_time_ >= 100) { // every 100ms + led_flash_state_ = !led_flash_state_; + last_toggle_time_ = current_time; + + if (led_flash_type_) { + uint8_t intensity = led_flash_state_ ? RGB_LED_BRIGHTNESS : 0; + EMSESP_RGB_WRITE(led_flash_gpio_, intensity, intensity, 0); // RGB LED - Yellow + } else { + digitalWrite(led_flash_gpio_, led_flash_state_ ? LED_ON : !LED_ON); // Standard LED + } + } + + // after duration, turn off the LED + if (current_time - led_flash_start_time_ >= led_flash_duration_) { + if (led_flash_type_) { + EMSESP_RGB_WRITE(led_flash_gpio_, 0, 0, 0); + } else { + digitalWrite(led_flash_gpio_, !LED_ON); + } + led_flash_timer_ = false; + command_format(nullptr, 0); // Execute format operation + } +} + +// Start the LED flash timer - duration in seconds +void System::start_led_flash(uint8_t duration) { + // Don't start if already running + if (led_flash_timer_) { + return; + } + + // Get LED settings + EMSESP::webSettingsService.read([&](WebSettings & settings) { + led_flash_type_ = settings.led_type; + led_flash_gpio_ = settings.led_gpio; + }); + + // Reset counter and state + led_flash_start_time_ = uuid::get_uptime(); // current time + led_flash_duration_ = duration * 1000; // duration in milliseconds + led_flash_timer_ = true; // it's active +} + +// button long press +void System::button_OnLongPress(PButton & b) { + LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); + EMSESP::system_.system_restart("boot"); +} + +// button indefinite press +void System::button_OnVLongPress(PButton & b) { + LOG_NOTICE("Button pressed - very long press - perform factory reset"); + start_led_flash(5); // Start LED flash timer for 5 seconds +} + +// push button +void System::button_init() { +#ifndef EMSESP_STANDALONE + if (!myPButton_.init(pbutton_gpio_, HIGH)) { + LOG_WARNING("Multi-functional button not detected"); + return; + } + LOG_DEBUG("Multi-functional button enabled"); + + myPButton_.onClick(BUTTON_Debounce, button_OnClick); + myPButton_.onDblClick(BUTTON_DblClickDelay, button_OnDblClick); + myPButton_.onLongPress(BUTTON_LongPressDelay, button_OnLongPress); + myPButton_.onVLongPress(BUTTON_VLongPressDelay, button_OnVLongPress); +#endif +} + +// set the LED to on or off when in normal operating mode +void System::led_init() { + // disabled old led port before setting new one + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + + if ((led_gpio_)) { // 0 means disabled + if (led_type_) { + // rgb LED WS2812B, use Neopixel + EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0); + } else { + pinMode(led_gpio_, OUTPUT); + digitalWrite(led_gpio_, !LED_ON); // start with LED off + } + } else { + LOG_INFO("LED disabled"); + } +} + +void System::uart_init() { + EMSuart::stop(); + EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked + EMSESP::txservice_.start(); // reset counters and send devices request +} + +// checks system health and handles LED flashing wizardry +// returns true if the LED flash is active +bool System::loop() { + // check if we're supposed to do a reset/restart + if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED) { + system_restart(); + } + + // if LED flashing is active, run the LED flash + if (led_flash_timer_) { + led_flash(); + return true; // is active + } + + led_monitor(); // check status and report back using the LED + myPButton_.check(); // check button press + system_check(); // check system health + +// syslog +#ifndef EMSESP_STANDALONE + if (syslog_enabled_) { + syslog_.loop(); + } +#endif + + send_info_mqtt(); + + return false; // LED flashing is not active +} + +// send MQTT info topic appended with the version information as JSON, as a retained flag +// this is only done once when the connection is established +void System::send_info_mqtt() { + static uint8_t _connection = 0; + uint8_t connection = (ethernet_connected() ? 1 : 0) + ((WiFi.status() == WL_CONNECTED) ? 2 : 0) + (ntp_connected_ ? 4 : 0) + (has_ipv6_ ? 8 : 0); + // check if connection status has changed + if (!Mqtt::connected() || connection == _connection) { + return; + } + _connection = connection; + JsonDocument doc; + // doc["event"] = "connected"; + doc["version"] = EMSESP_APP_VERSION; + + // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) + // https://github.com/emsesp/EMS-ESP32/issues/751 + if (ntp_connected_) { + char time_string[25]; + time_t now = time(nullptr) - uuid::get_uptime_sec(); + strftime(time_string, 25, "%FT%T%z", localtime(&now)); + doc["bootTime"] = time_string; + } + +#ifndef EMSESP_STANDALONE + if (EMSESP::system_.ethernet_connected()) { + doc["network"] = "ethernet"; + doc["hostname"] = ETH.getHostname(); + /* + doc["MAC"] = ETH.macAddress(); + doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); + doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP()); + doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP()); + if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { + doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); + } + */ + + } else if (WiFi.status() == WL_CONNECTED) { + doc["network"] = "wifi"; + doc["hostname"] = WiFi.getHostname(); + doc["SSID"] = WiFi.SSID(); + doc["BSSID"] = WiFi.BSSIDstr(); + doc["MAC"] = WiFi.macAddress(); + doc["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask()); + doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); + doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); + + if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { + doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); + } + } +#endif + Mqtt::queue_publish_retain(F_(info), doc.as()); // topic called "info" and it's Retained +} + +// create the json for heartbeat +void System::heartbeat_json(JsonObject output) { + switch (EMSESP::bus_status()) { + case EMSESP::BUS_STATUS_OFFLINE: + output["bus_status"] = "connecting"; // EMS-ESP is booting... + break; + case EMSESP::BUS_STATUS_TX_ERRORS: + output["bus_status"] = "txerror"; + break; + case EMSESP::BUS_STATUS_CONNECTED: + output["bus_status"] = "connected"; + break; + default: + output["bus_status"] = "disconnected"; + break; + } + + output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); + output["uptime_sec"] = uuid::get_uptime_sec(); + + output["rxreceived"] = EMSESP::rxservice_.telegram_count(); + output["rxfails"] = EMSESP::rxservice_.telegram_error_count(); + output["txreads"] = EMSESP::txservice_.telegram_read_count(); + output["txwrites"] = EMSESP::txservice_.telegram_write_count(); + output["txfails"] = EMSESP::txservice_.telegram_read_fail_count() + EMSESP::txservice_.telegram_write_fail_count(); + + if (Mqtt::enabled()) { + output["mqttcount"] = Mqtt::publish_count(); + output["mqttfails"] = Mqtt::publish_fails(); + output["mqttreconnects"] = Mqtt::connect_count(); + } + output["apicalls"] = WebAPIService::api_count(); // + WebAPIService::api_fails(); + output["apifails"] = WebAPIService::api_fails(); + + if (EMSESP::sensor_enabled() || EMSESP::analog_enabled()) { + output["sensorreads"] = EMSESP::temperaturesensor_.reads() + EMSESP::analogsensor_.reads(); + output["sensorfails"] = EMSESP::temperaturesensor_.fails() + EMSESP::analogsensor_.fails(); + } + +#ifndef EMSESP_STANDALONE + output["freemem"] = getHeapMem(); + output["max_alloc"] = getMaxAllocMem(); +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + output["temperature"] = (int)temperature_; +#endif +#endif + +#ifndef EMSESP_STANDALONE + if (!ethernet_connected_) { + int8_t rssi = WiFi.RSSI(); + output["rssi"] = rssi; + output["wifistrength"] = wifi_quality(rssi); + output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects(); + } +#endif +} + +// send periodic MQTT message with system information +void System::send_heartbeat() { + refreshHeapMem(); // refresh free heap and max alloc heap + + JsonDocument doc; + JsonObject json = doc.to(); + + heartbeat_json(json); + Mqtt::queue_publish(F_(heartbeat), json); // send to MQTT with retain off. This will add to MQTT queue. +} + +// initializes network +void System::network_init() { + last_system_check_ = 0; // force the LED to go from fast flash to pulse + +#if CONFIG_IDF_TARGET_ESP32 + bool disableEth; + EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { disableEth = settings.ssid.length() > 0; }); + + // no ethernet present or disabled + if (phy_type_ == PHY_type::PHY_TYPE_NONE || disableEth) { + return; + } // no ethernet present + + // configure Ethernet + int mdc = 23; // Pin# of the I²C clock signal for the Ethernet PHY - hardcoded + int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded + uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) + int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) + eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 + : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 + : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) + // clock mode: + // ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0 + // ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0 + // ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16 + // ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock + auto clock_mode = (eth_clock_mode_t)eth_clock_mode_; + + // reset power and add a delay as ETH doesn't not always start up correctly after a warm boot + if (eth_power_ != -1) { + pinMode(eth_power_, OUTPUT); + digitalWrite(eth_power_, LOW); + delay(500); + digitalWrite(eth_power_, HIGH); + } + eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); +#endif +} + +// check health of system, done every 5 seconds +void System::system_check() { + uint32_t current_uptime = uuid::get_uptime(); + if (!last_system_check_ || ((uint32_t)(current_uptime - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) { + last_system_check_ = current_uptime; + +#ifndef EMSESP_STANDALONE +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + temperature_sensor_get_celsius(temperature_handle_, &temperature_); +#endif +#endif + +#ifdef EMSESP_PINGTEST + static uint64_t ping_count = 0; + LOG_NOTICE("Ping test, #%d", ping_count++); +#endif + + // check if we have a valid network connection + if (!ethernet_connected() && (WiFi.status() != WL_CONNECTED)) { + healthcheck_ |= HEALTHCHECK_NO_NETWORK; + } else { + healthcheck_ &= ~HEALTHCHECK_NO_NETWORK; + } + + // check if we have a bus connection + if (!EMSbus::bus_connected()) { + healthcheck_ |= HEALTHCHECK_NO_BUS; + } else { + healthcheck_ &= ~HEALTHCHECK_NO_BUS; + } + + // see if the healthcheck state has changed + static uint8_t last_healthcheck_ = 0; + if (healthcheck_ != last_healthcheck_) { + last_healthcheck_ = healthcheck_; + + EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected + + // see if we're better now + if (healthcheck_ == 0) { + // everything is healthy, show LED permanently on or off depending on setting + // Green on RGB LED, on/off on standard LED + if (led_gpio_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) + : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // Green + } + } else { + // turn off LED so we're ready for the warning flashes + if (led_gpio_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + } + } + } + } +} + +// commands - takes static function pointers +// can be called via Console using 'call system ' +void System::commands_init() { + Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); +#if defined(EMSESP_TEST) + Command::add(EMSdevice::DeviceType::SYSTEM, ("test"), System::command_test, FL_(test_cmd)); +#endif + + // these commands will return data in JSON format + Command::add(EMSdevice::DeviceType::SYSTEM, F("response"), System::command_response, FL_(commands_response)); + + // MQTT subscribe "ems-esp/system/#" + Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback +} + +// uses LED to show system health +void System::led_monitor() { + // if button is pressed, show LED (yellow on RGB LED, on/off on standard LED) + static bool button_busy_ = false; + if (button_busy_ != myPButton_.button_busy()) { + button_busy_ = myPButton_.button_busy(); + if (led_type_) { + EMSESP_RGB_WRITE(led_gpio_, button_busy_ ? RGB_LED_BRIGHTNESS : 0, button_busy_ ? RGB_LED_BRIGHTNESS : 0, 0); // Yellow + } else { + digitalWrite(led_gpio_, button_busy_ ? LED_ON : !LED_ON); + } + } + + // we only need to run the LED healthcheck if there are errors + // skip if we're in the led_flash_timer or if a button has been pressed + if (!healthcheck_ || !led_gpio_ || button_busy_ || led_flash_timer_) { + return; // all good + } + + static uint32_t led_long_timer_ = 1; // 1 will kick it off immediately + static uint32_t led_short_timer_ = 0; + static uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer + + auto current_time = uuid::get_uptime(); + + // first long pause before we start flashing + if (led_long_timer_ && (uint32_t)(current_time - led_long_timer_) >= HEALTHCHECK_LED_LONG_DUARATION) { + led_short_timer_ = current_time; // start the short timer + led_long_timer_ = 0; // stop long timer + led_flash_step_ = 1; // enable the short flash timer + } + + // the flash timer which starts after the long pause + if (led_flash_step_ && (uint32_t)(current_time - led_short_timer_) >= HEALTHCHECK_LED_FLASH_DUARATION) { + led_long_timer_ = 0; // stop the long timer + led_short_timer_ = current_time; + static bool led_on_ = false; + + if (++led_flash_step_ == 8) { + // reset the whole sequence + led_long_timer_ = uuid::get_uptime(); + led_flash_step_ = 0; + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); // LED off + } else if (led_flash_step_ % 2) { + // handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED + // 1 flash (blue) is the EMS bus is not connected + // 2 flashes (red, red) if the network (wifi or ethernet) is not connected + // 3 flashes (red, red, blue) is both the bus and the network are not connected + bool no_network = (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK; + bool no_bus = (healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS; + + if (led_type_) { + if (led_flash_step_ == 3) { + if (no_network) { + EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red + } else if (no_bus) { + EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue + } + } + if (led_flash_step_ == 5 && no_network) { + EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red + } + if ((led_flash_step_ == 7) && no_network && no_bus) { + EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue + } + } else { + if ((led_flash_step_ == 3) && (no_network || no_bus)) { + led_on_ = true; + } + + if ((led_flash_step_ == 5) && no_network) { + led_on_ = true; + } + + if ((led_flash_step_ == 7) && no_network && no_bus) { + led_on_ = true; + } + + if (led_on_) { + digitalWrite(led_gpio_, LED_ON); // LED on + } + } + } else { + // turn the led off after the flash, on even number count + if (led_on_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + led_on_ = false; + } + } + } +} + +// Return the quality (Received Signal Strength Indicator) of the WiFi network as a % +// High quality: 90% ~= -55dBm +// Medium quality: 50% ~= -75dBm +// Low quality: 30% ~= -85dBm +// Unusable quality: 8% ~= -96dBm +int8_t System::wifi_quality(int8_t dBm) { + if (dBm <= -100) { + return 0; + } + + if (dBm >= -50) { + return 100; + } + return 2 * (dBm + 100); +} + +// print users to console +void System::show_users(uuid::console::Shell & shell) { + if (!shell.has_flags(CommandFlags::ADMIN)) { + shell.printfln("Unauthorized. You need to be an admin to view users."); + return; + } + + shell.printfln("Users:"); + +#ifndef EMSESP_STANDALONE + EMSESP::esp32React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { + for (const User & user : securitySettings.users) { + shell.printfln(" username: %s, password: %s, is_admin: %s", user.username.c_str(), user.password.c_str(), user.admin ? ("yes") : ("no")); + } + }); +#endif + + shell.println(); +} + +// shell command 'show system' +void System::show_system(uuid::console::Shell & shell) { + refreshHeapMem(); // refresh free heap and max alloc heap + + shell.println(); + shell.println("System:"); + shell.printfln(" Version: %s", EMSESP_APP_VERSION); +#ifndef EMSESP_STANDALONE + shell.printfln(" Platform: %s (%s)", EMSESP_PLATFORM, ESP.getChipModel()); + shell.printfln(" Model: %s", getBBQKeesGatewayDetails().c_str()); +#endif + shell.printfln(" Language: %s", locale().c_str()); + shell.printfln(" Board profile: %s", board_profile().c_str()); + shell.printfln(" Uptime: %s", uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); +#ifndef EMSESP_STANDALONE + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/misc_system_api.html + unsigned char mac_base[6] = {0}; + esp_efuse_mac_get_default(mac_base); + esp_read_mac(mac_base, ESP_MAC_WIFI_STA); + shell.printfln(" Base MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]); + + shell.printfln(" SDK version: %s", ESP.getSdkVersion()); + shell.printfln(" CPU frequency: %lu MHz", ESP.getCpuFreqMHz()); +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + shell.printfln(" CPU temperature: %d °C", (int)temperature()); +#endif + shell.printfln(" Free heap/Max alloc: %lu KB / %lu KB", getHeapMem(), getMaxAllocMem()); + shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree()); + uint32_t FSused = LittleFS.usedBytes() / 1024; + shell.printfln(" FS used/free: %lu KB / %lu KB", FSused, FStotal() - FSused); + shell.printfln(" Flash size: %lu KB", ESP.getFlashChipSize() / 1024); + if (PSram()) { + shell.printfln(" PSRAM size/free: %lu KB / %lu KB", PSram(), ESP.getFreePsram() / 1024); + } else { + shell.printfln(" PSRAM: not available"); + } + // GPIOs + shell.println(" GPIOs:"); + shell.printf(" allowed:"); + for (const auto & gpio : valid_system_gpios_) { + shell.printf(" %d", gpio); + } + shell.printfln(" [total %d]", valid_system_gpios_.size()); + shell.printf(" in use:"); + auto sorted_gpios = used_gpios_; + std::sort(sorted_gpios.begin(), sorted_gpios.end(), [](const GpioUsage & a, const GpioUsage & b) { return a.pin < b.pin; }); + for (const auto & gpio : sorted_gpios) { + shell.printf(" %d(%s)", gpio.pin, gpio.source.c_str()); + } + shell.printfln(" [total %d]", used_gpios_.size()); + auto available = available_gpios(); + shell.printf(" available:"); + for (const auto & gpio : available) { + shell.printf(" %d", gpio); + } + shell.printfln(" [total %d]", available.size()); + // List all partitions and their version info + shell.println(" Partitions:"); + for (const auto & partition : partition_info_) { + if (partition.second.version.empty()) { + continue; // no version, empty string + } + shell.printfln(" %s: v%s (%d KB%s) %s", + partition.first.c_str(), + partition.second.version.c_str(), + partition.second.size, + partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(), + (strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : ""); + } + + shell.println(); + shell.println("Network:"); + switch (WiFi.status()) { + case WL_IDLE_STATUS: + shell.printfln(" Status: Idle"); + break; + + case WL_NO_SSID_AVAIL: + shell.printfln(" Status: Network not found"); + break; + + case WL_SCAN_COMPLETED: + shell.printfln(" Status: Network scan complete"); + break; + + case WL_CONNECTED: + shell.printfln(" Status: WiFi connected"); + shell.printfln(" SSID: %s", WiFi.SSID().c_str()); + shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); + shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); + char result[10]; + shell.printfln(" TxPower: %s dBm", Helpers::render_value(result, (double)(WiFi.getTxPower() / 4), 1)); + shell.printfln(" MAC address: %s", WiFi.macAddress().c_str()); + shell.printfln(" Hostname: %s", WiFi.getHostname()); + shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); + shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); + shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); + if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); + } + break; + + case WL_CONNECT_FAILED: + shell.printfln(" WiFi Network: Connection failed"); + break; + + case WL_CONNECTION_LOST: + shell.printfln(" WiFi Network: Connection lost"); + break; + + case WL_DISCONNECTED: + shell.printfln(" WiFi Network: Disconnected"); + break; + + // case WL_NO_SHIELD: + default: + shell.printfln(" WiFi MAC address: %s", WiFi.macAddress().c_str()); + shell.printfln(" WiFi Network: not connected"); + break; + } + + // show Ethernet if connected + if (ethernet_connected_) { + shell.println(); + shell.printfln(" Ethernet Status: connected"); + shell.printfln(" Ethernet MAC address: %s", ETH.macAddress().c_str()); + shell.printfln(" Hostname: %s", ETH.getHostname()); + shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); + shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); + shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); + if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); + } + } + shell.println(); + + shell.println("Syslog:"); + if (!syslog_enabled_) { + shell.printfln(" Syslog: disabled"); + } else { + shell.printfln(" Syslog: %s", syslog_.started() ? "started" : "stopped"); + shell.print(" "); + shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : F_(unset)); + shell.printfln(" IP: %s", uuid::printable_to_string(syslog_.ip()).c_str()); + shell.print(" "); + shell.printfln(F_(port_fmt), syslog_port_); + shell.print(" "); + shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(syslog_level_))); + shell.print(" "); + shell.printfln(F_(mark_interval_fmt), syslog_mark_interval_); + shell.printfln(" Queued: %d", syslog_.queued()); + } + + shell.println(); +#endif +} + +// see if there is a restore of an older settings file that needs to be applied +// note there can be only one file at a time +bool System::check_restore() { + bool reboot_required = false; // true if we need to reboot + #ifndef EMSESP_STANDALONE // map each config filename to its human-readable section key static const std::pair SECTION_MAP[] = { diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index e708c94e7..db3293fbe 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -363,39 +363,139 @@ bool WebSchedulerService::command(const char * name, const std::string & command commands(s, false); url.replace(q + 1, l, s); } - if (http.begin(url.c_str())) { - // add any given headers - for (JsonPair p : doc["header"].as()) { - http.addHeader(p.key().c_str(), p.value().as().c_str()); + std::string value = doc["value"] | data; // extract value if its in the command, or take the data + std::string method = doc["method"] | "GET"; // default GET + commands(value, false); + if (value.length()) { + method = "POST"; + } + std::string result; + int httpResult = 0; +#ifndef NO_TLS_SUPPORT + if (Helpers::toLower(url.c_str()).starts_with("https://")) { + WiFiClient * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = new ESP_SSLClient; + ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert); + ssl_client->setBufferSizes(1024, 1024); + ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds) + url.replace(0, 8, ""); + std::string host = url; + auto index = url.find_first_of('/'); + if (index != std::string::npos) { + host = url.substr(0, index); + url.replace(0, index, ""); } - std::string value = doc["value"] | data.c_str(); // extract value if its in the command, or take the data - std::string method = doc["method"] | "GET"; // default GET - - commands(value, false); - // if there is data, force a POST - int httpResult = 0; - if (value.length() || method == "post") { // we have all lowercase - if (value.find_first_of('{') != std::string::npos) { - http.addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON + // EMSESP::logger().debug("Host: %s, URL: %s", host.c_str(), url.c_str()); + ssl_client->setClient(basic_client); + if (ssl_client->connect(host.c_str(), 443)) { + if (value.length() || Helpers::toLower(method) == "post") { + // EMSESP::logger().debug("POST %s HTTP/1.1", url.c_str()); + ssl_client->print("POST "); + ssl_client->print(url.c_str()); + ssl_client->println(" HTTP/1.1"); + ssl_client->print("Host: "); + ssl_client->println(host.c_str()); + bool content_set = false; + for (JsonPair p : doc["header"].as()) { + content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type"); + ssl_client->print(p.key().c_str()); + ssl_client->print(": "); + ssl_client->println(p.value().as().c_str()); + } + if (!content_set) { + ssl_client->print("Content-Type: "); + if (value.starts_with('{')) { + ssl_client->println(asyncsrv::T_application_json); + } else { + ssl_client->println(asyncsrv::T_text_plain); + } + } + ssl_client->print("Content-Length: "); + ssl_client->println(value.length()); + ssl_client->println("Connection: close"); + ssl_client->print("\r\n"); + ssl_client->print(value.c_str()); + } else { + // EMSESP::logger().debug("GET %s HTTP/1.1", url.c_str()); + ssl_client->print("GET "); + ssl_client->print(url.c_str()); + ssl_client->println(" HTTP/1.1"); + ssl_client->print("Host: "); + ssl_client->println(host.c_str()); + for (JsonPair p : doc["header"].as()) { + ssl_client->print(p.key().c_str()); + ssl_client->print(": "); + ssl_client->println(p.value().as().c_str()); + } + ssl_client->println("Connection: close"); + } + auto ms = millis(); + while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) { + delay(0); + } + while (ssl_client->available()) { + result += (char)ssl_client->read(); + } + ssl_client->stop(); + // EMSESP::logger().debug("HTTPS response: %s", result.c_str()); + index = result.find_first_of(' '); + if (index != std::string::npos) { + httpResult = stoi(result.substr(index + 1, 3)); + // EMSESP::logger().debug("HTTPS code: %i", httpResult); + } + index = result.find("\r\n\r\n"); + if (index != std::string::npos) { + result.replace(0, index + 4, ""); + // EMSESP::logger().debug("HTTPS response: %s", result.c_str()); } - httpResult = http.POST(value.c_str()); } else { - httpResult = http.GET(); // normal GET + EMSESP::logger().warning("HTTPS connection failed"); } - - http.end(); - + delete ssl_client; + delete basic_client; // check HTTP return code if (httpResult != 200) { - char error[100]; - snprintf(error, sizeof(error), "Schedule %s: URL command failed with http code %d", name, httpResult); - EMSESP::logger().warning(error); + EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult); + return false; + } + return true; + } else +#endif + if (Helpers::toLower(url.c_str()).starts_with("http://")) { + HTTPClient * http = new HTTPClient; + if (http->begin(url.c_str())) { + bool content_set = false; + for (JsonPair p : doc["header"].as()) { + http->addHeader(p.key().c_str(), p.value().as().c_str()); + content_set |= p.key() == "content-type"; + } + // if there is data, force a POST + if (Helpers::toLower(method) == "post") { // we have all lowercase + if (!content_set) { + // http->addHeader("Content-Type", value.find_first_of('{') != std::string::npos ? "application/json" : "text/plain"); + if (value.starts_with('{')) { + http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON + } else { + http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_text_plain, false); // auto-set to JSON + } + } + httpResult = http->POST(value.c_str()); + } else { + httpResult = http->GET(); // normal GET + if (httpResult > 0) { + result = http->getString().c_str(); + } + } + } + http->end(); + delete http; + // check HTTP return code + if (httpResult != 200) { + EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult); return false; } #if defined(EMSESP_DEBUG) - char msg[100]; - snprintf(msg, sizeof(msg), "Schedule %s: URL command successful with http code %d", name, httpResult); - EMSESP::logger().debug(msg); + EMSESP::logger().debug("Schedule %s: URL '%s' command successful with http code %d", name, url.c_str(), httpResult); #endif return true; }