mirror of
https://github.com/esphome/esphome.git
synced 2026-02-13 13:07:41 -07:00
Compare commits
2 Commits
integratio
...
fix-esp826
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de76dfd117 | ||
|
|
903971de12 |
@@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import heapq
|
||||
import json
|
||||
from operator import itemgetter
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -541,28 +540,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Export analysis results as JSON."""
|
||||
data = {
|
||||
"components": {
|
||||
name: {
|
||||
"text": mem.text_size,
|
||||
"rodata": mem.rodata_size,
|
||||
"data": mem.data_size,
|
||||
"bss": mem.bss_size,
|
||||
"flash_total": mem.flash_total,
|
||||
"ram_total": mem.ram_total,
|
||||
"symbol_count": mem.symbol_count,
|
||||
}
|
||||
for name, mem in self.components.items()
|
||||
},
|
||||
"totals": {
|
||||
"flash": sum(c.flash_total for c in self.components.values()),
|
||||
"ram": sum(c.ram_total for c in self.components.values()),
|
||||
},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
"""Dump uncategorized symbols for analysis."""
|
||||
# Sort by size descending
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -524,24 +524,24 @@ async def homeassistant_service_to_code(
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
@@ -609,24 +609,24 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -649,11 +649,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||
cg.add(var.set_service("esphome.tag_scanned"))
|
||||
# Initialize FixedVector with exact size (1 data field)
|
||||
cg.add(var.init_data(1))
|
||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
|
||||
cg.add(var.add_data("tag_id", templ))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -128,20 +128,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
|
||||
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
|
||||
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
template<typename T> void set_response_template(T response_template) {
|
||||
this->response_template_ = response_template;
|
||||
@@ -233,31 +219,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, keys may be in PROGMEM (from ESPHOME_F in codegen) and
|
||||
// FLASH_STRING values need copying via _P functions.
|
||||
// Allocate storage for all keys + all values (2 entries per source item).
|
||||
// strlen_P/memcpy_P handle both RAM and PROGMEM pointers safely.
|
||||
value_storage.init(source.size() * 2);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
|
||||
// Key: copy from possible PROGMEM
|
||||
{
|
||||
size_t key_len = strlen_P(it.key);
|
||||
value_storage.push_back(std::string(key_len, '\0'));
|
||||
memcpy_P(value_storage.back().data(), it.key, key_len);
|
||||
kv.key = StringRef(value_storage.back());
|
||||
}
|
||||
|
||||
// Value: value() handles FLASH_STRING via _P functions internally
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
#else
|
||||
// On non-ESP8266, strings are directly readable from flash-mapped memory.
|
||||
// Count non-static strings to allocate exact storage needed.
|
||||
// Count non-static strings to allocate exact storage needed
|
||||
size_t lambda_count = 0;
|
||||
for (const auto &it : source) {
|
||||
if (!it.value.is_static_string()) {
|
||||
@@ -271,15 +233,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string — pointer directly readable, zero allocation
|
||||
// Static string from YAML - zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda — evaluate and store result
|
||||
// Lambda evaluation - store result, reference it
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
|
||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
auto json_buf = builder.serialize();
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "bedjet_hub.h"
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cinttypes>
|
||||
|
||||
|
||||
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a basic display schema with configurable default update interval."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema(default_update_interval))
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config):
|
||||
@@ -83,41 +81,34 @@ def _validate_test_card(config):
|
||||
return config
|
||||
|
||||
|
||||
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a full display schema with configurable default update interval."""
|
||||
schema = basic_display_schema(default_update_interval).extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
schema.add_extra(_validate_test_card)
|
||||
return schema
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
|
||||
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||
|
||||
|
||||
async def setup_display_core_(var, config):
|
||||
|
||||
@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
break;
|
||||
}
|
||||
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
if (!isr_service_installed) {
|
||||
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
||||
if (res != ESP_OK) {
|
||||
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
isr_service_installed = true;
|
||||
}
|
||||
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
}
|
||||
|
||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
|
||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -124,10 +133,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -135,7 +144,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
}
|
||||
@@ -167,7 +176,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -177,7 +186,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -28,7 +28,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
|
||||
@@ -68,7 +68,7 @@ void FanCall::validate_() {
|
||||
auto traits = this->parent_.get_traits();
|
||||
|
||||
if (this->speed_.has_value()) {
|
||||
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
|
||||
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
||||
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace fan {
|
||||
class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
@@ -23,9 +23,9 @@ class FanTraits {
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
uint8_t supported_speed_count() const { return this->speed_count_; }
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
@@ -64,7 +64,7 @@ class FanTraits {
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ enum DecayMode {
|
||||
|
||||
class HBridgeFan : public Component, public fan::Fan {
|
||||
public:
|
||||
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *pin_b_;
|
||||
output::FloatOutput *enable_{nullptr};
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ static const char *const TAG = "json";
|
||||
static SpiRamAllocator global_json_allocator;
|
||||
#endif
|
||||
|
||||
SerializationBuffer<> build_json(const json_build_t &f) {
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -66,62 +66,14 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
// ===========================================================================================
|
||||
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
|
||||
// ===========================================================================================
|
||||
//
|
||||
// This function is carefully structured to enable NRVO. The compiler constructs `result`
|
||||
// directly in the caller's stack frame, eliminating the move constructor call entirely.
|
||||
//
|
||||
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
|
||||
// must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON
|
||||
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
|
||||
//
|
||||
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
|
||||
// where the caller needs it.
|
||||
//
|
||||
// Requirements for NRVO to work:
|
||||
// 1. Single named variable (`result`) returned from ALL paths
|
||||
// 2. All paths must return the SAME variable (not different variables)
|
||||
// 3. No std::move() on the return statement
|
||||
//
|
||||
// If you must modify this function:
|
||||
// - Keep a single `result` variable declared at the top
|
||||
// - All code paths must return `result` (not a different variable)
|
||||
// - Verify NRVO still works by checking the disassembly for move constructor calls
|
||||
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
|
||||
// Should show only destructor, NOT move constructor
|
||||
//
|
||||
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
|
||||
// Instead, try stack buffer first. 512 bytes covers 99.9% of JSON payloads (sensors ~200B,
|
||||
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
|
||||
//
|
||||
// ===========================================================================================
|
||||
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
|
||||
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
|
||||
|
||||
std::string JsonBuilder::serialize() {
|
||||
if (doc_.overflowed()) {
|
||||
ESP_LOGE(TAG, "JSON document overflow");
|
||||
auto *buf = result.data_writable_();
|
||||
buf[0] = '{';
|
||||
buf[1] = '}';
|
||||
buf[2] = '\0';
|
||||
result.set_size_(2);
|
||||
return result;
|
||||
return "{}";
|
||||
}
|
||||
|
||||
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
|
||||
if (size < buf_size) {
|
||||
// Fits in stack buffer - update size to actual length
|
||||
result.set_size_(size);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Needs heap allocation - reallocate and serialize again with exact size
|
||||
result.reallocate_heap_(size);
|
||||
serializeJson(doc_, result.data_writable_(), size + 1);
|
||||
return result;
|
||||
std::string output;
|
||||
serializeJson(doc_, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -16,108 +14,6 @@
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
/// Buffer for JSON serialization that uses stack allocation for small payloads.
|
||||
/// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes).
|
||||
/// Supports move semantics for efficient return-by-value.
|
||||
template<size_t STACK_SIZE = 512> class SerializationBuffer {
|
||||
public:
|
||||
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
|
||||
|
||||
/// Construct with known size (typically from measureJson)
|
||||
explicit SerializationBuffer(size_t size) : size_(size) {
|
||||
if (size + 1 <= STACK_SIZE) {
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
}
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
~SerializationBuffer() { delete[] heap_buffer_; }
|
||||
|
||||
// Move constructor - works with same template instantiation
|
||||
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
// Stack buffer - must copy content
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
// Heap buffer - steal ownership
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
|
||||
if (this != &other) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = other.heap_buffer_;
|
||||
size_ = other.size_;
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Delete copy operations
|
||||
SerializationBuffer(const SerializationBuffer &) = delete;
|
||||
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
|
||||
|
||||
/// Get null-terminated C string
|
||||
const char *c_str() const { return buffer_; }
|
||||
/// Get data pointer
|
||||
const char *data() const { return buffer_; }
|
||||
/// Get string length (excluding null terminator)
|
||||
size_t size() const { return size_; }
|
||||
|
||||
/// Implicit conversion to std::string for backward compatibility
|
||||
/// WARNING: This allocates a new std::string on the heap. Prefer using
|
||||
/// c_str() or data()/size() directly when possible to avoid allocation.
|
||||
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
|
||||
|
||||
private:
|
||||
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
|
||||
|
||||
/// Get writable buffer (for serialization)
|
||||
char *data_writable_() { return buffer_; }
|
||||
/// Set actual size after serialization (must not exceed allocated size)
|
||||
/// Also ensures null termination for c_str() safety
|
||||
void set_size_(size_t size) {
|
||||
size_ = size;
|
||||
buffer_[size] = '\0';
|
||||
}
|
||||
|
||||
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
|
||||
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
|
||||
void reallocate_heap_(size_t size) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
size_ = size;
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
char stack_buffer_[STACK_SIZE];
|
||||
char *heap_buffer_{nullptr};
|
||||
char *buffer_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
#ifdef USE_PSRAM
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
// This is only compiled when PSRAM is enabled
|
||||
@@ -150,8 +46,7 @@ using json_parse_t = std::function<bool(JsonObject)>;
|
||||
using json_build_t = std::function<void(JsonObject)>;
|
||||
|
||||
/// Build a JSON string with the provided json build function.
|
||||
/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
|
||||
SerializationBuffer<> build_json(const json_build_t &f);
|
||||
std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
@@ -176,9 +71,7 @@ class JsonBuilder {
|
||||
return root_;
|
||||
}
|
||||
|
||||
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
|
||||
/// Uses 512-byte stack buffer by default, falls back to heap for larger JSON
|
||||
SerializationBuffer<> serialize();
|
||||
std::string serialize();
|
||||
|
||||
private:
|
||||
#ifdef USE_PSRAM
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -114,11 +123,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
if (err != FDB_NO_ERR) {
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -126,7 +135,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +160,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Check size first - if different, data has changed
|
||||
if (kv.value_len != to_save.data.size()) {
|
||||
if (kv.value_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -165,7 +174,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Compare the actual data
|
||||
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
default:
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#if defined(USE_HOST)
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
}
|
||||
|
||||
void Logger::pre_setup() { global_logger = this; }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2040
|
||||
@@ -1,96 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
uint32_t dtr = 0;
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
|
||||
/* Poll if the DTR flag was set, optional */
|
||||
if (opened == dtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
|
||||
} else {
|
||||
this->uart_dev_ = uart_dev;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
if (nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
while (*msg) {
|
||||
uart_poll_out(this->uart_dev_, *msg);
|
||||
++msg;
|
||||
}
|
||||
uart_poll_out(this->uart_dev_, '\n');
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
@@ -540,8 +540,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
|
||||
auto message = json::build_json(f);
|
||||
return this->publish(topic, message.c_str(), message.size(), qos, retain);
|
||||
std::string message = json::build_json(f);
|
||||
return this->publish(topic, message.c_str(), message.length(), qos, retain);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::enable() {
|
||||
|
||||
@@ -2,97 +2,34 @@ import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
|
||||
from esphome.components import runtime_image
|
||||
from esphome.components.const import CONF_REQUEST_HEADERS
|
||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||
from esphome.components.image import (
|
||||
CONF_INVERT_ALPHA,
|
||||
CONF_TRANSPARENCY,
|
||||
IMAGE_SCHEMA,
|
||||
Image_,
|
||||
get_image_type_enum,
|
||||
get_transparency_enum,
|
||||
validate_settings,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
CONF_RESIZE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import Lambda
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
AUTO_LOAD = ["image", "runtime_image"]
|
||||
DEPENDENCIES = ["display", "http_request"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_PLACEHOLDER = "placeholder"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
|
||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||
|
||||
|
||||
class Format:
|
||||
def __init__(self, image_type):
|
||||
self.image_type = image_type
|
||||
|
||||
@property
|
||||
def enum(self):
|
||||
return getattr(ImageFormat, self.image_type)
|
||||
|
||||
def actions(self):
|
||||
pass
|
||||
|
||||
|
||||
class BMPFormat(Format):
|
||||
def __init__(self):
|
||||
super().__init__("BMP")
|
||||
|
||||
def actions(self):
|
||||
cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT")
|
||||
|
||||
|
||||
class JPEGFormat(Format):
|
||||
def __init__(self):
|
||||
super().__init__("JPEG")
|
||||
|
||||
def actions(self):
|
||||
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
|
||||
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
|
||||
|
||||
|
||||
class PNGFormat(Format):
|
||||
def __init__(self):
|
||||
super().__init__("PNG")
|
||||
|
||||
def actions(self):
|
||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
||||
cg.add_library("pngle", "1.1.0")
|
||||
|
||||
|
||||
IMAGE_FORMATS = {
|
||||
x.image_type: x
|
||||
for x in (
|
||||
BMPFormat(),
|
||||
JPEGFormat(),
|
||||
PNGFormat(),
|
||||
)
|
||||
}
|
||||
IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]})
|
||||
|
||||
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
||||
OnlineImage = online_image_ns.class_(
|
||||
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
|
||||
)
|
||||
|
||||
# Actions
|
||||
SetUrlAction = online_image_ns.class_(
|
||||
@@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
def remove_options(*options):
|
||||
return {
|
||||
cv.Optional(option): cv.invalid(
|
||||
f"{option} is an invalid option for online_image"
|
||||
)
|
||||
for option in options
|
||||
}
|
||||
|
||||
|
||||
ONLINE_IMAGE_SCHEMA = (
|
||||
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
|
||||
runtime_image.runtime_image_schema(OnlineImage)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
# Online Image specific options
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
|
||||
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
host=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_settings,
|
||||
runtime_image.validate_runtime_image_settings,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
|
||||
image_format.actions()
|
||||
# Use the enhanced helper function to get all runtime image parameters
|
||||
settings = await runtime_image.process_runtime_image_config(config)
|
||||
|
||||
url = config[CONF_URL]
|
||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||
transparent = get_transparency_enum(config[CONF_TRANSPARENCY])
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
image_format.enum,
|
||||
get_image_type_enum(config[CONF_TYPE]),
|
||||
transparent,
|
||||
settings.width,
|
||||
settings.height,
|
||||
settings.format_enum,
|
||||
settings.image_type_enum,
|
||||
settings.transparent,
|
||||
settings.placeholder or cg.nullptr,
|
||||
config[CONF_BUFFER_SIZE],
|
||||
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
||||
settings.byte_order_big_endian,
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
@@ -227,10 +150,6 @@ async def to_code(config):
|
||||
else:
|
||||
cg.add(var.add_request_header(key, value))
|
||||
|
||||
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
||||
placeholder = await cg.get_variable(placeholder_id)
|
||||
cg.add(var.set_placeholder(placeholder))
|
||||
|
||||
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(bool, "cached")], conf)
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
#include "image_decoder.h"
|
||||
#include "online_image.h"
|
||||
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::online_image {
|
||||
|
||||
static const char *const TAG = "online_image.decoder";
|
||||
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
bool success = this->image_->resize_(width, height) > 0;
|
||||
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
|
||||
return success;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
this->image_->draw_pixel_(i, j, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
static const char *const TAG = "online_image.download_buffer";
|
||||
|
||||
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
|
||||
this->buffer_ = this->allocator_.allocate(size);
|
||||
@@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) {
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::read(size_t len) {
|
||||
this->unread_ -= len;
|
||||
if (this->unread_ > 0) {
|
||||
memmove(this->data(), this->data(len), this->unread_);
|
||||
if (len >= this->unread_) {
|
||||
this->unread_ = 0;
|
||||
return 0;
|
||||
}
|
||||
this->unread_ -= len;
|
||||
memmove(this->data(), this->data(len), this->unread_);
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
@@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::online_image
|
||||
44
esphome/components/online_image/download_buffer.h
Normal file
44
esphome/components/online_image/download_buffer.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::online_image {
|
||||
|
||||
/**
|
||||
* @brief Buffer for managing downloaded data.
|
||||
*
|
||||
* This class provides a buffer for downloading data with tracking of
|
||||
* unread bytes and dynamic resizing capabilities.
|
||||
*/
|
||||
class DownloadBuffer {
|
||||
public:
|
||||
DownloadBuffer(size_t size);
|
||||
~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
||||
|
||||
uint8_t *data(size_t offset = 0);
|
||||
uint8_t *append() { return this->data(this->unread_); }
|
||||
|
||||
size_t unread() const { return this->unread_; }
|
||||
size_t size() const { return this->size_; }
|
||||
size_t free_capacity() const { return this->size_ - this->unread_; }
|
||||
|
||||
size_t read(size_t len);
|
||||
size_t write(size_t len) {
|
||||
this->unread_ += len;
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
size_t resize(size_t size);
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_{};
|
||||
uint8_t *buffer_;
|
||||
size_t size_;
|
||||
/** Total number of downloaded bytes not yet read. */
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
|
||||
static const char *const TAG = "online_image";
|
||||
static const char *const ETAG_HEADER_NAME = "etag";
|
||||
@@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match";
|
||||
static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
|
||||
static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||
|
||||
#include "image_decoder.h"
|
||||
namespace esphome::online_image {
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#include "bmp_image.h"
|
||||
#endif
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#include "jpeg_image.h"
|
||||
#endif
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include "png_image.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
using image::ImageType;
|
||||
|
||||
inline bool is_color_on(const Color &color) {
|
||||
// This produces the most accurate monochrome conversion, but is slightly slower.
|
||||
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
||||
|
||||
// Approximation using fast integer computations; produces acceptable results
|
||||
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
||||
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
||||
}
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
buffer_(nullptr),
|
||||
download_buffer_(download_buffer_size),
|
||||
download_buffer_initial_size_(download_buffer_size),
|
||||
format_(format),
|
||||
fixed_width_(width),
|
||||
fixed_height_(height),
|
||||
is_big_endian_(is_big_endian) {
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||
uint32_t buffer_size, bool is_big_endian)
|
||||
: RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height),
|
||||
download_buffer_(buffer_size),
|
||||
download_buffer_initial_size_(buffer_size) {
|
||||
this->set_url(url);
|
||||
}
|
||||
|
||||
void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
if (this->data_start_) {
|
||||
Image::draw(x, y, display, color_on, color_off);
|
||||
} else if (this->placeholder_) {
|
||||
this->placeholder_->draw(x, y, display, color_on, color_off);
|
||||
bool OnlineImage::validate_url_(const std::string &url) {
|
||||
if (url.empty()) {
|
||||
ESP_LOGE(TAG, "URL is empty");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::release() {
|
||||
if (this->buffer_) {
|
||||
ESP_LOGV(TAG, "Deallocating old buffer");
|
||||
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
||||
this->data_start_ = nullptr;
|
||||
this->buffer_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
this->last_modified_ = "";
|
||||
this->etag_ = "";
|
||||
this->end_connection_();
|
||||
if (url.length() > 2048) {
|
||||
ESP_LOGE(TAG, "URL is too long");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t OnlineImage::resize_(int width_in, int height_in) {
|
||||
int width = this->fixed_width_;
|
||||
int height = this->fixed_height_;
|
||||
if (this->is_auto_resize_()) {
|
||||
width = width_in;
|
||||
height = height_in;
|
||||
if (this->width_ != width && this->height_ != height) {
|
||||
this->release();
|
||||
}
|
||||
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
|
||||
ESP_LOGE(TAG, "URL must start with http:// or https://");
|
||||
return false;
|
||||
}
|
||||
size_t new_size = this->get_buffer_size_(width, height);
|
||||
if (this->buffer_) {
|
||||
// Buffer already allocated => no need to resize
|
||||
return new_size;
|
||||
}
|
||||
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
|
||||
this->buffer_ = this->allocator_.allocate(new_size);
|
||||
if (this->buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
|
||||
this->allocator_.get_max_free_block_size());
|
||||
this->end_connection_();
|
||||
return 0;
|
||||
}
|
||||
this->buffer_width_ = width;
|
||||
this->buffer_height_ = height;
|
||||
this->width_ = width;
|
||||
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
|
||||
return new_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnlineImage::update() {
|
||||
if (this->decoder_) {
|
||||
if (this->is_decoding()) {
|
||||
ESP_LOGW(TAG, "Image already being updated.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
|
||||
|
||||
std::list<http_request::Header> headers = {};
|
||||
|
||||
http_request::Header accept_header;
|
||||
accept_header.name = "Accept";
|
||||
std::string accept_mime_type;
|
||||
switch (this->format_) {
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
case ImageFormat::BMP:
|
||||
accept_mime_type = "image/bmp";
|
||||
break;
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
case ImageFormat::JPEG:
|
||||
accept_mime_type = "image/jpeg";
|
||||
break;
|
||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
case ImageFormat::PNG:
|
||||
accept_mime_type = "image/png";
|
||||
break;
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
default:
|
||||
accept_mime_type = "image/*";
|
||||
if (!this->validate_url_(this->url_)) {
|
||||
ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str());
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
accept_header.value = accept_mime_type + ",*/*;q=0.8";
|
||||
|
||||
ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str());
|
||||
|
||||
std::list<http_request::Header> headers;
|
||||
|
||||
// Add caching headers if we have them
|
||||
if (!this->etag_.empty()) {
|
||||
headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_});
|
||||
headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_});
|
||||
}
|
||||
|
||||
if (!this->last_modified_.empty()) {
|
||||
headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
|
||||
headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
|
||||
}
|
||||
|
||||
headers.push_back(accept_header);
|
||||
// Add Accept header based on image format
|
||||
const char *accept_mime_type;
|
||||
switch (this->get_format()) {
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
case runtime_image::BMP:
|
||||
accept_mime_type = "image/bmp,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
case runtime_image::JPEG:
|
||||
accept_mime_type = "image/jpeg,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
case runtime_image::PNG:
|
||||
accept_mime_type = "image/png,*/*;q=0.8";
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
accept_mime_type = "image/*,*/*;q=0.8";
|
||||
break;
|
||||
}
|
||||
headers.push_back({"Accept", accept_mime_type});
|
||||
|
||||
// User headers last so they can override any of the above
|
||||
for (auto &header : this->request_headers_) {
|
||||
headers.push_back(http_request::Header{header.first, header.second.value()});
|
||||
}
|
||||
@@ -175,186 +115,117 @@ void OnlineImage::update() {
|
||||
ESP_LOGD(TAG, "Starting download");
|
||||
size_t total_size = this->downloader_->content_length;
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
if (this->format_ == ImageFormat::BMP) {
|
||||
ESP_LOGD(TAG, "Allocating BMP decoder");
|
||||
this->decoder_ = make_unique<BmpDecoder>(this);
|
||||
this->enable_loop();
|
||||
// Initialize decoder with the known format
|
||||
if (!this->begin_decode(total_size)) {
|
||||
ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
if (this->format_ == ImageFormat::JPEG) {
|
||||
ESP_LOGD(TAG, "Allocating JPEG decoder");
|
||||
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
|
||||
this->enable_loop();
|
||||
}
|
||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
if (this->format_ == ImageFormat::PNG) {
|
||||
ESP_LOGD(TAG, "Allocating PNG decoder");
|
||||
this->decoder_ = make_unique<PngDecoder>(this);
|
||||
this->enable_loop();
|
||||
}
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
if (!this->decoder_) {
|
||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
auto prepare_result = this->decoder_->prepare(total_size);
|
||||
if (prepare_result < 0) {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
// JPEG requires the complete image in the download buffer before decoding
|
||||
if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) {
|
||||
this->download_buffer_.resize(total_size);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = ::time(nullptr);
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void OnlineImage::loop() {
|
||||
if (!this->decoder_) {
|
||||
if (!this->is_decoding()) {
|
||||
// Not decoding at the moment => nothing to do.
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||
this->data_start_ = buffer_;
|
||||
this->width_ = buffer_width_;
|
||||
this->height_ = buffer_height_;
|
||||
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
|
||||
this->width_, this->height_);
|
||||
ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_));
|
||||
|
||||
if (!this->downloader_) {
|
||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is complete — use decoder's format-specific completion check
|
||||
// to handle both known content-length and chunked transfer encoding
|
||||
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
|
||||
this->downloader_->get_bytes_read() >= this->downloader_->content_length &&
|
||||
this->download_buffer_.unread() == 0)) {
|
||||
// Finalize decoding
|
||||
this->end_decode();
|
||||
|
||||
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
|
||||
(uint32_t) (::time(nullptr) - this->start_time_));
|
||||
|
||||
// Save caching headers
|
||||
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
|
||||
this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
|
||||
|
||||
this->download_finished_callback_.call(false);
|
||||
this->end_connection_();
|
||||
return;
|
||||
}
|
||||
if (this->downloader_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||
return;
|
||||
}
|
||||
|
||||
// Download and decode more data
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available) {
|
||||
// Some decoders need to fully download the image before downloading.
|
||||
// In case of huge images, don't wait blocking until the whole image has been downloaded,
|
||||
// use smaller chunks
|
||||
if (available > 0) {
|
||||
// Download in chunks to avoid blocking
|
||||
available = std::min(available, this->download_buffer_initial_size_);
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
|
||||
if (len > 0) {
|
||||
this->download_buffer_.write(len);
|
||||
auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error when decoding image.");
|
||||
|
||||
// Feed data to decoder
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
|
||||
if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %d", consumed);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->download_buffer_.read(fed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::map_chroma_key(Color &color) {
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||
color.g = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
color.r = 0;
|
||||
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
|
||||
color.b = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||
return;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
||||
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||
return;
|
||||
}
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
switch (this->type_) {
|
||||
case ImageType::IMAGE_TYPE_BINARY: {
|
||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||
pos = x + y * width_8;
|
||||
auto bitno = 0x80 >> (pos % 8u);
|
||||
pos /= 8u;
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
}
|
||||
break;
|
||||
} else if (len < 0) {
|
||||
ESP_LOGE(TAG, "Error downloading image: %d", len);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
gray = 1;
|
||||
}
|
||||
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
if (color.w != 0xFF)
|
||||
gray = color.w;
|
||||
}
|
||||
this->buffer_[pos] = gray;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
this->map_chroma_key(color);
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
if (this->is_big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
} else {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
}
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB: {
|
||||
this->map_chroma_key(color);
|
||||
this->buffer_[pos + 0] = color.r;
|
||||
this->buffer_[pos + 1] = color.g;
|
||||
this->buffer_[pos + 2] = color.b;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// Buffer is full, need to decode some data first
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
} else if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else {
|
||||
// Decoder can't process more data, might need complete image
|
||||
// This is normal for JPEG which needs complete data
|
||||
ESP_LOGV(TAG, "Decoder waiting for more data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::end_connection_() {
|
||||
// Abort any in-progress decode to free decoder resources.
|
||||
// Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
|
||||
if (this->is_decoding()) {
|
||||
RuntimeImage::release();
|
||||
}
|
||||
if (this->downloader_) {
|
||||
this->downloader_->end();
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->decoder_.reset();
|
||||
this->download_buffer_.reset();
|
||||
}
|
||||
|
||||
bool OnlineImage::validate_url_(const std::string &url) {
|
||||
if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
|
||||
ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
|
||||
@@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
|
||||
this->download_error_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
void OnlineImage::release() {
|
||||
// Clear cache headers
|
||||
this->etag_ = "";
|
||||
this->last_modified_ = "";
|
||||
|
||||
// End any active connection
|
||||
this->end_connection_();
|
||||
|
||||
// Call parent's release to free the image buffer
|
||||
RuntimeImage::release();
|
||||
}
|
||||
|
||||
} // namespace esphome::online_image
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::online_image {
|
||||
|
||||
using t_http_codes = enum {
|
||||
HTTP_CODE_OK = 200,
|
||||
@@ -17,27 +16,13 @@ using t_http_codes = enum {
|
||||
HTTP_CODE_NOT_FOUND = 404,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Format that the image is encoded with.
|
||||
*/
|
||||
enum ImageFormat {
|
||||
/** Automatically detect from MIME type. Not supported yet. */
|
||||
AUTO,
|
||||
/** JPEG format. */
|
||||
JPEG,
|
||||
/** PNG format. */
|
||||
PNG,
|
||||
/** BMP format. */
|
||||
BMP,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||
* need to re-download or re-decode.
|
||||
*/
|
||||
class OnlineImage : public PollingComponent,
|
||||
public image::Image,
|
||||
public runtime_image::RuntimeImage,
|
||||
public Parented<esphome::http_request::HttpRequestComponent> {
|
||||
public:
|
||||
/**
|
||||
@@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent,
|
||||
* @param url URL to download the image from.
|
||||
* @param width Desired width of the target image area.
|
||||
* @param height Desired height of the target image area.
|
||||
* @param format Format that the image is encoded in (@see ImageFormat).
|
||||
* @param format Format that the image is encoded in (@see runtime_image::ImageFormat).
|
||||
* @param type The pixel format for the image.
|
||||
* @param transparency The transparency type for the image.
|
||||
* @param placeholder Optional placeholder image to show while loading.
|
||||
* @param buffer_size Size of the buffer used to download the image.
|
||||
* @param is_big_endian Whether the image is stored in big-endian format.
|
||||
*/
|
||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type,
|
||||
image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size,
|
||||
bool is_big_endian = false);
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void map_chroma_key(Color &color);
|
||||
|
||||
/** Set the URL to download the image from. */
|
||||
void set_url(const std::string &url) {
|
||||
@@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent,
|
||||
|
||||
/** Add the request header */
|
||||
template<typename V> void add_request_header(const std::string &header, V value) {
|
||||
this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string> >(header, value));
|
||||
this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string>>(header, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the image that needs to be shown as long as the downloaded image
|
||||
* is not available.
|
||||
*
|
||||
* @param placeholder Pointer to the (@link Image) to show as placeholder.
|
||||
*/
|
||||
void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; }
|
||||
|
||||
/**
|
||||
* Release the buffer storing the image. The image will need to be downloaded again
|
||||
* to be able to be displayed.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Resize the download buffer
|
||||
*
|
||||
* @param size The new size for the download buffer.
|
||||
*/
|
||||
size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); }
|
||||
|
||||
void add_on_finished_callback(std::function<void(bool)> &&callback);
|
||||
void add_on_error_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
bool validate_url_(const std::string &url);
|
||||
|
||||
RAMAllocator<uint8_t> allocator_{};
|
||||
|
||||
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
|
||||
int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; }
|
||||
|
||||
int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
|
||||
|
||||
ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
|
||||
|
||||
/**
|
||||
* @brief Resize the image buffer to the requested dimensions.
|
||||
*
|
||||
* The buffer will be allocated if not existing.
|
||||
* If the dimensions have been fixed in the yaml config, the buffer will be created
|
||||
* with those dimensions and not resized, even on request.
|
||||
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
|
||||
* allocated
|
||||
*
|
||||
* @param width
|
||||
* @param height
|
||||
* @return 0 if no memory could be allocated, the size of the new buffer otherwise.
|
||||
*/
|
||||
size_t resize_(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Draw a pixel into the buffer.
|
||||
*
|
||||
* This is used by the decoder to fill the buffer that will later be displayed
|
||||
* by the `draw` method. This will internally convert the supplied 32 bit RGBA
|
||||
* color into the requested image storage format.
|
||||
*
|
||||
* @param x Horizontal pixel position.
|
||||
* @param y Vertical pixel position.
|
||||
* @param color 32 bit color to put into the pixel.
|
||||
*/
|
||||
void draw_pixel_(int x, int y, Color color);
|
||||
|
||||
void end_connection_();
|
||||
|
||||
CallbackManager<void(bool)> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
|
||||
uint8_t *buffer_;
|
||||
DownloadBuffer download_buffer_;
|
||||
/**
|
||||
* This is the *initial* size of the download buffer, not the current size.
|
||||
@@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent,
|
||||
*/
|
||||
size_t download_buffer_initial_size_;
|
||||
|
||||
const ImageFormat format_;
|
||||
image::Image *placeholder_{nullptr};
|
||||
|
||||
std::string url_{""};
|
||||
|
||||
std::vector<std::pair<std::string, TemplatableValue<std::string> > > request_headers_;
|
||||
std::vector<std::pair<std::string, TemplatableValue<std::string>>> request_headers_;
|
||||
|
||||
/** width requested on configuration, or 0 if non specified. */
|
||||
const int fixed_width_;
|
||||
/** height requested on configuration, or 0 if non specified. */
|
||||
const int fixed_height_;
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_;
|
||||
/**
|
||||
* Actual width of the current image. If fixed_width_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
* starts and the original size is known.
|
||||
* This needs to be separate from "BaseImage::get_width()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images).
|
||||
*/
|
||||
int buffer_width_;
|
||||
/**
|
||||
* Actual height of the current image. If fixed_height_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
* starts and the original size is known.
|
||||
* This needs to be separate from "BaseImage::get_height()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images).
|
||||
*/
|
||||
int buffer_height_;
|
||||
/**
|
||||
* The value of the ETag HTTP header provided in the last response.
|
||||
*/
|
||||
@@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent,
|
||||
std::string last_modified_ = "";
|
||||
|
||||
time_t start_time_;
|
||||
|
||||
friend bool ImageDecoder::set_size(int width, int height);
|
||||
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
|
||||
};
|
||||
|
||||
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||
@@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::online_image
|
||||
|
||||
@@ -76,7 +76,7 @@ class PN532 : public PollingComponent {
|
||||
|
||||
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
|
||||
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
|
||||
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
|
||||
@@ -88,7 +88,7 @@ class PN532 : public PollingComponent {
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
||||
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
|
||||
bool clean_mifare_ultralight_();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn532.h"
|
||||
@@ -107,10 +106,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u
|
||||
}
|
||||
|
||||
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> blank_buffer(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> trailer_buffer(
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
bool error = false;
|
||||
|
||||
@@ -119,20 +118,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
||||
continue;
|
||||
}
|
||||
if (block != 0) {
|
||||
if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||
if (!this->write_mifare_classic_block_(block, blank_buffer)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
||||
error = true;
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
||||
error = true;
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block + 3);
|
||||
error = true;
|
||||
}
|
||||
@@ -142,28 +141,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
||||
}
|
||||
|
||||
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> empty_ndef_message(
|
||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> blank_block(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> block_1_data(
|
||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_2_data(
|
||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_3_trailer(
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
std::vector<uint8_t> ndef_trailer(
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) {
|
||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!");
|
||||
return false;
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()))
|
||||
if (!this->write_mifare_classic_block_(1, block_1_data))
|
||||
return false;
|
||||
if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()))
|
||||
if (!this->write_mifare_classic_block_(2, block_2_data))
|
||||
return false;
|
||||
if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()))
|
||||
if (!this->write_mifare_classic_block_(3, block_3_trailer))
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "Sector 0 formatted to NDEF");
|
||||
@@ -173,36 +172,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
|
||||
return false;
|
||||
}
|
||||
if (block == 4) {
|
||||
if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) {
|
||||
if (!this->write_mifare_classic_block_(block, empty_ndef_message)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||
}
|
||||
} else {
|
||||
if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||
if (!this->write_mifare_classic_block_(block, blank_block)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||
}
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 1, blank_block)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 2, blank_block)) {
|
||||
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
||||
}
|
||||
if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) {
|
||||
if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) {
|
||||
ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
std::vector<uint8_t> cmd({
|
||||
bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
||||
std::vector<uint8_t> data({
|
||||
PN532_COMMAND_INDATAEXCHANGE,
|
||||
0x01, // One card
|
||||
nfc::MIFARE_CMD_WRITE,
|
||||
block_num,
|
||||
});
|
||||
cmd.insert(cmd.end(), data, data + len);
|
||||
if (!this->write_command_(cmd)) {
|
||||
data.insert(data.end(), write_data.begin(), write_data.end());
|
||||
if (!this->write_command_(data)) {
|
||||
ESP_LOGE(TAG, "Error writing block %d", block_num);
|
||||
return false;
|
||||
}
|
||||
@@ -244,7 +243,8 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
||||
if (!this->write_mifare_classic_block_(current_block, data)) {
|
||||
return false;
|
||||
}
|
||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn532.h"
|
||||
@@ -144,7 +143,8 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *
|
||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
while (index < buffer_length) {
|
||||
if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
||||
if (!this->write_mifare_ultralight_page_(current_page, data)) {
|
||||
return false;
|
||||
}
|
||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
@@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||
if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) {
|
||||
if (!this->write_mifare_ultralight_page_(i, blank_data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
std::vector<uint8_t> cmd({
|
||||
bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
||||
std::vector<uint8_t> data({
|
||||
PN532_COMMAND_INDATAEXCHANGE,
|
||||
0x01, // One card
|
||||
nfc::MIFARE_CMD_WRITE_ULTRALIGHT,
|
||||
page_num,
|
||||
});
|
||||
cmd.insert(cmd.end(), write_data, write_data + len);
|
||||
if (!this->write_command_(cmd)) {
|
||||
data.insert(data.end(), write_data.begin(), write_data.end());
|
||||
if (!this->write_command_(data)) {
|
||||
ESP_LOGE(TAG, "Error writing page %u", page_num);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component {
|
||||
|
||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
uint8_t sect_to_auth_(uint8_t block_num);
|
||||
uint8_t format_mifare_classic_mifare_();
|
||||
@@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component {
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t clean_mifare_ultralight_();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7150.h"
|
||||
@@ -140,10 +139,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) {
|
||||
}
|
||||
|
||||
uint8_t PN7150::format_mifare_classic_mifare_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> blank_buffer(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> trailer_buffer(
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
auto status = nfc::STATUS_OK;
|
||||
|
||||
@@ -152,20 +151,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
|
||||
continue;
|
||||
}
|
||||
if (block != 0) {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
@@ -175,30 +174,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
|
||||
}
|
||||
|
||||
uint8_t PN7150::format_mifare_classic_ndef_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> empty_ndef_message(
|
||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> blank_block(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> block_1_data(
|
||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_2_data(
|
||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_3_trailer(
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
std::vector<uint8_t> ndef_trailer(
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
@@ -211,26 +210,25 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (block == 4) {
|
||||
if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
|
||||
nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
} else {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
@@ -238,7 +236,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||
|
||||
@@ -250,7 +248,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *da
|
||||
}
|
||||
// write command part two
|
||||
tx.set_payload({XCHG_DATA_OID});
|
||||
tx.get_message().insert(tx.get_message().end(), data, data + len);
|
||||
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end());
|
||||
|
||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||
@@ -296,8 +294,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
|
||||
}
|
||||
}
|
||||
|
||||
if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
||||
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
@@ -145,8 +144,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
|
||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
while (index < buffer_length) {
|
||||
if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
||||
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
@@ -159,19 +158,19 @@ uint8_t PN7150::clean_mifare_ultralight_() {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||
if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||
payload.insert(payload.end(), write_data, write_data + len);
|
||||
payload.insert(payload.end(), write_data.begin(), write_data.end());
|
||||
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
||||
|
||||
@@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component {
|
||||
|
||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||
uint8_t sect_to_auth_(uint8_t block_num);
|
||||
uint8_t format_mifare_classic_mifare_();
|
||||
@@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component {
|
||||
uint16_t read_mifare_ultralight_capacity_();
|
||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
uint8_t &message_start_index);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||
uint8_t clean_mifare_ultralight_();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "pn7160.h"
|
||||
@@ -140,10 +139,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) {
|
||||
}
|
||||
|
||||
uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> blank_buffer(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> trailer_buffer(
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
auto status = nfc::STATUS_OK;
|
||||
|
||||
@@ -152,20 +151,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||
continue;
|
||||
}
|
||||
if (block != 0) {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
@@ -175,30 +174,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||
}
|
||||
|
||||
uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
std::vector<uint8_t> empty_ndef_message(
|
||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> blank_block(
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
std::vector<uint8_t> block_1_data(
|
||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_2_data(
|
||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
||||
std::vector<uint8_t> block_3_trailer(
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
std::vector<uint8_t> ndef_trailer(
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
|
||||
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
|
||||
@@ -211,26 +210,25 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
if (block == 4) {
|
||||
if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
|
||||
nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
} else {
|
||||
if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
||||
status = nfc::STATUS_FAILED;
|
||||
}
|
||||
@@ -238,7 +236,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||
@@ -250,7 +248,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *da
|
||||
}
|
||||
// write command part two
|
||||
tx.set_payload({XCHG_DATA_OID});
|
||||
tx.get_message().insert(tx.get_message().end(), data, data + len);
|
||||
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end());
|
||||
|
||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||
@@ -296,8 +294,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
|
||||
}
|
||||
}
|
||||
|
||||
if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
||||
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
@@ -145,8 +144,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
|
||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
while (index < buffer_length) {
|
||||
if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
|
||||
nfc::STATUS_OK) {
|
||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
||||
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||
@@ -159,19 +158,19 @@ uint8_t PN7160::clean_mifare_ultralight_() {
|
||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||
|
||||
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||
if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
|
||||
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) {
|
||||
return nfc::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||
payload.insert(payload.end(), write_data, write_data + len);
|
||||
payload.insert(payload.end(), write_data.begin(), write_data.end());
|
||||
|
||||
nfc::NciMessage rx;
|
||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
||||
|
||||
191
esphome/components/runtime_image/__init__.py
Normal file
191
esphome/components/runtime_image/__init__.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER
|
||||
from esphome.components.image import (
|
||||
IMAGE_TYPE,
|
||||
Image_,
|
||||
validate_settings,
|
||||
validate_transparency,
|
||||
validate_type,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||
|
||||
CONF_PLACEHOLDER = "placeholder"
|
||||
CONF_TRANSPARENCY = "transparency"
|
||||
|
||||
runtime_image_ns = cg.esphome_ns.namespace("runtime_image")
|
||||
|
||||
# Base decoder classes
|
||||
ImageDecoder = runtime_image_ns.class_("ImageDecoder")
|
||||
BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder)
|
||||
JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder)
|
||||
PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder)
|
||||
|
||||
# Runtime image class
|
||||
RuntimeImage = runtime_image_ns.class_(
|
||||
"RuntimeImage", cg.esphome_ns.namespace("image").class_("Image")
|
||||
)
|
||||
|
||||
# Image format enum
|
||||
ImageFormat = runtime_image_ns.enum("ImageFormat")
|
||||
IMAGE_FORMAT_AUTO = ImageFormat.AUTO
|
||||
IMAGE_FORMAT_JPEG = ImageFormat.JPEG
|
||||
IMAGE_FORMAT_PNG = ImageFormat.PNG
|
||||
IMAGE_FORMAT_BMP = ImageFormat.BMP
|
||||
|
||||
# Export enum for decode errors
|
||||
DecodeError = runtime_image_ns.enum("DecodeError")
|
||||
DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT
|
||||
DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY
|
||||
|
||||
|
||||
class Format:
|
||||
"""Base class for image format definitions."""
|
||||
|
||||
def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None:
|
||||
self.name = name
|
||||
self.decoder_class = decoder_class
|
||||
|
||||
def actions(self) -> None:
|
||||
"""Add defines and libraries needed for this format."""
|
||||
|
||||
|
||||
class BMPFormat(Format):
|
||||
"""BMP format decoder configuration."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("BMP", BmpDecoder)
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_BMP")
|
||||
|
||||
|
||||
class JPEGFormat(Format):
|
||||
"""JPEG format decoder configuration."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("JPEG", JpegDecoder)
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
|
||||
|
||||
|
||||
class PNGFormat(Format):
|
||||
"""PNG format decoder configuration."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("PNG", PngDecoder)
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_PNG")
|
||||
cg.add_library("pngle", "1.1.0")
|
||||
|
||||
|
||||
# Registry of available formats
|
||||
IMAGE_FORMATS = {
|
||||
"BMP": BMPFormat(),
|
||||
"JPEG": JPEGFormat(),
|
||||
"PNG": PNGFormat(),
|
||||
"JPG": JPEGFormat(), # Alias for JPEG
|
||||
}
|
||||
|
||||
|
||||
def get_format(format_name: str) -> Format | None:
|
||||
"""Get a format instance by name."""
|
||||
return IMAGE_FORMATS.get(format_name.upper())
|
||||
|
||||
|
||||
def enable_format(format_name: str) -> Format | None:
|
||||
"""Enable a specific image format by adding its defines and libraries."""
|
||||
format_obj = get_format(format_name)
|
||||
if format_obj:
|
||||
format_obj.actions()
|
||||
return format_obj
|
||||
return None
|
||||
|
||||
|
||||
# Runtime image configuration schema base - to be extended by components
|
||||
def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema:
|
||||
"""Create a runtime image schema with the specified image class."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(image_class),
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
|
||||
"BIG_ENDIAN", "LITTLE_ENDIAN", upper=True
|
||||
),
|
||||
cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(),
|
||||
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_runtime_image_settings(config: dict) -> dict:
|
||||
"""Apply validate_settings from image component to runtime image config."""
|
||||
return validate_settings(config)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeImageSettings:
|
||||
"""Processed runtime image configuration parameters."""
|
||||
|
||||
width: int
|
||||
height: int
|
||||
format_enum: cg.MockObj
|
||||
image_type_enum: cg.MockObj
|
||||
transparent: cg.MockObj
|
||||
byte_order_big_endian: bool
|
||||
placeholder: cg.MockObj | None
|
||||
|
||||
|
||||
async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
"""
|
||||
Helper function to process common runtime image configuration parameters.
|
||||
Handles format enabling and returns all necessary enums and parameters.
|
||||
"""
|
||||
from esphome.components.image import get_image_type_enum, get_transparency_enum
|
||||
|
||||
# Get resize dimensions with default (0, 0)
|
||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||
|
||||
# Handle format (required for runtime images)
|
||||
format_name = config[CONF_FORMAT]
|
||||
# Enable the format in the runtime_image component
|
||||
enable_format(format_name)
|
||||
# Map format names to enum values (handle JPG as alias for JPEG)
|
||||
if format_name.upper() == "JPG":
|
||||
format_name = "JPEG"
|
||||
format_enum = getattr(ImageFormat, format_name.upper())
|
||||
|
||||
# Get image type enum
|
||||
image_type_enum = get_image_type_enum(config[CONF_TYPE])
|
||||
|
||||
# Get transparency enum
|
||||
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
|
||||
|
||||
# Get byte order (True for big endian, False for little endian)
|
||||
byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN"
|
||||
|
||||
# Get placeholder if specified
|
||||
placeholder = None
|
||||
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
||||
placeholder = await cg.get_variable(placeholder_id)
|
||||
|
||||
return RuntimeImageSettings(
|
||||
width=width,
|
||||
height=height,
|
||||
format_enum=format_enum,
|
||||
image_type_enum=image_type_enum,
|
||||
transparent=transparent,
|
||||
byte_order_big_endian=byte_order_big_endian,
|
||||
placeholder=placeholder,
|
||||
)
|
||||
@@ -1,15 +1,14 @@
|
||||
#include "bmp_image.h"
|
||||
#include "bmp_decoder.h"
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
static const char *const TAG = "online_image.bmp";
|
||||
static const char *const TAG = "image_decoder.bmp";
|
||||
|
||||
int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
size_t index = 0;
|
||||
@@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
|
||||
this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
|
||||
// BMP file contains its own size in the header
|
||||
size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
|
||||
if (this->expected_size_ == 0) {
|
||||
this->expected_size_ = file_size; // Use file header size if not provided
|
||||
}
|
||||
this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]);
|
||||
|
||||
this->current_index_ = 14;
|
||||
@@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
while (index < size) {
|
||||
uint8_t current_byte = buffer[index];
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
size_t x = (this->paint_index_ % this->width_) + i;
|
||||
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_);
|
||||
size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
|
||||
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
|
||||
this->draw(x, y, 1, 1, c);
|
||||
}
|
||||
@@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
uint8_t b = buffer[index];
|
||||
uint8_t g = buffer[index + 1];
|
||||
uint8_t r = buffer[index + 2];
|
||||
size_t x = this->paint_index_ % this->width_;
|
||||
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_);
|
||||
size_t x = this->paint_index_ % static_cast<size_t>(this->width_);
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
|
||||
Color c = Color(r, g, b);
|
||||
this->draw(x, y, 1, 1, c);
|
||||
this->paint_index_++;
|
||||
@@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
return size;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_BMP
|
||||
@@ -1,27 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Image decoder specialization for PNG images.
|
||||
* @brief Image decoder specialization for BMP images.
|
||||
*/
|
||||
class BmpDecoder : public ImageDecoder {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new BMP Decoder object.
|
||||
*
|
||||
* @param display The image to decode the stream into.
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
BmpDecoder(OnlineImage *image) : ImageDecoder(image) {}
|
||||
BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {}
|
||||
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
bool is_finished() const override {
|
||||
// BMP is finished when we've decoded all pixel data
|
||||
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
|
||||
}
|
||||
|
||||
protected:
|
||||
size_t current_index_{0};
|
||||
size_t paint_index_{0};
|
||||
@@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder {
|
||||
uint8_t padding_bytes_{0};
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_BMP
|
||||
28
esphome/components/runtime_image/image_decoder.cpp
Normal file
28
esphome/components/runtime_image/image_decoder.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
static const char *const TAG = "image_decoder";
|
||||
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
return success;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma once
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
enum DecodeError : int {
|
||||
DECODE_ERROR_INVALID_TYPE = -1,
|
||||
@@ -10,7 +9,7 @@ enum DecodeError : int {
|
||||
DECODE_ERROR_OUT_OF_MEMORY = -3,
|
||||
};
|
||||
|
||||
class OnlineImage;
|
||||
class RuntimeImage;
|
||||
|
||||
/**
|
||||
* @brief Class to abstract decoding different image formats.
|
||||
@@ -20,19 +19,19 @@ class ImageDecoder {
|
||||
/**
|
||||
* @brief Construct a new Image Decoder object
|
||||
*
|
||||
* @param image The image to decode the stream into.
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
ImageDecoder(OnlineImage *image) : image_(image) {}
|
||||
ImageDecoder(RuntimeImage *image) : image_(image) {}
|
||||
virtual ~ImageDecoder() = default;
|
||||
|
||||
/**
|
||||
* @brief Initialize the decoder.
|
||||
*
|
||||
* @param download_size The total number of bytes that need to be downloaded for the image.
|
||||
* @param expected_size Hint about the expected data size (0 if unknown).
|
||||
* @return int Returns 0 on success, a {@see DecodeError} value in case of an error.
|
||||
*/
|
||||
virtual int prepare(size_t download_size) {
|
||||
this->download_size_ = download_size;
|
||||
virtual int prepare(size_t expected_size) {
|
||||
this->expected_size_ = expected_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -73,49 +72,26 @@ class ImageDecoder {
|
||||
*/
|
||||
void draw(int x, int y, int w, int h, const Color &color);
|
||||
|
||||
bool is_finished() const { return this->decoded_bytes_ == this->download_size_; }
|
||||
/**
|
||||
* @brief Check if the decoder has finished processing.
|
||||
*
|
||||
* This should be overridden by decoders that can detect completion
|
||||
* based on format-specific markers rather than byte counts.
|
||||
*/
|
||||
virtual bool is_finished() const {
|
||||
if (this->expected_size_ > 0) {
|
||||
return this->decoded_bytes_ >= this->expected_size_;
|
||||
}
|
||||
// If size is unknown, derived classes should override this
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
OnlineImage *image_;
|
||||
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
|
||||
// Will be overwritten anyway once the download size is known.
|
||||
size_t download_size_ = 1;
|
||||
size_t decoded_bytes_ = 0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
};
|
||||
|
||||
class DownloadBuffer {
|
||||
public:
|
||||
DownloadBuffer(size_t size);
|
||||
|
||||
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
||||
|
||||
uint8_t *data(size_t offset = 0);
|
||||
|
||||
uint8_t *append() { return this->data(this->unread_); }
|
||||
|
||||
size_t unread() const { return this->unread_; }
|
||||
size_t size() const { return this->size_; }
|
||||
size_t free_capacity() const { return this->size_ - this->unread_; }
|
||||
|
||||
size_t read(size_t len);
|
||||
size_t write(size_t len) {
|
||||
this->unread_ += len;
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
|
||||
size_t resize(size_t size);
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_{};
|
||||
uint8_t *buffer_;
|
||||
size_t size_;
|
||||
/** Total number of downloaded bytes not yet read. */
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -1,16 +1,19 @@
|
||||
#include "jpeg_image.h"
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#include "jpeg_decoder.h"
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "online_image.h"
|
||||
static const char *const TAG = "online_image.jpeg";
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
static const char *const TAG = "image_decoder.jpeg";
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
|
||||
@@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
|
||||
|
||||
// Some very big images take too long to decode, so feed the watchdog on each callback
|
||||
// to avoid crashing.
|
||||
App.feed_wdt();
|
||||
// to avoid crashing if the executing task has a watchdog enabled.
|
||||
#ifdef USE_ESP_IDF
|
||||
if (esp_task_wdt_status(nullptr) == ESP_OK) {
|
||||
#endif
|
||||
App.feed_wdt();
|
||||
#ifdef USE_ESP_IDF
|
||||
}
|
||||
#endif
|
||||
size_t position = 0;
|
||||
size_t height = static_cast<size_t>(jpeg->iHeight);
|
||||
size_t width = static_cast<size_t>(jpeg->iWidth);
|
||||
@@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int JpegDecoder::prepare(size_t download_size) {
|
||||
ImageDecoder::prepare(download_size);
|
||||
auto size = this->image_->resize_download_buffer(download_size);
|
||||
if (size < download_size) {
|
||||
ESP_LOGE(TAG, "Download buffer resize failed!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
int JpegDecoder::prepare(size_t expected_size) {
|
||||
ImageDecoder::prepare(expected_size);
|
||||
// JPEG decoder needs complete data before decoding
|
||||
return 0;
|
||||
}
|
||||
|
||||
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
if (size < this->download_size_) {
|
||||
ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_);
|
||||
// JPEG decoder requires complete data
|
||||
// If we know the expected size, wait for it
|
||||
if (this->expected_size_ > 0 && size < this->expected_size_) {
|
||||
ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If size unknown, try to decode and see if it's valid
|
||||
// The JPEGDEC library will fail gracefully if data is incomplete
|
||||
|
||||
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
|
||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
@@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
return size;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
#include <JPEGDEC.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Image decoder specialization for JPEG images.
|
||||
@@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder {
|
||||
/**
|
||||
* @brief Construct a new JPEG Decoder object.
|
||||
*
|
||||
* @param display The image to decode the stream into.
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {}
|
||||
JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {}
|
||||
~JpegDecoder() override {}
|
||||
|
||||
int prepare(size_t download_size) override;
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
protected:
|
||||
JPEGDEC jpeg_{};
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
@@ -1,15 +1,14 @@
|
||||
#include "png_image.h"
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include "png_decoder.h"
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const char *const TAG = "online_image.png";
|
||||
static const char *const TAG = "image_decoder.png";
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||
@@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
|
||||
}
|
||||
}
|
||||
|
||||
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
|
||||
PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) {
|
||||
{
|
||||
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
|
||||
if (!pngle) {
|
||||
@@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() {
|
||||
}
|
||||
}
|
||||
|
||||
int PngDecoder::prepare(size_t download_size) {
|
||||
ImageDecoder::prepare(download_size);
|
||||
int PngDecoder::prepare(size_t expected_size) {
|
||||
ImageDecoder::prepare(expected_size);
|
||||
if (!this->pngle_) {
|
||||
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
@@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
|
||||
ESP_LOGD(TAG, "Waiting for data");
|
||||
// PNG can be decoded progressively, but wait for a reasonable chunk
|
||||
if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) {
|
||||
ESP_LOGD(TAG, "Waiting for more data");
|
||||
return 0;
|
||||
}
|
||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||
@@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
return fed;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_PNG
|
||||
@@ -3,11 +3,11 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "image_decoder.h"
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include "runtime_image.h"
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
#include <pngle.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Image decoder specialization for PNG images.
|
||||
@@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder {
|
||||
/**
|
||||
* @brief Construct a new PNG Decoder object.
|
||||
*
|
||||
* @param display The image to decode the stream into.
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
PngDecoder(OnlineImage *image);
|
||||
PngDecoder(RuntimeImage *image);
|
||||
~PngDecoder() override;
|
||||
|
||||
int prepare(size_t download_size) override;
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
|
||||
@@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder {
|
||||
|
||||
protected:
|
||||
RAMAllocator<pngle_t> allocator_;
|
||||
pngle_t *pngle_;
|
||||
pngle_t *pngle_{nullptr};
|
||||
uint32_t pixels_decoded_{0};
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#endif // USE_RUNTIME_IMAGE_PNG
|
||||
300
esphome/components/runtime_image/runtime_image.cpp
Normal file
300
esphome/components/runtime_image/runtime_image.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "runtime_image.h"
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
#include "bmp_decoder.h"
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
#include "jpeg_decoder.h"
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
#include "png_decoder.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
static const char *const TAG = "runtime_image";
|
||||
|
||||
inline bool is_color_on(const Color &color) {
|
||||
// This produces the most accurate monochrome conversion, but is slightly slower.
|
||||
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
||||
|
||||
// Approximation using fast integer computations; produces acceptable results
|
||||
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
||||
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
||||
}
|
||||
|
||||
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
format_(format),
|
||||
fixed_width_(fixed_width),
|
||||
fixed_height_(fixed_height),
|
||||
placeholder_(placeholder),
|
||||
is_big_endian_(is_big_endian) {}
|
||||
|
||||
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||
|
||||
int RuntimeImage::resize(int width, int height) {
|
||||
// Use fixed dimensions if specified (0 means auto-resize)
|
||||
int target_width = this->fixed_width_ ? this->fixed_width_ : width;
|
||||
int target_height = this->fixed_height_ ? this->fixed_height_ : height;
|
||||
|
||||
size_t result = this->resize_buffer_(target_width, target_height);
|
||||
if (result > 0 && this->progressive_display_) {
|
||||
// Update display dimensions for progressive display
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||
return;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
||||
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->type_) {
|
||||
case image::IMAGE_TYPE_BINARY: {
|
||||
const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u;
|
||||
uint32_t pos = x + y * width_8;
|
||||
auto bitno = 0x80 >> (pos % 8u);
|
||||
pos /= 8u;
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
gray = 1;
|
||||
}
|
||||
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
if (color.w != 0xFF)
|
||||
gray = color.w;
|
||||
}
|
||||
this->buffer_[pos] = gray;
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB565: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
|
||||
if (this->is_big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||
} else {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||
}
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
this->buffer_[pos + 0] = mapped_color.r;
|
||||
this->buffer_[pos + 1] = mapped_color.g;
|
||||
this->buffer_[pos + 2] = mapped_color.b;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::map_chroma_key(Color &color) {
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||
color.g = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
color.r = 0;
|
||||
color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1;
|
||||
color.b = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
if (this->data_start_) {
|
||||
// If we have a complete image, use the base class draw method
|
||||
Image::draw(x, y, display, color_on, color_off);
|
||||
} else if (this->placeholder_) {
|
||||
// Show placeholder while the runtime image is not available
|
||||
this->placeholder_->draw(x, y, display, color_on, color_off);
|
||||
}
|
||||
// If no image is loaded and no placeholder, nothing to draw
|
||||
}
|
||||
|
||||
bool RuntimeImage::begin_decode(size_t expected_size) {
|
||||
if (this->decoder_) {
|
||||
ESP_LOGW(TAG, "Decoding already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->decoder_ = this->create_decoder_();
|
||||
if (!this->decoder_) {
|
||||
ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->total_size_ = expected_size;
|
||||
this->decoded_bytes_ = 0;
|
||||
|
||||
// Initialize decoder
|
||||
int result = this->decoder_->prepare(expected_size);
|
||||
if (result < 0) {
|
||||
ESP_LOGE(TAG, "Failed to prepare decoder: %d", result);
|
||||
this->decoder_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int RuntimeImage::feed_data(uint8_t *data, size_t len) {
|
||||
if (!this->decoder_) {
|
||||
ESP_LOGE(TAG, "No decoder initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int consumed = this->decoder_->decode(data, len);
|
||||
if (consumed > 0) {
|
||||
this->decoded_bytes_ += consumed;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
bool RuntimeImage::end_decode() {
|
||||
if (!this->decoder_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finalize the image for display
|
||||
if (!this->progressive_display_) {
|
||||
// Only now make the image visible
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
}
|
||||
|
||||
// Clean up decoder
|
||||
this->decoder_ = nullptr;
|
||||
|
||||
ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeImage::is_decode_finished() const {
|
||||
if (!this->decoder_) {
|
||||
return false;
|
||||
}
|
||||
return this->decoder_->is_finished();
|
||||
}
|
||||
|
||||
void RuntimeImage::release() {
|
||||
this->release_buffer_();
|
||||
// Reset decoder separately — release() can be called from within the decoder
|
||||
// (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here.
|
||||
// The decoder lifecycle is managed by begin_decode()/end_decode().
|
||||
this->decoder_ = nullptr;
|
||||
}
|
||||
|
||||
void RuntimeImage::release_buffer_() {
|
||||
if (this->buffer_) {
|
||||
ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
|
||||
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
|
||||
this->buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t RuntimeImage::resize_buffer_(int width, int height) {
|
||||
size_t new_size = this->get_buffer_size_(width, height);
|
||||
|
||||
if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
|
||||
// Buffer already allocated with correct size
|
||||
return new_size;
|
||||
}
|
||||
|
||||
// Release old buffer if dimensions changed
|
||||
if (this->buffer_) {
|
||||
this->release_buffer_();
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size);
|
||||
this->buffer_ = this->allocator_.allocate(new_size);
|
||||
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size,
|
||||
this->allocator_.get_max_free_block_size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear buffer
|
||||
memset(this->buffer_, 0, new_size);
|
||||
|
||||
this->buffer_width_ = width;
|
||||
this->buffer_height_ = height;
|
||||
|
||||
return new_size;
|
||||
}
|
||||
|
||||
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
|
||||
return (this->get_bpp() * width + 7u) / 8u * height;
|
||||
}
|
||||
|
||||
int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
|
||||
|
||||
std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_() {
|
||||
switch (this->format_) {
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
case BMP:
|
||||
return make_unique<BmpDecoder>(this);
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
case JPEG:
|
||||
return make_unique<JpegDecoder>(this);
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
case PNG:
|
||||
return make_unique<PngDecoder>(this);
|
||||
#endif
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unsupported image format: %d", this->format_);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
214
esphome/components/runtime_image/runtime_image.h
Normal file
214
esphome/components/runtime_image/runtime_image.h
Normal file
@@ -0,0 +1,214 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
// Forward declaration
|
||||
class ImageDecoder;
|
||||
|
||||
/**
|
||||
* @brief Image format types that can be decoded dynamically.
|
||||
*/
|
||||
enum ImageFormat {
|
||||
/** Automatically detect from data. Not implemented yet. */
|
||||
AUTO,
|
||||
/** JPEG format. */
|
||||
JPEG,
|
||||
/** PNG format. */
|
||||
PNG,
|
||||
/** BMP format. */
|
||||
BMP,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A dynamic image that can be loaded and decoded at runtime.
|
||||
*
|
||||
* This class provides dynamic buffer allocation and management for images
|
||||
* that are decoded at runtime, as opposed to static images compiled into
|
||||
* the firmware. It serves as a base class for components that need to
|
||||
* load images dynamically from various sources.
|
||||
*/
|
||||
class RuntimeImage : public image::Image {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new RuntimeImage object.
|
||||
*
|
||||
* @param format The image format to decode.
|
||||
* @param type The pixel format for the image.
|
||||
* @param transparency The transparency type for the image.
|
||||
* @param placeholder Optional placeholder image to show while loading.
|
||||
* @param is_big_endian Whether the image is stored in big-endian format.
|
||||
* @param fixed_width Fixed width for the image (0 for auto-resize).
|
||||
* @param fixed_height Fixed height for the image (0 for auto-resize).
|
||||
*/
|
||||
RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||
image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0,
|
||||
int fixed_height = 0);
|
||||
|
||||
~RuntimeImage();
|
||||
|
||||
// Decoder interface methods
|
||||
/**
|
||||
* @brief Resize the image buffer to the requested dimensions.
|
||||
*
|
||||
* The buffer will be allocated if not existing.
|
||||
* If fixed dimensions have been specified in the constructor, the buffer will be created
|
||||
* with those dimensions and not resized, even on request.
|
||||
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
|
||||
* dimensions allocated.
|
||||
*
|
||||
* @param width Requested width (ignored if fixed_width_ is set)
|
||||
* @param height Requested height (ignored if fixed_height_ is set)
|
||||
* @return Size of the allocated buffer in bytes, or 0 if allocation failed.
|
||||
*/
|
||||
int resize(int width, int height);
|
||||
void draw_pixel(int x, int y, const Color &color);
|
||||
void map_chroma_key(Color &color);
|
||||
int get_buffer_width() const { return this->buffer_width_; }
|
||||
int get_buffer_height() const { return this->buffer_height_; }
|
||||
|
||||
// Image drawing interface
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
/**
|
||||
* @brief Begin decoding an image.
|
||||
*
|
||||
* @param expected_size Optional hint about the expected data size.
|
||||
* @return true if decoder was successfully initialized.
|
||||
*/
|
||||
bool begin_decode(size_t expected_size = 0);
|
||||
|
||||
/**
|
||||
* @brief Feed data to the decoder.
|
||||
*
|
||||
* @param data Pointer to the data buffer.
|
||||
* @param len Length of data to process.
|
||||
* @return Number of bytes consumed by the decoder.
|
||||
*/
|
||||
int feed_data(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Complete the decoding process.
|
||||
*
|
||||
* @return true if decoding completed successfully.
|
||||
*/
|
||||
bool end_decode();
|
||||
|
||||
/**
|
||||
* @brief Check if decoding is currently in progress.
|
||||
*/
|
||||
bool is_decoding() const { return this->decoder_ != nullptr; }
|
||||
|
||||
/**
|
||||
* @brief Check if the decoder has finished processing all data.
|
||||
*
|
||||
* This delegates to the decoder's format-specific completion check,
|
||||
* which handles both known-size and chunked transfer cases.
|
||||
*/
|
||||
bool is_decode_finished() const;
|
||||
|
||||
/**
|
||||
* @brief Check if an image is currently loaded.
|
||||
*/
|
||||
bool is_loaded() const { return this->buffer_ != nullptr; }
|
||||
|
||||
/**
|
||||
* @brief Get the image format.
|
||||
*/
|
||||
ImageFormat get_format() const { return this->format_; }
|
||||
|
||||
/**
|
||||
* @brief Release the image buffer and free memory.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* @brief Set whether to allow progressive display during decode.
|
||||
*
|
||||
* When enabled, the image can be displayed even while still decoding.
|
||||
* When disabled, the image is only displayed after decoding completes.
|
||||
*/
|
||||
void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Resize the image buffer to the requested dimensions.
|
||||
*
|
||||
* @param width New width in pixels.
|
||||
* @param height New height in pixels.
|
||||
* @return Size of the allocated buffer, or 0 on failure.
|
||||
*/
|
||||
size_t resize_buffer_(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Release only the image buffer without resetting the decoder.
|
||||
*
|
||||
* This is safe to call from within the decoder (e.g., during resize).
|
||||
*/
|
||||
void release_buffer_();
|
||||
|
||||
/**
|
||||
* @brief Get the buffer size in bytes for given dimensions.
|
||||
*/
|
||||
size_t get_buffer_size_(int width, int height) const;
|
||||
|
||||
/**
|
||||
* @brief Get the position in the buffer for a pixel.
|
||||
*/
|
||||
int get_position_(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief Create decoder instance for the image's format.
|
||||
*/
|
||||
std::unique_ptr<ImageDecoder> create_decoder_();
|
||||
|
||||
// Memory management
|
||||
RAMAllocator<uint8_t> allocator_{};
|
||||
uint8_t *buffer_{nullptr};
|
||||
|
||||
// Decoder management
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
/** The image format this RuntimeImage is configured to decode. */
|
||||
const ImageFormat format_;
|
||||
|
||||
/**
|
||||
* Actual width of the current image.
|
||||
* This needs to be separate from "Image::get_width()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images). When progressive_display_ is enabled, Image dimensions
|
||||
* are updated during decoding to allow rendering in progress.
|
||||
*/
|
||||
int buffer_width_{0};
|
||||
/**
|
||||
* Actual height of the current image.
|
||||
* This needs to be separate from "Image::get_height()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images). When progressive_display_ is enabled, Image dimensions
|
||||
* are updated during decoding to allow rendering in progress.
|
||||
*/
|
||||
int buffer_height_{0};
|
||||
|
||||
// Decoding state
|
||||
size_t total_size_{0};
|
||||
size_t decoded_bytes_{0};
|
||||
|
||||
/** Fixed width requested on configuration, or 0 if not specified. */
|
||||
const int fixed_width_{0};
|
||||
/** Fixed height requested on configuration, or 0 if not specified. */
|
||||
const int fixed_height_{0};
|
||||
|
||||
/** Placeholder image to show when the runtime image is not available. */
|
||||
image::Image *placeholder_{nullptr};
|
||||
|
||||
// Configuration
|
||||
bool progressive_display_{false};
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -78,21 +78,23 @@ class Select : public EntityBase {
|
||||
|
||||
void add_on_state_callback(std::function<void(size_t)> &&callback);
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control().
|
||||
* Override this to work directly with indices and avoid string conversions.
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
size_t active_index_{0};
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding this index-based version is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control(const std::string&).
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
/** Set the value of the select, this is a virtual method that each select integration can implement.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
|
||||
@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SPEED): cv.invalid(
|
||||
"Configuring individual speeds is deprecated."
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace speed {
|
||||
|
||||
class SpeedFan : public Component, public fan::Fan {
|
||||
public:
|
||||
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
|
||||
SpeedFan(int speed_count) : speed_count_(speed_count) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_output(output::FloatOutput *output) { this->output_ = output; }
|
||||
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *output_;
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
output::BinaryOutput *direction_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
|
||||
void dump_config() override;
|
||||
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
|
||||
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
|
||||
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
|
||||
void set_speed_count(int count) { this->speed_count_ = count; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
|
||||
fan::FanTraits get_traits() override { return this->traits_; }
|
||||
|
||||
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
|
||||
|
||||
bool has_oscillating_{false};
|
||||
bool has_direction_{false};
|
||||
uint8_t speed_count_{0};
|
||||
int speed_count_{0};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
|
||||
#include "posix_tz.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
// Global timezone - set once at startup, rarely changes
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
|
||||
static ParsedTimezone global_tz_{};
|
||||
|
||||
void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; }
|
||||
|
||||
const ParsedTimezone &get_global_tz() { return global_tz_; }
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Helper to parse an unsigned integer from string, updating pointer
|
||||
static uint32_t parse_uint(const char *&p) {
|
||||
uint32_t value = 0;
|
||||
while (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
value = value * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }
|
||||
|
||||
// Get days in year (avoids duplicate is_leap_year calls)
|
||||
static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
|
||||
|
||||
// Convert days since epoch to year, updating days to remainder
|
||||
static int __attribute__((noinline)) days_to_year(int64_t &days) {
|
||||
int year = 1970;
|
||||
int diy;
|
||||
while (days >= (diy = days_in_year(year))) {
|
||||
days -= diy;
|
||||
year++;
|
||||
}
|
||||
while (days < 0) {
|
||||
year--;
|
||||
days += days_in_year(year);
|
||||
}
|
||||
return year;
|
||||
}
|
||||
|
||||
// Extract just the year from a UTC epoch
|
||||
static int epoch_to_year(time_t epoch) {
|
||||
int64_t days = epoch / 86400;
|
||||
if (epoch < 0 && epoch % 86400 != 0)
|
||||
days--;
|
||||
return days_to_year(days);
|
||||
}
|
||||
|
||||
int days_in_month(int year, int month) {
|
||||
switch (month) {
|
||||
case 2:
|
||||
return is_leap_year(year) ? 29 : 28;
|
||||
case 4:
|
||||
case 6:
|
||||
case 9:
|
||||
case 11:
|
||||
return 30;
|
||||
default:
|
||||
return 31;
|
||||
}
|
||||
}
|
||||
|
||||
// Zeller-like algorithm for day of week (0 = Sunday)
|
||||
int __attribute__((noinline)) day_of_week(int year, int month, int day) {
|
||||
// Adjust for January/February
|
||||
if (month < 3) {
|
||||
month += 12;
|
||||
year--;
|
||||
}
|
||||
int k = year % 100;
|
||||
int j = year / 100;
|
||||
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
|
||||
// Convert from Zeller (0=Sat) to standard (0=Sun)
|
||||
return ((h + 6) % 7);
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
|
||||
// Days since epoch
|
||||
int64_t days = epoch / 86400;
|
||||
int32_t remaining_secs = epoch % 86400;
|
||||
if (remaining_secs < 0) {
|
||||
days--;
|
||||
remaining_secs += 86400;
|
||||
}
|
||||
|
||||
out_tm->tm_sec = remaining_secs % 60;
|
||||
remaining_secs /= 60;
|
||||
out_tm->tm_min = remaining_secs % 60;
|
||||
out_tm->tm_hour = remaining_secs / 60;
|
||||
|
||||
// Day of week (Jan 1, 1970 was Thursday = 4)
|
||||
out_tm->tm_wday = static_cast<int>((days + 4) % 7);
|
||||
if (out_tm->tm_wday < 0)
|
||||
out_tm->tm_wday += 7;
|
||||
|
||||
// Calculate year (updates days to day-of-year)
|
||||
int year = days_to_year(days);
|
||||
out_tm->tm_year = year - 1900;
|
||||
out_tm->tm_yday = static_cast<int>(days);
|
||||
|
||||
// Calculate month and day
|
||||
int month = 1;
|
||||
int dim;
|
||||
while (days >= (dim = days_in_month(year, month))) {
|
||||
days -= dim;
|
||||
month++;
|
||||
}
|
||||
|
||||
out_tm->tm_mon = month - 1;
|
||||
out_tm->tm_mday = static_cast<int>(days) + 1;
|
||||
out_tm->tm_isdst = 0;
|
||||
}
|
||||
|
||||
bool skip_tz_name(const char *&p) {
|
||||
if (*p == '<') {
|
||||
// Angle-bracket quoted name: <+07>, <-03>, <AEST>
|
||||
p++; // skip '<'
|
||||
while (*p && *p != '>') {
|
||||
p++;
|
||||
}
|
||||
if (*p == '>') {
|
||||
p++; // skip '>'
|
||||
return true;
|
||||
}
|
||||
return false; // Unterminated
|
||||
}
|
||||
|
||||
// Standard name: 3+ letters
|
||||
const char *start = p;
|
||||
while (*p && std::isalpha(static_cast<unsigned char>(*p))) {
|
||||
p++;
|
||||
}
|
||||
return (p - start) >= 3;
|
||||
}
|
||||
|
||||
int32_t __attribute__((noinline)) parse_offset(const char *&p) {
|
||||
int sign = 1;
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
p++;
|
||||
} else if (*p == '+') {
|
||||
p++;
|
||||
}
|
||||
|
||||
int hours = parse_uint(p);
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
minutes = parse_uint(p);
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
seconds = parse_uint(p);
|
||||
}
|
||||
}
|
||||
|
||||
return sign * (hours * 3600 + minutes * 60 + seconds);
|
||||
}
|
||||
|
||||
// Helper to parse the optional /time suffix (reuses parse_offset logic)
|
||||
static void parse_transition_time(const char *&p, DSTRule &rule) {
|
||||
rule.time_seconds = 2 * 3600; // Default 02:00
|
||||
if (*p == '/') {
|
||||
p++;
|
||||
rule.time_seconds = parse_offset(p);
|
||||
}
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) {
|
||||
// J format: day 1-365, Feb 29 is NOT counted even in leap years
|
||||
// So day 60 is always March 1
|
||||
// Iterate forward through months (no array needed)
|
||||
int remaining = julian_day;
|
||||
out_month = 1;
|
||||
while (out_month <= 12) {
|
||||
// Days in month for non-leap year (J format ignores leap years)
|
||||
int dim = days_in_month(2001, out_month); // 2001 is non-leap year
|
||||
if (remaining <= dim) {
|
||||
out_day = remaining;
|
||||
return;
|
||||
}
|
||||
remaining -= dim;
|
||||
out_month++;
|
||||
}
|
||||
out_day = remaining;
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) {
|
||||
// Plain format: day 0-365, Feb 29 IS counted in leap years
|
||||
// Day 0 = Jan 1
|
||||
int remaining = day_of_year;
|
||||
out_month = 1;
|
||||
|
||||
while (out_month <= 12) {
|
||||
int days_this_month = days_in_month(year, out_month);
|
||||
if (remaining < days_this_month) {
|
||||
out_day = remaining + 1;
|
||||
return;
|
||||
}
|
||||
remaining -= days_this_month;
|
||||
out_month++;
|
||||
}
|
||||
|
||||
// Shouldn't reach here with valid input
|
||||
out_month = 12;
|
||||
out_day = 31;
|
||||
}
|
||||
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule) {
|
||||
rule = {}; // Zero initialize
|
||||
|
||||
if (*p == 'M' || *p == 'm') {
|
||||
// M format: Mm.w.d (month.week.day)
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
p++;
|
||||
|
||||
rule.month = parse_uint(p);
|
||||
if (rule.month < 1 || rule.month > 12)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.week = parse_uint(p);
|
||||
if (rule.week < 1 || rule.week > 5)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.day_of_week = parse_uint(p);
|
||||
if (rule.day_of_week > 6)
|
||||
return false;
|
||||
|
||||
} else if (*p == 'J' || *p == 'j') {
|
||||
// J format: Jn (Julian day 1-365, not counting Feb 29)
|
||||
rule.type = DSTRuleType::JULIAN_NO_LEAP;
|
||||
p++;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day < 1 || rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else if (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
// Plain number format: n (day 0-365, counting Feb 29)
|
||||
rule.type = DSTRuleType::DAY_OF_YEAR;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse optional /time suffix
|
||||
parse_transition_time(p, rule);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate days from Jan 1 of given year to given month/day
|
||||
static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) {
|
||||
int days = day - 1;
|
||||
for (int m = 1; m < month; m++) {
|
||||
days += days_in_month(year, m);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to Jan 1 of given year (for DST transition calculations)
|
||||
// Only supports years >= 1970. Timezone is either compiled in from YAML or set by
|
||||
// Home Assistant, so pre-1970 dates are not a concern.
|
||||
static int64_t __attribute__((noinline)) days_to_year_start(int year) {
|
||||
int64_t days = 0;
|
||||
for (int y = 1970; y < year; y++) {
|
||||
days += days_in_year(y);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
|
||||
int month, day;
|
||||
|
||||
switch (rule.type) {
|
||||
case DSTRuleType::MONTH_WEEK_DAY: {
|
||||
// Find the nth occurrence of day_of_week in the given month
|
||||
int first_dow = day_of_week(year, rule.month, 1);
|
||||
|
||||
// Days until first occurrence of target day
|
||||
int days_until_first = (rule.day_of_week - first_dow + 7) % 7;
|
||||
int first_occurrence = 1 + days_until_first;
|
||||
|
||||
if (rule.week == 5) {
|
||||
// "Last" occurrence - find the last one in the month
|
||||
int dim = days_in_month(year, rule.month);
|
||||
day = first_occurrence;
|
||||
while (day + 7 <= dim) {
|
||||
day += 7;
|
||||
}
|
||||
} else {
|
||||
// nth occurrence
|
||||
day = first_occurrence + (rule.week - 1) * 7;
|
||||
}
|
||||
month = rule.month;
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTRuleType::JULIAN_NO_LEAP:
|
||||
// J format: day 1-365, Feb 29 not counted
|
||||
julian_to_month_day(rule.day, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::DAY_OF_YEAR:
|
||||
// Plain format: day 0-365, Feb 29 counted
|
||||
day_of_year_to_month_day(rule.day, year, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::NONE:
|
||||
// Should never be called with NONE, but handle it gracefully
|
||||
month = 1;
|
||||
day = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to this date
|
||||
int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
|
||||
|
||||
// Convert to epoch and add transition time and base offset
|
||||
return days * 86400 + rule.time_seconds + base_offset_seconds;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
|
||||
if (!tz.has_dst()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int year = internal::epoch_to_year(utc_epoch);
|
||||
|
||||
// Calculate DST start and end for this year
|
||||
// DST start transition happens in standard time
|
||||
time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
|
||||
// DST end transition happens in daylight time
|
||||
time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
|
||||
|
||||
if (dst_start < dst_end) {
|
||||
// Northern hemisphere: DST is between start and end
|
||||
return (utc_epoch >= dst_start && utc_epoch < dst_end);
|
||||
} else {
|
||||
// Southern hemisphere: DST is outside the range (wraps around year)
|
||||
return (utc_epoch >= dst_start || utc_epoch < dst_end);
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
|
||||
if (!tz_string || !*tz_string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *p = tz_string;
|
||||
|
||||
// Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false)
|
||||
result.std_offset_seconds = 0;
|
||||
result.dst_offset_seconds = 0;
|
||||
result.dst_start = {};
|
||||
result.dst_end = {};
|
||||
|
||||
// Skip standard timezone name
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse standard offset (required)
|
||||
if (!*p || (!std::isdigit(static_cast<unsigned char>(*p)) && *p != '+' && *p != '-')) {
|
||||
return false;
|
||||
}
|
||||
result.std_offset_seconds = internal::parse_offset(p);
|
||||
|
||||
// Check for DST name
|
||||
if (!*p) {
|
||||
return true; // No DST
|
||||
}
|
||||
|
||||
// If next char is comma, there's no DST name but there are rules (invalid)
|
||||
if (*p == ',') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there's something that looks like a DST name start
|
||||
// (letter or angle bracket). If not, treat as trailing garbage and return success.
|
||||
if (!std::isalpha(static_cast<unsigned char>(*p)) && *p != '<') {
|
||||
return true; // No DST, trailing characters ignored
|
||||
}
|
||||
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false; // Invalid DST name (started but malformed)
|
||||
}
|
||||
|
||||
// Optional DST offset (default is std - 1 hour)
|
||||
if (*p && *p != ',' && (std::isdigit(static_cast<unsigned char>(*p)) || *p == '+' || *p == '-')) {
|
||||
result.dst_offset_seconds = internal::parse_offset(p);
|
||||
} else {
|
||||
result.dst_offset_seconds = result.std_offset_seconds - 3600;
|
||||
}
|
||||
|
||||
// Parse DST rules (required when DST name is present)
|
||||
if (*p != ',') {
|
||||
// DST name without rules - treat as no DST since we can't determine transitions
|
||||
return true;
|
||||
}
|
||||
|
||||
p++;
|
||||
if (!internal::parse_dst_rule(p, result.dst_start)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second rule is required per POSIX
|
||||
if (*p != ',') {
|
||||
return false;
|
||||
}
|
||||
p++;
|
||||
// has_dst() now returns true since dst_start.type was set by parse_dst_rule
|
||||
return internal::parse_dst_rule(p, result.dst_end);
|
||||
}
|
||||
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
|
||||
if (!out_tm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine DST status once (avoids duplicate is_in_dst calculation)
|
||||
bool in_dst = is_in_dst(utc_epoch, tz);
|
||||
int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
|
||||
|
||||
// Apply offset (POSIX offset is positive west, so subtract to get local)
|
||||
time_t local_epoch = utc_epoch - offset;
|
||||
|
||||
internal::epoch_to_tm_utc(local_epoch, out_tm);
|
||||
out_tm->tm_isdst = in_dst ? 1 : 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#ifndef USE_HOST
|
||||
// Override libc's localtime functions to use our timezone on embedded platforms.
|
||||
// This allows user lambdas calling ::localtime() to get correct local time
|
||||
// without needing the TZ environment variable (which pulls in scanf bloat).
|
||||
// On host, we use the normal TZ mechanism since there's no memory constraint.
|
||||
|
||||
// Thread-safe version
|
||||
extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) {
|
||||
if (timer == nullptr || result == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Non-thread-safe version (uses static buffer, standard libc behavior)
|
||||
extern "C" struct tm *localtime(const time_t *timer) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static struct tm localtime_buf;
|
||||
return localtime_r(timer, &localtime_buf);
|
||||
}
|
||||
#endif // !USE_HOST
|
||||
|
||||
#endif // USE_TIME_TIMEZONE
|
||||
@@ -1,132 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
/// Type of DST transition rule
|
||||
enum class DSTRuleType : uint8_t {
|
||||
NONE = 0, ///< No DST rule (used to indicate no DST)
|
||||
MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March)
|
||||
JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted)
|
||||
DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years)
|
||||
};
|
||||
|
||||
/// Rule for DST transition (packed for 32-bit: 12 bytes)
|
||||
struct DSTRule {
|
||||
int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM)
|
||||
uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
|
||||
DSTRuleType type; ///< Type of rule
|
||||
uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY)
|
||||
uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY)
|
||||
uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
|
||||
};
|
||||
|
||||
/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
|
||||
struct ParsedTimezone {
|
||||
int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west)
|
||||
int32_t dst_offset_seconds; ///< DST offset from UTC in seconds
|
||||
DSTRule dst_start; ///< When DST starts
|
||||
DSTRule dst_end; ///< When DST ends
|
||||
|
||||
/// Check if this timezone has DST rules
|
||||
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
|
||||
};
|
||||
|
||||
/// Parse a POSIX TZ string into a ParsedTimezone struct.
|
||||
/// Supports formats like:
|
||||
/// - "EST5" (simple offset, no DST)
|
||||
/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules)
|
||||
/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times)
|
||||
/// - "<+07>-7" (angle-bracket notation for special names)
|
||||
/// - "IST-5:30" (half-hour offsets)
|
||||
/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day)
|
||||
/// - "EST5EDT,60,300" (plain day number: day of year with leap day)
|
||||
/// @param tz_string The POSIX TZ string to parse
|
||||
/// @param result Output: the parsed timezone data
|
||||
/// @return true if parsing succeeded, false on error
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result);
|
||||
|
||||
/// Convert a UTC epoch to local time using the parsed timezone.
|
||||
/// This replaces libc's localtime() to avoid scanf dependency.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @param[out] out_tm Output tm struct with local time
|
||||
/// @return true on success
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm);
|
||||
|
||||
/// Set the global timezone used by epoch_to_local_tm() when called without a timezone.
|
||||
/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local()
|
||||
/// to work without libc's localtime().
|
||||
void set_global_tz(const ParsedTimezone &tz);
|
||||
|
||||
/// Get the global timezone.
|
||||
const ParsedTimezone &get_global_tz();
|
||||
|
||||
/// Check if a given UTC epoch falls within DST for the parsed timezone.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @return true if DST is in effect at the given time
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
|
||||
|
||||
// Internal helper functions exposed for testing
|
||||
|
||||
namespace internal {
|
||||
|
||||
/// Skip a timezone name (letters or <...> quoted format)
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return true if a valid name was found
|
||||
bool skip_tz_name(const char *&p);
|
||||
|
||||
/// Parse an offset in format [-]hh[:mm[:ss]]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return Offset in seconds
|
||||
int32_t parse_offset(const char *&p);
|
||||
|
||||
/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @param rule Output: the parsed rule
|
||||
/// @return true if parsing succeeded
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule);
|
||||
|
||||
/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day
|
||||
/// @param julian_day Day number 1-365
|
||||
/// @param[out] month Output: month 1-12
|
||||
/// @param[out] day Output: day of month
|
||||
void julian_to_month_day(int julian_day, int &month, int &day);
|
||||
|
||||
/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day
|
||||
/// @param day_of_year Day number 0-365
|
||||
/// @param year The year (for leap year calculation)
|
||||
/// @param[out] month Output: month 1-12
|
||||
/// @param[out] day Output: day of month
|
||||
void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day);
|
||||
|
||||
/// Calculate day of week for any date (0 = Sunday)
|
||||
/// Uses a simplified algorithm that works for years 1970-2099
|
||||
int day_of_week(int year, int month, int day);
|
||||
|
||||
/// Get the number of days in a month
|
||||
int days_in_month(int year, int month);
|
||||
|
||||
/// Check if a year is a leap year
|
||||
bool is_leap_year(int year);
|
||||
|
||||
/// Convert epoch to year/month/day/hour/min/sec (UTC)
|
||||
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm);
|
||||
|
||||
/// Calculate the epoch timestamp for a DST transition in a given year.
|
||||
/// @param year The year (e.g., 2026)
|
||||
/// @param rule The DST rule (month, week, day_of_week, time)
|
||||
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
|
||||
/// @return Unix epoch timestamp of the transition
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
#endif // USE_TIME_TIMEZONE
|
||||
@@ -14,8 +14,8 @@
|
||||
#include <sys/time.h>
|
||||
#endif
|
||||
#include <cerrno>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
@@ -23,33 +23,9 @@ static const char *const TAG = "time";
|
||||
|
||||
RealTimeClock::RealTimeClock() = default;
|
||||
|
||||
ESPTime __attribute__((noinline)) RealTimeClock::now() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
time_t epoch = this->timestamp_now();
|
||||
struct tm local_tm;
|
||||
if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) {
|
||||
return ESPTime::from_c_tm(&local_tm, epoch);
|
||||
}
|
||||
// Fallback to UTC if parsing failed
|
||||
return ESPTime::from_epoch_utc(epoch);
|
||||
#else
|
||||
return ESPTime::from_epoch_local(this->timestamp_now());
|
||||
#endif
|
||||
}
|
||||
|
||||
void RealTimeClock::dump_config() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
const auto &tz = get_global_tz();
|
||||
// POSIX offset is positive west, negate for conventional UTC+X display
|
||||
int std_h = -tz.std_offset_seconds / 3600;
|
||||
int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60;
|
||||
if (tz.has_dst()) {
|
||||
int dst_h = -tz.dst_offset_seconds / 3600;
|
||||
int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60;
|
||||
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||
#endif
|
||||
auto time = this->now();
|
||||
ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
|
||||
@@ -96,6 +72,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
ret = settimeofday(&timev, nullptr);
|
||||
}
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
// Move timezone back to local timezone.
|
||||
this->apply_timezone_();
|
||||
#endif
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
|
||||
}
|
||||
@@ -108,29 +89,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
}
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void RealTimeClock::apply_timezone_(const char *tz) {
|
||||
ParsedTimezone parsed{};
|
||||
|
||||
// Handle null or empty input - use UTC
|
||||
if (tz == nullptr || *tz == '\0') {
|
||||
set_global_tz(parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_HOST
|
||||
// On host platform, also set TZ environment variable for libc compatibility
|
||||
setenv("TZ", tz, 1);
|
||||
void RealTimeClock::apply_timezone_() {
|
||||
setenv("TZ", this->timezone_.c_str(), 1);
|
||||
tzset();
|
||||
#endif
|
||||
|
||||
// Parse the POSIX TZ string using our custom parser
|
||||
if (!parse_posix_tz(tz, parsed)) {
|
||||
ESP_LOGW(TAG, "Failed to parse timezone: %s", tz);
|
||||
// parsed stays as default (UTC) on failure
|
||||
}
|
||||
|
||||
// Set global timezone for all time conversions
|
||||
set_global_tz(parsed);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/time.h"
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
#include "posix_tz.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
@@ -23,31 +20,26 @@ class RealTimeClock : public PollingComponent {
|
||||
explicit RealTimeClock();
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
/// Set the time zone from a POSIX TZ string.
|
||||
void set_timezone(const char *tz) { this->apply_timezone_(tz); }
|
||||
|
||||
/// Set the time zone from a character buffer with known length.
|
||||
/// The buffer does not need to be null-terminated.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (tz == nullptr) {
|
||||
this->apply_timezone_(nullptr);
|
||||
return;
|
||||
}
|
||||
// Stack buffer - TZ strings from tzdata are typically short (< 50 chars)
|
||||
char buf[128];
|
||||
if (len >= sizeof(buf))
|
||||
len = sizeof(buf) - 1;
|
||||
memcpy(buf, tz, len);
|
||||
buf[len] = '\0';
|
||||
this->apply_timezone_(buf);
|
||||
/// Set the time zone.
|
||||
void set_timezone(const std::string &tz) {
|
||||
this->timezone_ = tz;
|
||||
this->apply_timezone_();
|
||||
}
|
||||
|
||||
/// Set the time zone from a std::string.
|
||||
void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); }
|
||||
/// Set the time zone from raw buffer, only if it differs from the current one.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
|
||||
this->timezone_.assign(tz, len);
|
||||
this->apply_timezone_();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the time zone currently in use.
|
||||
std::string get_timezone() { return this->timezone_; }
|
||||
#endif
|
||||
|
||||
/// Get the time in the currently defined timezone.
|
||||
ESPTime now();
|
||||
ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
|
||||
|
||||
/// Get the time without any time zone or DST corrections.
|
||||
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
|
||||
@@ -66,7 +58,8 @@ class RealTimeClock : public PollingComponent {
|
||||
void synchronize_epoch_(uint32_t epoch);
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void apply_timezone_(const char *tz);
|
||||
std::string timezone_{};
|
||||
void apply_timezone_();
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> time_sync_callback_;
|
||||
|
||||
@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace tuya {
|
||||
|
||||
class TuyaFan : public Component, public fan::Fan {
|
||||
public:
|
||||
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
|
||||
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
|
||||
@@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan {
|
||||
optional<uint8_t> switch_id_{};
|
||||
optional<uint8_t> oscillation_id_{};
|
||||
optional<uint8_t> direction_id_{};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
TuyaDatapointType speed_type_{};
|
||||
TuyaDatapointType oscillation_type_{};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Trigger CI memory impact (uses updated ESPAsyncWebServer from web_server_base)
|
||||
#include "web_server.h"
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/json/json_util.h"
|
||||
@@ -215,7 +214,7 @@ DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_gener
|
||||
void DeferredUpdateEventSource::process_deferred_queue_() {
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
auto message = de.message_generator_(web_server_, de.source_);
|
||||
std::string message = de.message_generator_(web_server_, de.source_);
|
||||
if (this->send(message.c_str(), "state") != DISCARDED) {
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
@@ -272,7 +271,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
|
||||
// deferred queue still not empty which means downstream event queue full, no point trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
auto message = message_generator(web_server_, source);
|
||||
std::string message = message_generator(web_server_, source);
|
||||
if (this->send(message.c_str(), "state") == DISCARDED) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
@@ -326,7 +325,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
|
||||
ws->defer([ws, source]() {
|
||||
// Configure reconnect timeout and send config
|
||||
// this should always go through since the AsyncEventSourceClient event queue is empty on connect
|
||||
auto message = ws->get_config_json();
|
||||
std::string message = ws->get_config_json();
|
||||
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
@@ -335,10 +334,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
|
||||
JsonObject root = builder.root();
|
||||
root[ESPHOME_F("name")] = group.second.name;
|
||||
root[ESPHOME_F("sorting_weight")] = group.second.weight;
|
||||
auto group_msg = builder.serialize();
|
||||
message = builder.serialize();
|
||||
|
||||
// up to 31 groups should be able to be queued initially without defer
|
||||
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
|
||||
source->try_send_nodefer(message.c_str(), "sorting_group");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -371,7 +370,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ =
|
||||
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
|
||||
#endif
|
||||
|
||||
json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
std::string WebServer::get_config_json() {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -604,20 +603,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->sensor_json_(obj, obj->state, detail);
|
||||
std::string data = this->sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
|
||||
std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -651,23 +650,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->text_sensor_json_(obj, obj->state, detail);
|
||||
std::string data = this->text_sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
|
||||
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
|
||||
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
|
||||
JsonDetail start_config) {
|
||||
std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
|
||||
JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -712,7 +711,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->switch_json_(obj, obj->state, detail);
|
||||
std::string data = this->switch_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -737,13 +736,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
|
||||
std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -765,7 +764,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->button_json_(obj, detail);
|
||||
std::string data = this->button_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("press"))) {
|
||||
DEFER_ACTION(obj, obj->press());
|
||||
@@ -778,10 +777,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
|
||||
std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -808,23 +807,22 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->binary_sensor_json_(obj, obj->state, detail);
|
||||
std::string data = this->binary_sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
|
||||
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
|
||||
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
|
||||
JsonDetail start_config) {
|
||||
std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -851,7 +849,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->fan_json_(obj, detail);
|
||||
std::string data = this->fan_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("toggle"))) {
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
@@ -892,13 +890,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
|
||||
std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -932,7 +930,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->light_json_(obj, detail);
|
||||
std::string data = this->light_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("toggle"))) {
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
@@ -971,13 +969,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
|
||||
std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1011,7 +1009,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->cover_json_(obj, detail);
|
||||
std::string data = this->cover_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1059,13 +1057,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
|
||||
std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1100,7 +1098,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->number_json_(obj, obj->state, detail);
|
||||
std::string data = this->number_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1119,13 +1117,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
|
||||
std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1167,7 +1165,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->date_json_(obj, detail);
|
||||
std::string data = this->date_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1193,13 +1191,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
|
||||
std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1228,7 +1226,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->time_json_(obj, detail);
|
||||
std::string data = this->time_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1253,13 +1251,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
|
||||
std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1288,7 +1286,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->datetime_json_(obj, detail);
|
||||
std::string data = this->datetime_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1313,13 +1311,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
|
||||
std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1350,7 +1348,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->text_json_(obj, obj->state, detail);
|
||||
std::string data = this->text_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1369,13 +1367,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
|
||||
std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1407,7 +1405,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
|
||||
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1426,15 +1424,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) {
|
||||
auto *obj = (select::Select *) (source);
|
||||
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) {
|
||||
auto *obj = (select::Select *) (source);
|
||||
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
|
||||
std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1466,7 +1464,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->climate_json_(obj, detail);
|
||||
std::string data = this->climate_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1499,15 +1497,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
|
||||
std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -1640,7 +1638,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->lock_json_(obj, obj->state, detail);
|
||||
std::string data = this->lock_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1665,13 +1663,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
|
||||
std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1699,7 +1697,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->valve_json_(obj, detail);
|
||||
std::string data = this->valve_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1745,13 +1743,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
|
||||
std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1784,7 +1782,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
|
||||
std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1824,19 +1822,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
|
||||
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
|
||||
DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
|
||||
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
|
||||
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
|
||||
DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value,
|
||||
JsonDetail start_config) {
|
||||
std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value,
|
||||
JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -1865,7 +1863,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->water_heater_json_(obj, detail);
|
||||
std::string data = this->water_heater_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -1901,14 +1899,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
|
||||
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);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
|
||||
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];
|
||||
@@ -1971,7 +1969,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->infrared_json_(obj, detail);
|
||||
std::string data = this->infrared_json_(obj, detail);
|
||||
request->send(200, ESPHOME_F("application/json"), data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2031,12 +2029,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
|
||||
}
|
||||
|
||||
json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -2071,7 +2069,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->event_json_(obj, StringRef(), detail);
|
||||
std::string data = this->event_json_(obj, StringRef(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2081,16 +2079,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
|
||||
|
||||
json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||
auto *event = static_cast<event::Event *>(source);
|
||||
return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
|
||||
}
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) {
|
||||
auto *event = static_cast<event::Event *>(source);
|
||||
return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
|
||||
std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -2124,7 +2122,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
auto data = this->update_json_(obj, detail);
|
||||
std::string data = this->update_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2140,15 +2138,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
|
||||
std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||
std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "list_entities.h"
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/core/component.h"
|
||||
@@ -104,7 +103,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
|
||||
can be forgotten.
|
||||
*/
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
|
||||
using message_generator_t = std::string(WebServer *, void *);
|
||||
|
||||
class DeferredUpdateEventSourceList;
|
||||
class DeferredUpdateEventSource : public AsyncEventSource {
|
||||
@@ -265,7 +264,7 @@ class WebServer : public Controller,
|
||||
void handle_index_request(AsyncWebServerRequest *request);
|
||||
|
||||
/// Return the webserver configuration as JSON.
|
||||
json::SerializationBuffer<> get_config_json();
|
||||
std::string get_config_json();
|
||||
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
/// Handle included css request under '/0.css'.
|
||||
@@ -287,8 +286,8 @@ class WebServer : public Controller,
|
||||
/// Handle a sensor request under '/sensor/<id>'.
|
||||
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
@@ -297,8 +296,8 @@ class WebServer : public Controller,
|
||||
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string switch_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string switch_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
@@ -306,7 +305,7 @@ class WebServer : public Controller,
|
||||
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
// Buttons are stateless, so there is no button_state_json_generator
|
||||
static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string button_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -315,8 +314,8 @@ class WebServer : public Controller,
|
||||
/// Handle a binary sensor request under '/binary_sensor/<id>'.
|
||||
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
@@ -325,8 +324,8 @@ class WebServer : public Controller,
|
||||
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string fan_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string fan_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
@@ -335,8 +334,8 @@ class WebServer : public Controller,
|
||||
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
|
||||
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string light_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string light_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
@@ -345,8 +344,8 @@ class WebServer : public Controller,
|
||||
/// Handle a text sensor request under '/text_sensor/<id>'.
|
||||
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_sensor_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_sensor_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_COVER
|
||||
@@ -355,8 +354,8 @@ class WebServer : public Controller,
|
||||
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
|
||||
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string cover_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string cover_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
@@ -364,8 +363,8 @@ class WebServer : public Controller,
|
||||
/// Handle a number request under '/number/<id>'.
|
||||
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string number_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string number_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
@@ -373,8 +372,8 @@ class WebServer : public Controller,
|
||||
/// Handle a date request under '/date/<id>'.
|
||||
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string date_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string date_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
@@ -382,8 +381,8 @@ class WebServer : public Controller,
|
||||
/// Handle a time request under '/time/<id>'.
|
||||
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string time_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string time_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
@@ -391,8 +390,8 @@ class WebServer : public Controller,
|
||||
/// Handle a datetime request under '/datetime/<id>'.
|
||||
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string datetime_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string datetime_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
@@ -400,8 +399,8 @@ class WebServer : public Controller,
|
||||
/// Handle a text input request under '/text/<id>'.
|
||||
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string text_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
@@ -409,8 +408,8 @@ class WebServer : public Controller,
|
||||
/// Handle a select request under '/select/<id>'.
|
||||
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string select_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string select_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
@@ -418,8 +417,8 @@ class WebServer : public Controller,
|
||||
/// Handle a climate request under '/climate/<id>'.
|
||||
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string climate_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string climate_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
@@ -428,8 +427,8 @@ class WebServer : public Controller,
|
||||
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
|
||||
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string lock_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string lock_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
@@ -438,8 +437,8 @@ class WebServer : public Controller,
|
||||
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
|
||||
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string valve_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string valve_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
@@ -448,8 +447,8 @@ class WebServer : public Controller,
|
||||
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
|
||||
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
@@ -458,22 +457,22 @@ class WebServer : public Controller,
|
||||
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
|
||||
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
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_INFRARED
|
||||
/// Handle an infrared request under '/infrared/<id>/transmit'.
|
||||
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
|
||||
static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string event_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string event_all_json_generator(WebServer *web_server, void *source);
|
||||
|
||||
/// Handle a event request under '/event<id>'.
|
||||
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
@@ -485,8 +484,8 @@ class WebServer : public Controller,
|
||||
/// Handle a update request under '/update/<id>'.
|
||||
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source);
|
||||
static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source);
|
||||
static std::string update_state_json_generator(WebServer *web_server, void *source);
|
||||
static std::string update_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
/// Override the web handler's canHandle method.
|
||||
@@ -594,74 +593,71 @@ class WebServer : public Controller,
|
||||
|
||||
private:
|
||||
#ifdef USE_SENSOR
|
||||
json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
|
||||
std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
|
||||
std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config);
|
||||
std::string button_json_(button::Button *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
|
||||
JsonDetail start_config);
|
||||
std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config);
|
||||
std::string fan_json_(fan::Fan *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config);
|
||||
std::string light_json_(light::LightState *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
|
||||
JsonDetail start_config);
|
||||
std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config);
|
||||
std::string cover_json_(cover::Cover *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config);
|
||||
std::string number_json_(number::Number *obj, float value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config);
|
||||
std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
|
||||
std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
|
||||
std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
|
||||
std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
|
||||
std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config);
|
||||
std::string climate_json_(climate::Climate *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
|
||||
std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config);
|
||||
std::string valve_json_(valve::Valve *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value,
|
||||
JsonDetail start_config);
|
||||
std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
|
||||
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
|
||||
std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "web_server_base";
|
||||
|
||||
WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void __attribute__((flatten)) WebServerBase::add_handler(AsyncWebHandler *handler) {
|
||||
void WebServerBase::add_handler(AsyncWebHandler *handler) {
|
||||
// remove all handlers
|
||||
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
|
||||
@@ -457,9 +457,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
void AsyncResponseStream::print(float value) {
|
||||
// Use stack buffer to avoid temporary string allocation
|
||||
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
|
||||
constexpr size_t float_buf_size = 32;
|
||||
char buf[float_buf_size];
|
||||
int len = snprintf(buf, float_buf_size, "%f", value);
|
||||
char buf[32];
|
||||
int len = snprintf(buf, sizeof(buf), "%f", value);
|
||||
this->content_.append(buf, len);
|
||||
}
|
||||
|
||||
@@ -564,7 +563,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
// Configure reconnect timeout and send config
|
||||
// this should always go through since the tcp send buffer is empty on connect
|
||||
auto message = ws->get_config_json();
|
||||
std::string message = ws->get_config_json();
|
||||
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
@@ -618,7 +617,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g
|
||||
void AsyncEventSourceResponse::process_deferred_queue_() {
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
auto message = de.message_generator_(web_server_, de.source_);
|
||||
std::string message = de.message_generator_(web_server_, de.source_);
|
||||
if (this->try_send_nodefer(message.c_str(), "state")) {
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
@@ -855,7 +854,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
|
||||
// trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
auto message = message_generator(web_server_, source);
|
||||
std::string message = message_generator(web_server_, source);
|
||||
if (!this->try_send_nodefer(message.c_str(), "state")) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server/list_entities.h"
|
||||
#endif
|
||||
|
||||
@@ -251,7 +250,7 @@ class AsyncWebHandler {
|
||||
class AsyncEventSource;
|
||||
class AsyncEventSourceResponse;
|
||||
|
||||
using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *);
|
||||
using message_generator_t = std::string(esphome::web_server::WebServer *, void *);
|
||||
|
||||
/*
|
||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
@@ -719,7 +718,7 @@ void WiFiComponent::restart_adapter() {
|
||||
// through start_connecting() first. Without this clear, stale errors would
|
||||
// trigger spurious "failed (callback)" logs. The canonical clear location
|
||||
// is in start_connecting(); this is the only exception to that pattern.
|
||||
this->error_from_callback_ = 0;
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -1149,7 +1148,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
// This is the canonical location for clearing the flag since all connection
|
||||
// attempts go through start_connecting(). The only other clear is in
|
||||
// restart_adapter() which enters COOLDOWN without calling start_connecting().
|
||||
this->error_from_callback_ = 0;
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
@@ -1333,61 +1332,20 @@ void WiFiComponent::start_scanning() {
|
||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||
//
|
||||
// Uses raw memcpy instead of copy assignment to avoid CompactString's
|
||||
// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
|
||||
// Copy assignment calls ~CompactString() then placement-new for every shift,
|
||||
// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
|
||||
// networks (e.g., captive portal showing full scan results), this caused
|
||||
// event loop blocking from hundreds of heap operations in a tight loop.
|
||||
//
|
||||
// This is safe because we're permuting elements within the same array —
|
||||
// each slot is overwritten exactly once, so no ownership duplication occurs.
|
||||
// All members of WiFiScanResult are either trivially copyable (bssid, channel,
|
||||
// rssi, priority, flags) or CompactString, which stores either inline data or
|
||||
// a heap pointer — never a self-referential pointer (unlike std::string's SSO
|
||||
// on some implementations). This was not possible before PR#13472 replaced
|
||||
// std::string with CompactString, since std::string's internal layout is
|
||||
// implementation-defined and may use self-referential pointers.
|
||||
//
|
||||
// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
|
||||
// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
|
||||
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||
// memcpy-based sort requires no self-referential pointers or virtual dispatch.
|
||||
// These static_asserts guard the assumptions. If any fire, the memcpy sort
|
||||
// must be reviewed for safety before updating the expected values.
|
||||
//
|
||||
// No vtable pointers (memcpy would corrupt vptr)
|
||||
static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
|
||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
||||
// Standard layout ensures predictable memory layout with no virtual bases
|
||||
// and no mixed-access-specifier reordering
|
||||
static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
|
||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
||||
// Size checks catch added/removed fields that may need safety review
|
||||
static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
|
||||
static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
|
||||
// Alignment must match for reinterpret_cast of key_buf to be valid
|
||||
static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
|
||||
const size_t size = results.size();
|
||||
constexpr size_t elem_size = sizeof(WiFiScanResult);
|
||||
// Suppress warnings for intentional memcpy on non-trivially-copyable type.
|
||||
// Safety is guaranteed by the static_asserts above and the permutation invariant.
|
||||
// NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
|
||||
auto *memcpy_fn = &memcpy;
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
alignas(WiFiScanResult) uint8_t key_buf[elem_size];
|
||||
memcpy_fn(key_buf, &results[i], elem_size);
|
||||
const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
|
||||
// Make a copy to avoid issues with move semantics during comparison
|
||||
WiFiScanResult key = results[i];
|
||||
int32_t j = i - 1;
|
||||
|
||||
// Move elements that are worse than key to the right
|
||||
// For stability, we only move if key is strictly better than results[j]
|
||||
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
|
||||
memcpy_fn(&results[j + 1], &results[j], elem_size);
|
||||
results[j + 1] = results[j];
|
||||
j--;
|
||||
}
|
||||
memcpy_fn(&results[j + 1], key_buf, elem_size);
|
||||
results[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1969,7 +1927,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
|
||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
||||
}
|
||||
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
@@ -220,14 +219,6 @@ class CompactString {
|
||||
};
|
||||
|
||||
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
|
||||
// CompactString is not trivially copyable (non-trivial destructor/copy for heap case).
|
||||
// However, its layout has no self-referential pointers: storage_[] contains either inline
|
||||
// data or an external heap pointer — never a pointer to itself. This is unlike libstdc++
|
||||
// std::string SSO where _M_p points to _M_local_buf within the same object.
|
||||
// This property allows memcpy-based permutation sorting where each element ends up in
|
||||
// exactly one slot (no ownership duplication). These asserts document that layout property.
|
||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
||||
|
||||
class WiFiAP {
|
||||
friend class WiFiComponent;
|
||||
|
||||
@@ -224,8 +224,14 @@ bool WiFiComponent::wifi_apply_hostname_() {
|
||||
#else
|
||||
intf->hostname = wifi_station_get_hostname();
|
||||
#endif
|
||||
if (netif_dhcp_data(intf) != nullptr) {
|
||||
// renew already started DHCP leases
|
||||
if (netif_dhcp_data(intf) != nullptr && netif_is_link_up(intf)) {
|
||||
// Renew already started DHCP leases to inform server of hostname change.
|
||||
// Only attempt when the interface has link — calling dhcp_renew() without
|
||||
// an active connection corrupts lwIP's DHCP state machine (it unconditionally
|
||||
// sets state to RENEWING before attempting to send, and never rolls back on
|
||||
// failure). This causes dhcp_network_changed() to call dhcp_reboot() instead
|
||||
// of dhcp_discover() when WiFi later connects, sending a bogus DHCP REQUEST
|
||||
// for IP 0.0.0.0 that can put some routers into a persistent bad state.
|
||||
err_t lwipret = dhcp_renew(intf);
|
||||
if (lwipret != ERR_OK) {
|
||||
ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname,
|
||||
|
||||
@@ -775,7 +775,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
s_sta_connected = false;
|
||||
s_sta_connecting = false;
|
||||
error_from_callback_ = 1;
|
||||
error_from_callback_ = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
|
||||
@@ -495,7 +495,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
s_ignored_disconnect_count, get_disconnect_reason_str(it.reason));
|
||||
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = 1;
|
||||
this->error_from_callback_ = true;
|
||||
// Don't break - fall through to notify listeners
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)",
|
||||
@@ -521,7 +521,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
|
||||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = 1;
|
||||
this->error_from_callback_ = true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
@@ -537,7 +537,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = 1;
|
||||
this->error_from_callback_ = true;
|
||||
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -766,15 +766,6 @@ class EsphomeCore:
|
||||
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
|
||||
return self.relative_build_path(".piolibdeps", *path)
|
||||
|
||||
@property
|
||||
def platformio_cache_dir(self) -> str:
|
||||
"""Get the PlatformIO cache directory path."""
|
||||
# Check if running in Docker/HA addon with custom cache dir
|
||||
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
|
||||
return cache_dir
|
||||
# Default PlatformIO cache location
|
||||
return os.path.expanduser("~/.platformio/.cache")
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
@@ -57,16 +56,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
this->static_str_ = str;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, __FlashStringHelper* is a distinct type from const char*.
|
||||
// ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM.
|
||||
// Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions
|
||||
// to access the PROGMEM pointer safely.
|
||||
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
|
||||
this->static_str_ = reinterpret_cast<const char *>(str);
|
||||
}
|
||||
#endif
|
||||
|
||||
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
|
||||
if constexpr (USE_HEAP_STORAGE) {
|
||||
this->value_ = new T(std::move(value));
|
||||
@@ -100,7 +89,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
this->f_ = new std::function<T(X...)>(*other.f_);
|
||||
} else if (this->type_ == STATELESS_LAMBDA) {
|
||||
this->stateless_f_ = other.stateless_f_;
|
||||
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
|
||||
} else if (this->type_ == STATIC_STRING) {
|
||||
this->static_str_ = other.static_str_;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +108,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
other.f_ = nullptr;
|
||||
} else if (this->type_ == STATELESS_LAMBDA) {
|
||||
this->stateless_f_ = other.stateless_f_;
|
||||
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
|
||||
} else if (this->type_ == STATIC_STRING) {
|
||||
this->static_str_ = other.static_str_;
|
||||
}
|
||||
other.type_ = NONE;
|
||||
@@ -152,7 +141,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
} else if (this->type_ == LAMBDA) {
|
||||
delete this->f_;
|
||||
}
|
||||
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||
}
|
||||
|
||||
bool has_value() const { return this->type_ != NONE; }
|
||||
@@ -176,17 +165,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
return std::string(this->static_str_);
|
||||
}
|
||||
__builtin_unreachable();
|
||||
#ifdef USE_ESP8266
|
||||
case FLASH_STRING:
|
||||
// PROGMEM pointer — must use _P functions to access on ESP8266
|
||||
if constexpr (std::same_as<T, std::string>) {
|
||||
size_t len = strlen_P(this->static_str_);
|
||||
std::string result(len, '\0');
|
||||
memcpy_P(result.data(), this->static_str_, len);
|
||||
return result;
|
||||
}
|
||||
__builtin_unreachable();
|
||||
#endif
|
||||
case NONE:
|
||||
default:
|
||||
return T{};
|
||||
@@ -208,12 +186,9 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
}
|
||||
|
||||
/// Check if this holds a static string (const char* stored without allocation)
|
||||
/// The pointer is always directly readable (RAM or flash-mapped).
|
||||
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
|
||||
bool is_static_string() const { return this->type_ == STATIC_STRING; }
|
||||
|
||||
/// Get the static string pointer (only valid if is_static_string() returns true)
|
||||
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
|
||||
const char *get_static_string() const { return this->static_str_; }
|
||||
|
||||
/// Check if the string value is empty without allocating (for std::string specialization).
|
||||
@@ -225,12 +200,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
return true;
|
||||
case STATIC_STRING:
|
||||
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
|
||||
#ifdef USE_ESP8266
|
||||
case FLASH_STRING:
|
||||
// PROGMEM pointer — must use progmem_read_byte on ESP8266
|
||||
return this->static_str_ == nullptr ||
|
||||
progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
|
||||
#endif
|
||||
case VALUE:
|
||||
return this->value_->empty();
|
||||
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
||||
@@ -240,9 +209,8 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
|
||||
/// Get a StringRef to the string value without heap allocation when possible.
|
||||
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
|
||||
/// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
|
||||
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
|
||||
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
|
||||
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
|
||||
/// @param lambda_buf_size Size of the buffer.
|
||||
/// @return StringRef pointing to the string data.
|
||||
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
|
||||
@@ -253,19 +221,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
if (this->static_str_ == nullptr)
|
||||
return StringRef();
|
||||
return StringRef(this->static_str_, strlen(this->static_str_));
|
||||
#ifdef USE_ESP8266
|
||||
case FLASH_STRING:
|
||||
if (this->static_str_ == nullptr)
|
||||
return StringRef();
|
||||
{
|
||||
// PROGMEM pointer — copy to buffer via _P functions
|
||||
size_t len = strlen_P(this->static_str_);
|
||||
size_t copy_len = std::min(len, lambda_buf_size - 1);
|
||||
memcpy_P(lambda_buf, this->static_str_, copy_len);
|
||||
lambda_buf[copy_len] = '\0';
|
||||
return StringRef(lambda_buf, copy_len);
|
||||
}
|
||||
#endif
|
||||
case VALUE:
|
||||
return StringRef(this->value_->data(), this->value_->size());
|
||||
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
||||
@@ -284,7 +239,6 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
LAMBDA,
|
||||
STATELESS_LAMBDA,
|
||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
|
||||
} type_;
|
||||
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
|
||||
// For other types, store value inline as before.
|
||||
@@ -293,7 +247,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
ValueStorage value_; // T for inline storage, T* for heap storage
|
||||
std::function<T(X...)> *f_;
|
||||
T (*stateless_f_)(X...);
|
||||
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
|
||||
const char *static_str_; // For STATIC_STRING type
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -148,9 +148,9 @@
|
||||
#define USE_MQTT
|
||||
#define USE_MQTT_COVER_JSON
|
||||
#define USE_NETWORK
|
||||
#define USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#define USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#define USE_RUNTIME_IMAGE_BMP
|
||||
#define USE_RUNTIME_IMAGE_PNG
|
||||
#define USE_RUNTIME_IMAGE_JPEG
|
||||
#define USE_OTA
|
||||
#define USE_OTA_PASSWORD
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
|
||||
@@ -152,13 +152,11 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m
|
||||
this->unit_of_measurement_ = unit_of_measurement;
|
||||
}
|
||||
|
||||
#ifdef USE_ENTITY_ICON
|
||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_icon_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
|
||||
if (!obj.get_device_class_ref().empty()) {
|
||||
|
||||
@@ -231,12 +231,8 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
|
||||
};
|
||||
|
||||
/// Log entity icon if set (for use in dump_config)
|
||||
#ifdef USE_ENTITY_ICON
|
||||
#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj)
|
||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
#else
|
||||
#define LOG_ENTITY_ICON(tag, prefix, obj)
|
||||
#endif
|
||||
/// Log entity device class if set (for use in dump_config)
|
||||
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
|
||||
|
||||
@@ -348,10 +348,7 @@ std::string format_hex(const uint8_t *data, size_t length) {
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
|
||||
@@ -520,8 +517,10 @@ int8_t step_to_accuracy_decimals(float step) {
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
|
||||
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
|
||||
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Helper function to find the index of a base64/base64url character in the lookup table.
|
||||
// Returns the character's position (0-63) if found, or 0 if not found.
|
||||
|
||||
@@ -133,78 +133,6 @@ template<typename T> class ConstVector {
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/// Small buffer optimization - stores data inline when small, heap-allocates for large data
|
||||
/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes.
|
||||
/// Memory management is encapsulated - callers just use set() and data().
|
||||
template<size_t InlineSize = 8> class SmallInlineBuffer {
|
||||
public:
|
||||
SmallInlineBuffer() = default;
|
||||
~SmallInlineBuffer() {
|
||||
if (!this->is_inline_())
|
||||
delete[] this->heap_;
|
||||
}
|
||||
|
||||
// Move constructor
|
||||
SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) {
|
||||
if (other.is_inline_()) {
|
||||
memcpy(this->inline_, other.inline_, this->len_);
|
||||
} else {
|
||||
this->heap_ = other.heap_;
|
||||
other.heap_ = nullptr;
|
||||
}
|
||||
other.len_ = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (!this->is_inline_())
|
||||
delete[] this->heap_;
|
||||
this->len_ = other.len_;
|
||||
if (other.is_inline_()) {
|
||||
memcpy(this->inline_, other.inline_, this->len_);
|
||||
} else {
|
||||
this->heap_ = other.heap_;
|
||||
other.heap_ = nullptr;
|
||||
}
|
||||
other.len_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Disable copy (would need deep copy of heap data)
|
||||
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
|
||||
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
|
||||
|
||||
/// Set buffer contents, allocating heap if needed
|
||||
void set(const uint8_t *src, size_t size) {
|
||||
// Free existing heap allocation if switching from heap to inline or different heap size
|
||||
if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) {
|
||||
delete[] this->heap_;
|
||||
this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes
|
||||
}
|
||||
// Allocate new heap buffer if needed
|
||||
if (size > InlineSize && (this->is_inline_() || size != this->len_)) {
|
||||
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
this->len_ = size;
|
||||
memcpy(this->data(), src, size);
|
||||
}
|
||||
|
||||
uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
size_t size() const { return this->len_; }
|
||||
|
||||
protected:
|
||||
bool is_inline_() const { return this->len_ <= InlineSize; }
|
||||
|
||||
size_t len_{0};
|
||||
union {
|
||||
uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state
|
||||
uint8_t *heap_;
|
||||
};
|
||||
};
|
||||
|
||||
/// Minimal static vector - saves memory by avoiding std::vector overhead
|
||||
template<typename T, size_t N> class StaticVector {
|
||||
public:
|
||||
@@ -269,9 +197,6 @@ template<typename T, size_t N> class StaticVector {
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
|
||||
// Direct access to size counter for efficient in-place construction
|
||||
size_t &count() { return count_; }
|
||||
|
||||
// Direct access to underlying data
|
||||
T *data() { return data_.data(); }
|
||||
const T *data() const { return data_.data(); }
|
||||
@@ -1143,17 +1068,13 @@ std::string format_hex(const std::vector<uint8_t> &data);
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
|
||||
val = convert_big_endian(val);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
/// Format the std::array \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
|
||||
return format_hex(data.data(), data.size());
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
/** Format a byte array in pretty-printed, human-readable hex format.
|
||||
|
||||
@@ -149,9 +149,10 @@ class Scheduler {
|
||||
} name_;
|
||||
uint32_t interval;
|
||||
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
|
||||
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
|
||||
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
|
||||
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
|
||||
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
|
||||
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// even when devices run for months. Split into two fields for better memory
|
||||
// alignment on 32-bit systems.
|
||||
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -66,121 +67,56 @@ std::string ESPTime::strftime(const char *format) {
|
||||
|
||||
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
|
||||
|
||||
// Helper to parse exactly N digits, returns false if not enough digits
|
||||
static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) {
|
||||
value = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (p >= end || *p < '0' || *p > '9')
|
||||
return false;
|
||||
value = value * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper to check for expected character
|
||||
static bool expect_char(const char *&p, const char *end, char expected) {
|
||||
if (p >= end || *p != expected)
|
||||
return false;
|
||||
p++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
|
||||
// Supported formats:
|
||||
// YYYY-MM-DD HH:MM:SS (19 chars)
|
||||
// YYYY-MM-DD HH:MM (16 chars)
|
||||
// YYYY-MM-DD (10 chars)
|
||||
// HH:MM:SS (8 chars)
|
||||
// HH:MM (5 chars)
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
int num;
|
||||
const int ilen = static_cast<int>(len);
|
||||
|
||||
if (time_to_parse == nullptr || len == 0)
|
||||
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, // NOLINT
|
||||
&second, &num) == 6 && // NOLINT
|
||||
num == ilen) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = second;
|
||||
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, &num) == 5 && // NOLINT
|
||||
num == ilen) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = 0;
|
||||
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
|
||||
num == ilen) {
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = second;
|
||||
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
|
||||
num == ilen) {
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = 0;
|
||||
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
|
||||
num == ilen) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
const char *p = time_to_parse;
|
||||
const char *end = time_to_parse + len;
|
||||
uint16_t v1, v2, v3, v4, v5, v6;
|
||||
|
||||
// Try date formats first (start with 4-digit year)
|
||||
if (len >= 10 && time_to_parse[4] == '-') {
|
||||
// YYYY-MM-DD...
|
||||
if (!parse_digits(p, end, 4, v1))
|
||||
return false;
|
||||
if (!expect_char(p, end, '-'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v2))
|
||||
return false;
|
||||
if (!expect_char(p, end, '-'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v3))
|
||||
return false;
|
||||
|
||||
esp_time.year = v1;
|
||||
esp_time.month = v2;
|
||||
esp_time.day_of_month = v3;
|
||||
|
||||
if (p == end) {
|
||||
// YYYY-MM-DD (date only)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!expect_char(p, end, ' '))
|
||||
return false;
|
||||
|
||||
// Continue with time part: HH:MM[:SS]
|
||||
if (!parse_digits(p, end, 2, v4))
|
||||
return false;
|
||||
if (!expect_char(p, end, ':'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v5))
|
||||
return false;
|
||||
|
||||
esp_time.hour = v4;
|
||||
esp_time.minute = v5;
|
||||
|
||||
if (p == end) {
|
||||
// YYYY-MM-DD HH:MM
|
||||
esp_time.second = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!expect_char(p, end, ':'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v6))
|
||||
return false;
|
||||
|
||||
esp_time.second = v6;
|
||||
return p == end; // YYYY-MM-DD HH:MM:SS
|
||||
}
|
||||
|
||||
// Try time-only formats (HH:MM[:SS])
|
||||
if (len >= 5 && time_to_parse[2] == ':') {
|
||||
if (!parse_digits(p, end, 2, v1))
|
||||
return false;
|
||||
if (!expect_char(p, end, ':'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v2))
|
||||
return false;
|
||||
|
||||
esp_time.hour = v1;
|
||||
esp_time.minute = v2;
|
||||
|
||||
if (p == end) {
|
||||
// HH:MM
|
||||
esp_time.second = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!expect_char(p, end, ':'))
|
||||
return false;
|
||||
if (!parse_digits(p, end, 2, v3))
|
||||
return false;
|
||||
|
||||
esp_time.second = v3;
|
||||
return p == end; // HH:MM:SS
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESPTime::increment_second() {
|
||||
@@ -257,67 +193,27 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
|
||||
}
|
||||
|
||||
void ESPTime::recalc_timestamp_local() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
// Calculate timestamp as if fields were UTC
|
||||
this->recalc_timestamp_utc(false);
|
||||
if (this->timestamp == -1) {
|
||||
return; // Invalid time
|
||||
}
|
||||
struct tm tm;
|
||||
|
||||
// Now convert from local to UTC by adding the offset
|
||||
// POSIX: local = utc - offset, so utc = local + offset
|
||||
const auto &tz = time::get_global_tz();
|
||||
tm.tm_year = this->year - 1900;
|
||||
tm.tm_mon = this->month - 1;
|
||||
tm.tm_mday = this->day_of_month;
|
||||
tm.tm_hour = this->hour;
|
||||
tm.tm_min = this->minute;
|
||||
tm.tm_sec = this->second;
|
||||
tm.tm_isdst = -1;
|
||||
|
||||
if (!tz.has_dst()) {
|
||||
// No DST - just apply standard offset
|
||||
this->timestamp += tz.std_offset_seconds;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try both interpretations to match libc mktime() with tm_isdst=-1
|
||||
// For ambiguous times (fall-back repeated hour), prefer standard time
|
||||
// For invalid times (spring-forward skipped hour), libc normalizes forward
|
||||
time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds;
|
||||
time_t utc_if_std = this->timestamp + tz.std_offset_seconds;
|
||||
|
||||
bool dst_valid = time::is_in_dst(utc_if_dst, tz);
|
||||
bool std_valid = !time::is_in_dst(utc_if_std, tz);
|
||||
|
||||
if (dst_valid && std_valid) {
|
||||
// Ambiguous time (repeated hour during fall-back) - prefer standard time
|
||||
this->timestamp = utc_if_std;
|
||||
} else if (dst_valid) {
|
||||
// Only DST interpretation is valid
|
||||
this->timestamp = utc_if_dst;
|
||||
} else if (std_valid) {
|
||||
// Only standard interpretation is valid
|
||||
this->timestamp = utc_if_std;
|
||||
} else {
|
||||
// Invalid time (skipped hour during spring-forward)
|
||||
// libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT
|
||||
// Using std offset achieves this since the UTC result falls during DST
|
||||
this->timestamp = utc_if_std;
|
||||
}
|
||||
#else
|
||||
// No timezone support - treat as UTC
|
||||
this->recalc_timestamp_utc(false);
|
||||
#endif
|
||||
this->timestamp = mktime(&tm);
|
||||
}
|
||||
|
||||
int32_t ESPTime::timezone_offset() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
time_t now = ::time(nullptr);
|
||||
const auto &tz = time::get_global_tz();
|
||||
// POSIX offset is positive west, but we return offset to add to UTC to get local
|
||||
// So we negate the POSIX offset
|
||||
if (time::is_in_dst(now, tz)) {
|
||||
return -tz.dst_offset_seconds;
|
||||
}
|
||||
return -tz.std_offset_seconds;
|
||||
#else
|
||||
// No timezone support - no offset
|
||||
return 0;
|
||||
#endif
|
||||
struct tm local_tm = *::localtime(&now);
|
||||
local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset.
|
||||
time_t local_time = mktime(&local_tm);
|
||||
struct tm utc_tm = *::gmtime(&now);
|
||||
time_t utc_time = mktime(&utc_tm);
|
||||
return static_cast<int32_t>(local_time - utc_time);
|
||||
}
|
||||
|
||||
bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
#include "esphome/components/time/posix_tz.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
template<typename T> bool increment_time_value(T ¤t, uint16_t begin, uint16_t end);
|
||||
@@ -109,17 +105,11 @@ struct ESPTime {
|
||||
* @return The generated ESPTime
|
||||
*/
|
||||
static ESPTime from_epoch_local(time_t epoch) {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
struct tm local_tm;
|
||||
if (time::epoch_to_local_tm(epoch, time::get_global_tz(), &local_tm)) {
|
||||
return ESPTime::from_c_tm(&local_tm, epoch);
|
||||
struct tm *c_tm = ::localtime(&epoch);
|
||||
if (c_tm == nullptr) {
|
||||
return ESPTime{}; // Return an invalid ESPTime
|
||||
}
|
||||
// Fallback to UTC if conversion failed
|
||||
return ESPTime::from_epoch_utc(epoch);
|
||||
#else
|
||||
// No timezone support - return UTC (no TZ configured, localtime would return UTC anyway)
|
||||
return ESPTime::from_epoch_utc(epoch);
|
||||
#endif
|
||||
return ESPTime::from_c_tm(c_tm, epoch);
|
||||
}
|
||||
/** Convert an UTC epoch timestamp to a UTC time ESPTime instance.
|
||||
*
|
||||
|
||||
@@ -247,23 +247,6 @@ class LogStringLiteral(Literal):
|
||||
return f"LOG_STR({cpp_string_escape(self.string)})"
|
||||
|
||||
|
||||
class FlashStringLiteral(Literal):
|
||||
"""A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266.
|
||||
|
||||
On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM).
|
||||
On other platforms, ESPHOME_F(s) expands to plain s (no-op).
|
||||
"""
|
||||
|
||||
__slots__ = ("string",)
|
||||
|
||||
def __init__(self, string: str) -> None:
|
||||
super().__init__()
|
||||
self.string = string
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ESPHOME_F({cpp_string_escape(self.string)})"
|
||||
|
||||
|
||||
class IntLiteral(Literal):
|
||||
__slots__ = ("i",)
|
||||
|
||||
@@ -778,10 +761,6 @@ async def templatable(
|
||||
if is_template(value):
|
||||
return await process_lambda(value, args, return_type=output_type)
|
||||
if to_exp is None:
|
||||
# Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266.
|
||||
# On other platforms ESPHOME_F() is a no-op returning const char*.
|
||||
if isinstance(value, str) and str(output_type) == "std::string":
|
||||
return FlashStringLiteral(value)
|
||||
return value
|
||||
if isinstance(to_exp, dict):
|
||||
return to_exp[value]
|
||||
|
||||
@@ -173,16 +173,7 @@ def run_compile(config, verbose):
|
||||
args = []
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
|
||||
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
|
||||
result = run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
# Run memory analysis if enabled
|
||||
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
|
||||
try:
|
||||
analyze_memory_usage(config)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to analyze memory usage: %s", e)
|
||||
|
||||
return result
|
||||
return run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
|
||||
def _run_idedata(config):
|
||||
@@ -436,74 +427,3 @@ class IDEData:
|
||||
def defines(self) -> list[str]:
|
||||
"""Return the list of preprocessor defines from idedata."""
|
||||
return self.raw.get("defines", [])
|
||||
|
||||
|
||||
def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
"""Analyze memory usage by component after compilation."""
|
||||
# Lazy import to avoid overhead when not needed
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.helpers import get_esphome_components
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
elf_path = idedata.firmware_elf_path
|
||||
objdump_path = idedata.objdump_path
|
||||
readelf_path = idedata.readelf_path
|
||||
|
||||
# Debug logging
|
||||
_LOGGER.debug("ELF path from idedata: %s", elf_path)
|
||||
|
||||
# Check if file exists
|
||||
if not Path(elf_path).exists():
|
||||
# Try alternate path
|
||||
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
|
||||
if alt_path.exists():
|
||||
elf_path = str(alt_path)
|
||||
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
|
||||
else:
|
||||
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
|
||||
return
|
||||
|
||||
# Extract external components from config
|
||||
external_components = set()
|
||||
|
||||
# Get the list of built-in ESPHome components
|
||||
builtin_components = get_esphome_components()
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
NON_COMPONENT_KEYS = {
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"<<",
|
||||
}
|
||||
|
||||
# Check all top-level keys in config
|
||||
for key in config:
|
||||
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
|
||||
# This is an external component
|
||||
external_components.add(key)
|
||||
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
elf_path, objdump_path, readelf_path, external_components
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
report = analyzer.generate_report()
|
||||
_LOGGER.info("\n%s", report)
|
||||
|
||||
# Optionally save to file
|
||||
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
|
||||
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
|
||||
if report_file.suffix == ".json":
|
||||
report_file.write_text(analyzer.to_json())
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
else:
|
||||
report_file.write_text(report)
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
|
||||
@@ -66,7 +66,6 @@ def create_test_config(config_name: str, includes: list[str]) -> dict:
|
||||
],
|
||||
"build_flags": [
|
||||
"-Og", # optimize for debug
|
||||
"-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing
|
||||
],
|
||||
"debug_build_flags": [ # only for debug builds
|
||||
"-g3", # max debug info
|
||||
|
||||
@@ -4,16 +4,15 @@ interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test build_json - returns SerializationBuffer, use auto to avoid heap allocation
|
||||
auto json_buf = esphome::json::build_json([](JsonObject root) {
|
||||
// Test build_json
|
||||
std::string json_str = esphome::json::build_json([](JsonObject root) {
|
||||
root["sensor"] = "temperature";
|
||||
root["value"] = 23.5;
|
||||
root["unit"] = "°C";
|
||||
});
|
||||
ESP_LOGD("test", "Built JSON: %s", json_buf.c_str());
|
||||
ESP_LOGD("test", "Built JSON: %s", json_str.c_str());
|
||||
|
||||
// Test parse_json - implicit conversion to std::string for backward compatibility
|
||||
std::string json_str = json_buf;
|
||||
// Test parse_json
|
||||
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
|
||||
if (root["sensor"].is<const char*>() && root["value"].is<float>()) {
|
||||
const char* sensor = root["sensor"];
|
||||
@@ -27,10 +26,10 @@ interval:
|
||||
});
|
||||
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");
|
||||
|
||||
// Test JsonBuilder class - returns SerializationBuffer
|
||||
// Test JsonBuilder class
|
||||
esphome::json::JsonBuilder builder;
|
||||
JsonObject obj = builder.root();
|
||||
obj["test"] = "direct_builder";
|
||||
obj["count"] = 42;
|
||||
auto result = builder.serialize();
|
||||
std::string result = builder.serialize();
|
||||
ESP_LOGD("test", "JsonBuilder result: %s", result.c_str());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
||||
esphome:
|
||||
name: test-user-services-union
|
||||
friendly_name: Test User Services Union Storage
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: "test"
|
||||
password: "password"
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Test service with no arguments
|
||||
- action: test_no_args
|
||||
then:
|
||||
- logger.log: "No args service called"
|
||||
|
||||
# Test service with one argument
|
||||
- action: test_one_arg
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "One arg service: %d"
|
||||
args: [value]
|
||||
|
||||
# Test service with multiple arguments of different types
|
||||
- action: test_multi_args
|
||||
variables:
|
||||
int_val: int
|
||||
float_val: float
|
||||
str_val: string
|
||||
bool_val: bool
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Multi args: %d, %.2f, %s, %d"
|
||||
args: [int_val, float_val, str_val.c_str(), bool_val]
|
||||
|
||||
# Test service with max typical arguments
|
||||
- action: test_many_args
|
||||
variables:
|
||||
arg1: int
|
||||
arg2: int
|
||||
arg3: int
|
||||
arg4: string
|
||||
arg5: float
|
||||
then:
|
||||
- logger.log: "Many args service called"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Test Binary Sensor"
|
||||
id: test_sensor
|
||||
@@ -248,12 +248,6 @@ class TestLiterals:
|
||||
(cg.FloatLiteral(4.2), "4.2f"),
|
||||
(cg.FloatLiteral(1.23456789), "1.23456789f"),
|
||||
(cg.FloatLiteral(math.nan), "NAN"),
|
||||
(cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'),
|
||||
(cg.FlashStringLiteral(""), 'ESPHOME_F("")'),
|
||||
(
|
||||
cg.FlashStringLiteral('quote"here'),
|
||||
'ESPHOME_F("quote\\042here")',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_str__simple(self, target: cg.Literal, expected: str):
|
||||
@@ -630,75 +624,3 @@ class TestProcessLambda:
|
||||
# Test invalid tuple format (single element)
|
||||
with pytest.raises(AssertionError):
|
||||
await cg.process_lambda(lambda_obj, [(int,)])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__string_with_std_string_returns_flash_literal() -> None:
|
||||
"""Static string with std::string output_type returns FlashStringLiteral."""
|
||||
result = await cg.templatable("hello", [], ct.std_string)
|
||||
|
||||
assert isinstance(result, cg.FlashStringLiteral)
|
||||
assert str(result) == 'ESPHOME_F("hello")'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__empty_string_with_std_string() -> None:
|
||||
"""Empty static string with std::string output_type returns FlashStringLiteral."""
|
||||
result = await cg.templatable("", [], ct.std_string)
|
||||
|
||||
assert isinstance(result, cg.FlashStringLiteral)
|
||||
assert str(result) == 'ESPHOME_F("")'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__string_with_none_output_type() -> None:
|
||||
"""Static string with output_type=None returns raw string (no wrapping)."""
|
||||
result = await cg.templatable("hello", [], None)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert result == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__int_with_std_string() -> None:
|
||||
"""Non-string value with std::string output_type returns raw value."""
|
||||
result = await cg.templatable(42, [], ct.std_string)
|
||||
|
||||
assert result == 42
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__string_with_non_string_output_type() -> None:
|
||||
"""Static string with non-std::string output_type returns raw string."""
|
||||
result = await cg.templatable("hello", [], ct.bool_)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert result == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__with_to_exp_callable() -> None:
|
||||
"""When to_exp is provided, it is applied to non-template values."""
|
||||
result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2)
|
||||
|
||||
assert result == 84
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__with_to_exp_dict() -> None:
|
||||
"""When to_exp is a dict, value is looked up."""
|
||||
mapping: dict[str, int] = {"on": 1, "off": 0}
|
||||
result = await cg.templatable("on", [], None, to_exp=mapping)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_templatable__lambda_with_std_string() -> None:
|
||||
"""Lambda value returns LambdaExpression, not FlashStringLiteral."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda('return "hello";')
|
||||
result = await cg.templatable(lambda_obj, [], ct.std_string)
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
|
||||
@@ -453,7 +453,6 @@ def test_clean_build(
|
||||
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
|
||||
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
|
||||
mock_core.relative_build_path.return_value = dependencies_lock
|
||||
mock_core.platformio_cache_dir = str(platformio_cache_dir)
|
||||
|
||||
# Verify all exist before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
Reference in New Issue
Block a user