mirror of
https://github.com/esphome/esphome.git
synced 2026-02-12 12:37:36 -07:00
Compare commits
24 Commits
web_server
...
water_heat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c11dba61d | ||
|
|
5f696e7097 | ||
|
|
ea1a968921 | ||
|
|
6131e18388 | ||
|
|
e3a7587917 | ||
|
|
e0ff7fdaa1 | ||
|
|
3c9b300c46 | ||
|
|
8d226c7faa | ||
|
|
7d468e6143 | ||
|
|
6b1f2456bd | ||
|
|
ec74fe6cdf | ||
|
|
5c8ee735c3 | ||
|
|
cde3c02a00 | ||
|
|
dec8cbbe41 | ||
|
|
c5177e8553 | ||
|
|
b8cac2cb7f | ||
|
|
a695ebf551 | ||
|
|
7c91625408 | ||
|
|
916dc78e51 | ||
|
|
dd13b42f78 | ||
|
|
5b062e4e3c | ||
|
|
7ea56489e3 | ||
|
|
9bb46c564f | ||
|
|
72f612b5a0 |
@@ -4,18 +4,24 @@ from typing import Any
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, update
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
AUTO_LOAD = ["sha256", "watchdog"]
|
||||
AUTO_LOAD = ["sha256", "watchdog", "json"]
|
||||
DEPENDENCIES = ["esp32_hosted"]
|
||||
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
|
||||
TYPE_EMBEDDED = "embedded"
|
||||
TYPE_HTTP = "http"
|
||||
|
||||
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
|
||||
http_request_ns = cg.esphome_ns.namespace("http_request")
|
||||
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
|
||||
Esp32HostedUpdate = esp32_hosted_ns.class_(
|
||||
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
|
||||
"Esp32HostedUpdate", update.UpdateEntity, cg.PollingComponent
|
||||
)
|
||||
|
||||
|
||||
@@ -30,12 +36,29 @@ def _validate_sha256(value: Any) -> str:
|
||||
return value
|
||||
|
||||
|
||||
BASE_SCHEMA = update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
|
||||
cv.polling_component_schema("6h")
|
||||
)
|
||||
|
||||
EMBEDDED_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
cv.Required(CONF_SHA256): _validate_sha256,
|
||||
}
|
||||
)
|
||||
|
||||
HTTP_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_SOURCE): cv.url,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
|
||||
cv.typed_schema(
|
||||
{
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
cv.Required(CONF_SHA256): _validate_sha256,
|
||||
TYPE_EMBEDDED: EMBEDDED_SCHEMA,
|
||||
TYPE_HTTP: HTTP_SCHEMA,
|
||||
}
|
||||
),
|
||||
esp32.only_on_variant(
|
||||
@@ -48,6 +71,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def _validate_firmware(config: dict[str, Any]) -> None:
|
||||
if config[CONF_TYPE] != TYPE_EMBEDDED:
|
||||
return
|
||||
|
||||
path = CORE.relative_config_path(config[CONF_PATH])
|
||||
with open(path, "rb") as f:
|
||||
firmware_data = f.read()
|
||||
@@ -65,14 +91,22 @@ FINAL_VALIDATE_SCHEMA = _validate_firmware
|
||||
async def to_code(config: dict[str, Any]) -> None:
|
||||
var = await update.new_update(config)
|
||||
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
if config[CONF_TYPE] == TYPE_EMBEDDED:
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
|
||||
prog_arr = cg.progmem_array(arr_id, rhs)
|
||||
|
||||
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
|
||||
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
|
||||
cg.add(var.set_firmware_data(prog_arr))
|
||||
cg.add(var.set_firmware_size(len(firmware_data)))
|
||||
else:
|
||||
http_request_var = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
|
||||
cg.add(var.set_http_request_parent(http_request_var))
|
||||
cg.add(var.set_source_url(config[CONF_SOURCE]))
|
||||
cg.add_define("USE_ESP32_HOSTED_HTTP_UPDATE")
|
||||
|
||||
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
|
||||
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
|
||||
cg.add(var.set_firmware_data(prog_arr))
|
||||
cg.add(var.set_firmware_size(len(firmware_data)))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include <esp_image_format.h>
|
||||
#include <esp_app_desc.h>
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_host_fw_ver.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <esp_hosted_ota.h>
|
||||
@@ -16,18 +22,50 @@ namespace esphome::esp32_hosted {
|
||||
|
||||
static const char *const TAG = "esp32_hosted.update";
|
||||
|
||||
// older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
constexpr size_t CHUNK_SIZE = 1500;
|
||||
|
||||
// Compile-time version string from esp_hosted_host_fw_ver.h macros
|
||||
#define STRINGIFY_(x) #x
|
||||
#define STRINGIFY(x) STRINGIFY_(x)
|
||||
static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_MAJOR_1) "." STRINGIFY(
|
||||
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Parse version string "major.minor.patch" into components
|
||||
// Returns true if parsing succeeded
|
||||
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
|
||||
major = minor = patch = 0;
|
||||
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare two versions, returns:
|
||||
// -1 if v1 < v2
|
||||
// 0 if v1 == v2
|
||||
// 1 if v1 > v2
|
||||
static int compare_versions(int major1, int minor1, int patch1, int major2, int minor2, int patch2) {
|
||||
if (major1 != major2)
|
||||
return major1 < major2 ? -1 : 1;
|
||||
if (minor1 != minor2)
|
||||
return minor1 < minor2 ? -1 : 1;
|
||||
if (patch1 != patch2)
|
||||
return patch1 < patch2 ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Esp32HostedUpdate::setup() {
|
||||
this->update_info_.title = "ESP32 Hosted Coprocessor";
|
||||
|
||||
// if wifi is not present, connect to the coprocessor
|
||||
#ifndef USE_WIFI
|
||||
// If WiFi is not present, connect to the coprocessor
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
#endif
|
||||
|
||||
// get coprocessor version
|
||||
// Get coprocessor version
|
||||
esp_hosted_coprocessor_fwver_t ver_info;
|
||||
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
|
||||
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
|
||||
@@ -36,7 +74,8 @@ void Esp32HostedUpdate::setup() {
|
||||
}
|
||||
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
|
||||
|
||||
// get image version
|
||||
#ifndef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Embedded mode: get image version from embedded firmware
|
||||
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
|
||||
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
|
||||
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
|
||||
@@ -64,58 +103,272 @@ void Esp32HostedUpdate::setup() {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
|
||||
// publish state
|
||||
// Publish state
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#else
|
||||
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
|
||||
// Only if update interval is > 1 minute to avoid redundant checks
|
||||
if (this->get_update_interval() > 60000) {
|
||||
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
|
||||
if (!network::is_connected()) {
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
this->check();
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP32 Hosted Update:\n"
|
||||
" Current Version: %s\n"
|
||||
" Latest Version: %s\n"
|
||||
" Latest Size: %zu bytes",
|
||||
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
|
||||
" Host Library Version: %s\n"
|
||||
" Coprocessor Version: %s\n"
|
||||
" Latest Version: %s",
|
||||
ESP_HOSTED_VERSION_STR, this->update_info_.current_version.c_str(),
|
||||
this->update_info_.latest_version.c_str());
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: HTTP\n"
|
||||
" Source URL: %s",
|
||||
this->source_url_.c_str());
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: Embedded\n"
|
||||
" Firmware Size: %zu bytes",
|
||||
this->firmware_size_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::perform(bool force) {
|
||||
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
|
||||
ESP_LOGW(TAG, "Update not available");
|
||||
void Esp32HostedUpdate::check() {
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
if (!network::is_connected()) {
|
||||
ESP_LOGD(TAG, "Network not connected, skipping update check");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->fetch_manifest_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
if (this->update_info_.latest_version.empty() ||
|
||||
this->update_info_.latest_version == this->update_info_.current_version) {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
}
|
||||
|
||||
this->update_info_.has_progress = false;
|
||||
this->update_info_.progress = 0.0f;
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
ESP_LOGD(TAG, "Fetching manifest");
|
||||
|
||||
auto container = this->http_request_parent_->get(this->source_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
|
||||
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
yield();
|
||||
}
|
||||
container->end();
|
||||
|
||||
// Parse JSON manifest
|
||||
// Format: {"versions": [{"version": "2.7.0", "url": "...", "sha256": "..."}]}
|
||||
// Only consider versions <= host library version to avoid compatibility issues
|
||||
bool valid = json::parse_json(json_str, [this](JsonObject root) -> bool {
|
||||
if (!root["versions"].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain 'versions' array");
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonArray versions = root["versions"].as<JsonArray>();
|
||||
if (versions.size() == 0) {
|
||||
ESP_LOGE(TAG, "Manifest 'versions' array is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the highest version that is compatible with the host library
|
||||
// (version <= host version to avoid upgrading coprocessor ahead of host)
|
||||
int best_major = -1, best_minor = -1, best_patch = -1;
|
||||
std::string best_version, best_url, best_sha256;
|
||||
|
||||
for (JsonObject entry : versions) {
|
||||
if (!entry["version"].is<const char *>() || !entry["url"].is<const char *>() ||
|
||||
!entry["sha256"].is<const char *>()) {
|
||||
continue; // Skip malformed entries
|
||||
}
|
||||
|
||||
std::string ver_str = entry["version"].as<std::string>();
|
||||
int major, minor, patch;
|
||||
if (!parse_version(ver_str, major, minor, patch)) {
|
||||
ESP_LOGW(TAG, "Failed to parse version: %s", ver_str.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this version is compatible (not newer than host)
|
||||
if (compare_versions(major, minor, patch, ESP_HOSTED_VERSION_MAJOR_1, ESP_HOSTED_VERSION_MINOR_1,
|
||||
ESP_HOSTED_VERSION_PATCH_1) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is better than our current best
|
||||
if (best_major < 0 || compare_versions(major, minor, patch, best_major, best_minor, best_patch) > 0) {
|
||||
best_major = major;
|
||||
best_minor = minor;
|
||||
best_patch = patch;
|
||||
best_version = ver_str;
|
||||
best_url = entry["url"].as<std::string>();
|
||||
best_sha256 = entry["sha256"].as<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
if (best_major < 0) {
|
||||
ESP_LOGW(TAG, "No compatible firmware version found (host is %s)", ESP_HOSTED_VERSION_STR);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->update_info_.latest_version = best_version;
|
||||
this->firmware_url_ = best_url;
|
||||
|
||||
// Parse SHA256 hex string to bytes
|
||||
if (!parse_hex(best_sha256, this->firmware_sha256_.data(), 32)) {
|
||||
ESP_LOGE(TAG, "Invalid SHA256: %s", best_sha256.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Best compatible version: %s", this->update_info_.latest_version.c_str());
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse manifest JSON");
|
||||
this->status_set_error(LOG_STR("Failed to parse manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
ESP_LOGI(TAG, "Downloading firmware");
|
||||
|
||||
auto container = this->http_request_parent_->get(this->firmware_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch firmware");
|
||||
this->status_set_error(LOG_STR("Failed to fetch firmware"));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t total_size = container->content_length;
|
||||
ESP_LOGI(TAG, "Firmware size: %zu bytes", total_size);
|
||||
|
||||
// Begin OTA on coprocessor
|
||||
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Failed to begin OTA"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
|
||||
alignas(32) sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Failed to write OTA data"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
container->end();
|
||||
|
||||
// Verify SHA256
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
ESP_LOGE(TAG, "SHA256 mismatch");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->status_set_error(LOG_STR("SHA256 verification failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "SHA256 verified successfully");
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
|
||||
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
|
||||
ESP_LOGE(TAG, "No firmware data available");
|
||||
return;
|
||||
this->status_set_error(LOG_STR("No firmware data available"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ESP32-S3 hardware SHA acceleration requires 32-byte DMA alignment (IDF 5.5.x+)
|
||||
// Verify SHA256 before writing
|
||||
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
|
||||
alignas(32) sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
hasher.add(this->firmware_data_, this->firmware_size_);
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
ESP_LOGE(TAG, "SHA256 mismatch");
|
||||
this->status_set_error(LOG_STR("SHA256 verification failed"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
|
||||
|
||||
watchdog::WatchdogManager watchdog(20000);
|
||||
update::UpdateState prev_state = this->state_;
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = false;
|
||||
this->publish_state();
|
||||
|
||||
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to begin OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t chunk[CHUNK_SIZE];
|
||||
@@ -128,42 +381,68 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to write OTA data"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
data_ptr += chunk_size;
|
||||
remaining -= chunk_size;
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
err = esp_hosted_slave_ota_end(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Esp32HostedUpdate::perform(bool force) {
|
||||
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
|
||||
ESP_LOGW(TAG, "Update not available");
|
||||
return;
|
||||
}
|
||||
|
||||
update::UpdateState prev_state = this->state_;
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = false;
|
||||
this->publish_state();
|
||||
|
||||
watchdog::WatchdogManager watchdog(60000);
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
if (!this->stream_firmware_to_coprocessor_())
|
||||
#else
|
||||
if (!this->write_embedded_firmware_to_coprocessor_())
|
||||
#endif
|
||||
{
|
||||
this->state_ = prev_state;
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// End OTA and activate new firmware
|
||||
esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
|
||||
if (end_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to end OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// activate new firmware
|
||||
err = esp_hosted_slave_ota_activate(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
|
||||
esp_err_t activate_err = esp_hosted_slave_ota_activate(); // NOLINT
|
||||
if (activate_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(activate_err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to activate OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// update state
|
||||
// Update state
|
||||
ESP_LOGI(TAG, "OTA update successful");
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
|
||||
// schedule a restart to ensure everything is in sync
|
||||
// Schedule a restart to ensure everything is in sync
|
||||
ESP_LOGI(TAG, "Restarting in 1 second");
|
||||
this->set_timeout(1000, []() { App.safe_reboot(); });
|
||||
}
|
||||
|
||||
@@ -5,26 +5,55 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_hosted {
|
||||
|
||||
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
|
||||
class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void update() override { this->check(); } // PollingComponent - delegates to check()
|
||||
void perform(bool force) override;
|
||||
void check() override {}
|
||||
void check() override;
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode setters
|
||||
void set_source_url(const std::string &url) { this->source_url_ = url; }
|
||||
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
||||
#else
|
||||
// Embedded mode setters
|
||||
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
|
||||
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
|
||||
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode members
|
||||
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
||||
std::string source_url_;
|
||||
std::string firmware_url_;
|
||||
|
||||
// HTTP mode helpers
|
||||
bool fetch_manifest_();
|
||||
bool stream_firmware_to_coprocessor_();
|
||||
#else
|
||||
// Embedded mode members
|
||||
const uint8_t *firmware_data_{nullptr};
|
||||
size_t firmware_size_{0};
|
||||
std::array<uint8_t, 32> firmware_sha256_;
|
||||
|
||||
// Embedded mode helper
|
||||
bool write_embedded_firmware_to_coprocessor_();
|
||||
#endif
|
||||
|
||||
std::array<uint8_t, 32> firmware_sha256_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_hosted
|
||||
|
||||
@@ -136,7 +136,7 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
|
||||
// Water heater web_server support not yet implemented - this stub acknowledges the entity
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::water_heater_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_LOCAL
|
||||
#if USE_WEBSERVER_VERSION == 2
|
||||
#include "server_index_v2.h"
|
||||
@@ -558,7 +562,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
|
||||
|
||||
// Helper to get request detail parameter
|
||||
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
auto *param = request->getParam("detail");
|
||||
auto *param = request->getParam(ESPHOME_F("detail"));
|
||||
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
|
||||
}
|
||||
|
||||
@@ -837,10 +841,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
}
|
||||
auto call = is_on ? obj->turn_on() : obj->turn_off();
|
||||
|
||||
parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed);
|
||||
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||
|
||||
if (request->hasParam("oscillation")) {
|
||||
auto speed = request->getParam("oscillation")->value();
|
||||
if (request->hasParam(ESPHOME_F("oscillation"))) {
|
||||
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
|
||||
auto val = parse_on_off(speed.c_str());
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -920,20 +924,20 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (is_on) {
|
||||
// Parse color parameters
|
||||
parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f);
|
||||
parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f);
|
||||
parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f);
|
||||
parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f);
|
||||
parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f);
|
||||
parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature);
|
||||
parse_light_param_(request, ESPHOME_F("brightness"), call, &decltype(call)::set_brightness, 255.0f);
|
||||
parse_light_param_(request, ESPHOME_F("r"), call, &decltype(call)::set_red, 255.0f);
|
||||
parse_light_param_(request, ESPHOME_F("g"), call, &decltype(call)::set_green, 255.0f);
|
||||
parse_light_param_(request, ESPHOME_F("b"), call, &decltype(call)::set_blue, 255.0f);
|
||||
parse_light_param_(request, ESPHOME_F("white_value"), call, &decltype(call)::set_white, 255.0f);
|
||||
parse_light_param_(request, ESPHOME_F("color_temp"), call, &decltype(call)::set_color_temperature);
|
||||
|
||||
// Parse timing parameters
|
||||
parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000);
|
||||
parse_light_param_uint_(request, ESPHOME_F("flash"), call, &decltype(call)::set_flash_length, 1000);
|
||||
}
|
||||
parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000);
|
||||
parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000);
|
||||
|
||||
if (is_on) {
|
||||
parse_string_param_(request, "effect", call, &decltype(call)::set_effect);
|
||||
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
|
||||
}
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
@@ -1016,14 +1020,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if ((request->hasParam("position") && !traits.get_supports_position()) ||
|
||||
(request->hasParam("tilt") && !traits.get_supports_tilt())) {
|
||||
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_float_param_(request, "position", call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt);
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1082,7 +1086,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_float_param_(request, "value", call, &decltype(call)::set_value);
|
||||
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1150,12 +1154,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
if (!request->hasParam("value")) {
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_date);
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1214,12 +1218,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
if (!request->hasParam("value")) {
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_time);
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1277,12 +1281,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
if (!request->hasParam("value")) {
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_datetime);
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1342,7 +1346,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_value);
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1400,7 +1404,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_string_param_(request, "option", call, &decltype(call)::set_option);
|
||||
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1460,14 +1464,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
auto call = obj->make_call();
|
||||
|
||||
// Parse string mode parameters
|
||||
parse_string_param_(request, "mode", call, &decltype(call)::set_mode);
|
||||
parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode);
|
||||
parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode);
|
||||
parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode);
|
||||
parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode);
|
||||
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high);
|
||||
parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low);
|
||||
parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
&decltype(call)::set_target_temperature_high);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1706,12 +1711,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if (request->hasParam("position") && !traits.get_supports_position()) {
|
||||
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_float_param_(request, "position", call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1764,7 +1769,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_string_param_(request, "code", call, &decltype(call)::set_code);
|
||||
parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code);
|
||||
|
||||
// Lookup table for alarm control panel methods
|
||||
static const struct {
|
||||
@@ -1825,6 +1830,116 @@ std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmContr
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
void WebServer::on_water_heater_update(water_heater::WaterHeater *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", water_heater_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (water_heater::WaterHeater *obj : App.get_water_heaters()) {
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->water_heater_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
if (!match.method_equals("set")) {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
auto call = obj->make_call();
|
||||
// Use base class reference for template deduction (make_call returns WaterHeaterCallInternal)
|
||||
water_heater::WaterHeaterCall &base_call = call;
|
||||
|
||||
// Parse mode parameter
|
||||
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||
|
||||
// Parse away mode parameter
|
||||
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
|
||||
|
||||
// Parse on/off parameter
|
||||
parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
|
||||
}
|
||||
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
|
||||
}
|
||||
std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
char buf[PSTR_LOCAL_SIZE];
|
||||
|
||||
const auto mode = obj->get_mode();
|
||||
const char *mode_s = PSTR_LOCAL(water_heater::water_heater_mode_to_string(mode));
|
||||
|
||||
set_json_icon_state_value(root, obj, "water_heater", mode_s, mode, start_config);
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray modes = root[ESPHOME_F("modes")].to<JsonArray>();
|
||||
for (auto m : traits.get_supported_modes())
|
||||
modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m)));
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
float current = obj->get_current_temperature();
|
||||
if (!std::isnan(current))
|
||||
root[ESPHOME_F("current_temperature")] = current;
|
||||
}
|
||||
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
float low = obj->get_target_temperature_low();
|
||||
float high = obj->get_target_temperature_high();
|
||||
if (!std::isnan(low))
|
||||
root[ESPHOME_F("target_temperature_low")] = low;
|
||||
if (!std::isnan(high))
|
||||
root[ESPHOME_F("target_temperature_high")] = high;
|
||||
} else {
|
||||
float target = obj->get_target_temperature();
|
||||
if (!std::isnan(target))
|
||||
root[ESPHOME_F("target_temperature")] = target;
|
||||
}
|
||||
|
||||
root[ESPHOME_F("min_temperature")] = traits.get_min_temperature();
|
||||
root[ESPHOME_F("max_temperature")] = traits.get_max_temperature();
|
||||
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
|
||||
|
||||
if (traits.get_supports_away_mode()) {
|
||||
root[ESPHOME_F("away")] = obj->is_away();
|
||||
}
|
||||
|
||||
if (traits.has_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF)) {
|
||||
root[ESPHOME_F("is_on")] = obj->is_on();
|
||||
}
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void WebServer::on_event(event::Event *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
@@ -2060,6 +2175,9 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
"update",
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
"water_heater",
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -2220,6 +2338,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||
else if (match.domain_equals("update")) {
|
||||
this->handle_update_request(request, match);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
else if (match.domain_equals("water_heater")) {
|
||||
this->handle_water_heater_request(request, match);
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
// No matching handler found - send 404
|
||||
|
||||
@@ -35,6 +35,13 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
// Type for parameter names that can be stored in flash on ESP8266
|
||||
#ifdef USE_ESP8266
|
||||
using ParamNameType = const __FlashStringHelper *;
|
||||
#else
|
||||
using ParamNameType = const char *;
|
||||
#endif
|
||||
|
||||
/// Result of matching a URL against an entity
|
||||
struct EntityMatchResult {
|
||||
bool matched; ///< True if entity matched the URL
|
||||
@@ -429,6 +436,16 @@ class WebServer : public Controller,
|
||||
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj) override;
|
||||
|
||||
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
|
||||
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static std::string water_heater_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
|
||||
@@ -472,7 +489,7 @@ class WebServer : public Controller,
|
||||
#ifdef USE_LIGHT
|
||||
// Helper to parse and apply a float parameter with optional scaling
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float),
|
||||
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
|
||||
float scale = 1.0f) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
@@ -484,7 +501,7 @@ class WebServer : public Controller,
|
||||
|
||||
// Helper to parse and apply a uint32_t parameter with optional scaling
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call,
|
||||
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
|
||||
@@ -497,7 +514,7 @@ class WebServer : public Controller,
|
||||
|
||||
// Generic helper to parse and apply a float parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) {
|
||||
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
@@ -508,7 +525,7 @@ class WebServer : public Controller,
|
||||
|
||||
// Generic helper to parse and apply an int parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) {
|
||||
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
@@ -519,7 +536,7 @@ class WebServer : public Controller,
|
||||
|
||||
// Generic helper to parse and apply a string parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call,
|
||||
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(const std::string &)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
@@ -528,6 +545,16 @@ class WebServer : public Controller,
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper to parse and apply a bool parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto param_value = request->getParam(param_name)->value();
|
||||
bool value = param_value == "true" || param_value == "1";
|
||||
(call.*setter)(value);
|
||||
}
|
||||
}
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
#ifdef USE_ESP32
|
||||
AsyncEventSource events_{"/events", this};
|
||||
@@ -606,6 +633,9 @@ class WebServer : public Controller,
|
||||
#ifdef USE_EVENT
|
||||
std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
|
||||
@@ -232,6 +232,13 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
for (auto *obj : App.get_water_heaters()) {
|
||||
if (this->include_internal_ || !obj->is_internal())
|
||||
write_row(stream, obj, "water_heater", "");
|
||||
}
|
||||
#endif
|
||||
|
||||
stream->print(ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/\">ESPHome Web API</a> for "
|
||||
"REST API documentation.</p>"));
|
||||
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
|
||||
|
||||
@@ -69,7 +69,7 @@ def validate_number_of_ep(config: ConfigType) -> None:
|
||||
raise cv.Invalid(
|
||||
"Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888"
|
||||
)
|
||||
if count > CONF_MAX_EP_NUMBER:
|
||||
if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode:
|
||||
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}")
|
||||
|
||||
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
update:
|
||||
- platform: esp32_hosted
|
||||
name: "Coprocessor Firmware Update"
|
||||
type: embedded
|
||||
path: $component_dir/test_firmware.bin
|
||||
sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31
|
||||
10
tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml
Normal file
10
tests/components/esp32_hosted/test-http.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
http_request:
|
||||
|
||||
update:
|
||||
- platform: esp32_hosted
|
||||
name: "Coprocessor Firmware Update"
|
||||
type: http
|
||||
source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json
|
||||
update_interval: 6h
|
||||
Reference in New Issue
Block a user