1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Compare commits

...

45 Commits

Author SHA1 Message Date
J. Nick Koston
c12b116176 fix 2026-01-09 13:41:14 -10:00
J. Nick Koston
a1044664ab fix 2026-01-09 13:39:38 -10:00
J. Nick Koston
4365e7a963 fix 2026-01-09 13:36:11 -10:00
J. Nick Koston
b0dfceb6d2 tweaks 2026-01-09 13:24:35 -10:00
J. Nick Koston
883d2c506f tweaks 2026-01-09 13:21:23 -10:00
J. Nick Koston
8bb201262f tweaks 2026-01-09 13:18:38 -10:00
J. Nick Koston
e54060e296 tweaks 2026-01-09 13:17:38 -10:00
J. Nick Koston
d3c3454778 tweaks 2026-01-09 13:16:52 -10:00
J. Nick Koston
32e0f2624d tweaks 2026-01-09 13:15:29 -10:00
J. Nick Koston
80d3adf703 tweaks 2026-01-09 13:15:13 -10:00
J. Nick Koston
b7f95f5fc0 tweaks 2026-01-09 13:08:41 -10:00
J. Nick Koston
6640205150 tweaks 2026-01-09 13:07:18 -10:00
J. Nick Koston
8de9be679c tweaks 2026-01-09 13:02:26 -10:00
J. Nick Koston
72983bd83d tweak 2026-01-09 11:53:51 -10:00
J. Nick Koston
fc2324c002 wip 2026-01-09 11:49:07 -10:00
J. Nick Koston
e227fc4f1a wip 2026-01-09 11:24:36 -10:00
J. Nick Koston
59bd60b4e2 wip 2026-01-09 11:21:20 -10:00
J. Nick Koston
6b02f9f1ec wip 2026-01-09 11:16:51 -10:00
J. Nick Koston
45cab9b5b8 wip 2026-01-09 11:16:20 -10:00
J. Nick Koston
12c8c5cd7f Merge remote-tracking branch 'upstream/dev' into web_server_cap_portal_co_exist 2026-01-09 10:17:51 -10:00
J. Nick Koston
be059b3368 update header 2026-01-09 10:10:25 -10:00
J. Nick Koston
32f90b2855 [mdns] Remove deprecated api password from test configuration (#13107) 2026-01-09 09:40:24 -10:00
J. Nick Koston
2fb7c0d453 [mapping] Fix test SPI data rate for RP2040 (#13108) 2026-01-09 09:39:53 -10:00
dependabot[bot]
7935fba4b1 Bump esphome-dashboard from 20251013.0 to 20260110.0 (#13109)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-09 14:37:53 -05:00
Stuart Parmenter
ab32b93928 [hub75] Fix gamma_correct to use enum value instead of key string (#13102)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 13:34:04 -06:00
J. Nick Koston
3d54ccac65 Revert "[wifi] Disable SoftAP support on Arduino ESP32 when ap: not configured" (#13099) 2026-01-09 09:35:19 -05:00
Keith Burzinski
c40f44f4bd [remote_base] Add zero-copy packed sint32 decoder for #12985 (#13093) 2026-01-09 04:06:03 -06:00
Keith Burzinski
62cb08c3dc [api] Add methods supporting efficient packed repeated sint32 field encoding for #12985 (#13094) 2026-01-09 04:05:47 -06:00
Stuart Parmenter
7576e032f8 [hub75] Fix depth and gamma mode defines (#13091) 2026-01-09 01:56:51 -06:00
J. Nick Koston
cd43b4114e [api] Fire on_client_disconnected trigger after removing client from list (#13088) 2026-01-08 20:36:24 -10:00
J. Nick Koston
2c165e4817 [web_server] Use centralized length constants for buffer sizing (#13073) 2026-01-08 20:36:08 -10:00
J. Nick Koston
5afe4b7b12 [wifi] Warn when AP is configured without captive_portal or web_server (#13087) 2026-01-08 16:41:34 -10:00
J. Nick Koston
10ed44165d [captive_portal] Allow web_server access while captive portal is active 2026-01-08 15:42:05 -10:00
Anton Viktorov
dcb8c994cc [ac_dimmer] Added support for ESP-IDF (5+) (#7072)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 15:24:01 -10:00
Rodrigo Martín
012a1e2afd [mqtt] Include session_present and reason parameters in connection callbacks (#12413)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-08 22:05:53 +00:00
J. Nick Koston
d4969f581a [wifi] Limit ignored disconnect events on LibreTiny to speed up AP failover (#13070) 2026-01-08 11:42:30 -10:00
J. Nick Koston
40f108116b [mqtt] Reduce heap allocations in topic string building (#13072) 2026-01-08 11:42:18 -10:00
J. Nick Koston
52459d1bc7 [wifi] Fix infinite roaming when best-signal AP is crashed/broken (#13071) 2026-01-08 11:42:06 -10:00
dependabot[bot]
325c938074 Bump ruff from 0.14.10 to 0.14.11 (#13082)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-08 20:57:30 +00:00
J. Nick Koston
423a617b15 [core] Improve minimum_chip_revision warning for PSRAM users (#13074) 2026-01-08 10:52:27 -10:00
J. Nick Koston
eb5c4f34e2 [wifi] Disable SoftAP support on Arduino ESP32 when ap: not configured (#13076) 2026-01-08 10:51:58 -10:00
J. Nick Koston
c9ab4ca018 [libretiny] Bump to 1.9.2 (#13077) 2026-01-08 10:51:35 -10:00
J. Nick Koston
da0b01f4d0 [logger] Enable loop disable optimization for LibreTiny task log buffer (#13078) 2026-01-08 10:51:18 -10:00
Keith Burzinski
e301b8d0e0 [thermostat] Allow heat_cool_mode without an automation (#13069)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-01-07 21:44:10 -06:00
Clyde Stubbs
738678e87b [image] Add define and core data (#13058) 2026-01-08 11:20:37 +11:00
105 changed files with 1447 additions and 497 deletions

View File

@@ -1 +1 @@
191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.14.11
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "ac_dimmer.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -9,12 +7,12 @@
#ifdef USE_ESP8266
#include <core_esp8266_waveform.h>
#endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp32-hal-timer.h>
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#endif
namespace esphome {
namespace ac_dimmer {
namespace esphome::ac_dimmer {
static const char *const TAG = "ac_dimmer";
@@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
/// However other factors like gate driver propagation time
/// are also considered and a really low value is not important
/// See also: https://github.com/esphome/issues/issues/1632
static const uint32_t GATE_ENABLE_TIME = 50;
static constexpr uint32_t GATE_ENABLE_TIME = 50;
#ifdef USE_ESP32
/// Timer frequency in Hz (1 MHz = 1µs resolution)
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
/// Timer interrupt interval in microseconds
static constexpr uint64_t TIMER_INTERVAL_US = 50;
#endif
/// Function called from timer interrupt
/// Input is current time in microseconds (micros())
@@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
#ifdef USE_ESP32
// ESP32 implementation, uses basically the same code but needs to wrap
// timer_interrupt() function to auto-reschedule
static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
#endif
@@ -194,15 +199,15 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
// timer frequency of 1mhz
dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timerAlarm(dimmer_timer, 50, true, 0);
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
#endif
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
@@ -210,6 +215,7 @@ void AcDimmer::write_state(float state) {
this->store_.init_cycle = this->init_with_half_cycle_;
this->store_.value = new_value;
}
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
@@ -230,7 +236,4 @@ void AcDimmer::dump_config() {
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO
} // namespace esphome::ac_dimmer

View File

@@ -1,13 +1,10 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
namespace esphome {
namespace ac_dimmer {
namespace esphome::ac_dimmer {
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
@@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component {
DimMethod method_;
};
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO
} // namespace esphome::ac_dimmer

View File

@@ -0,0 +1,152 @@
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#include "freertos/FreeRTOS.h"
#include "esphome/core/log.h"
#include "driver/gptimer.h"
#include "esp_clk_tree.h"
#include "soc/clk_tree_defs.h"
static const char *const TAG = "hw_timer_esp_idf";
namespace esphome::ac_dimmer {
// GPTimer divider constraints from ESP-IDF documentation
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
using voidFuncPtr = void (*)();
using voidFuncPtrArg = void (*)(void *);
struct InterruptConfigT {
voidFuncPtr fn{nullptr};
void *arg{nullptr};
};
struct HWTimer {
gptimer_handle_t timer_handle{nullptr};
InterruptConfigT interrupt_handle{};
bool timer_started{false};
};
HWTimer *timer_begin(uint32_t frequency) {
esp_err_t err = ESP_OK;
uint32_t counter_src_hz = 0;
uint32_t divider = 0;
soc_module_clk_t clk;
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
clk = clk_candidate;
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
divider = counter_src_hz / frequency;
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
break;
} else {
divider = 0;
}
}
if (divider == 0) {
ESP_LOGE(TAG, "Resolution not possible; aborting");
return nullptr;
}
gptimer_config_t config = {
.clk_src = static_cast<gptimer_clock_source_t>(clk),
.direction = GPTIMER_COUNT_UP,
.resolution_hz = frequency,
.flags = {.intr_shared = true},
};
HWTimer *timer = new HWTimer();
err = gptimer_new_timer(&config, &timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
delete timer;
return nullptr;
}
err = gptimer_enable(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
err = gptimer_start(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
gptimer_disable(timer->timer_handle);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
timer->timer_started = true;
return timer;
}
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
auto *isr = static_cast<InterruptConfigT *>(args);
if (isr->fn) {
if (isr->arg) {
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
} else {
isr->fn();
}
}
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
return false;
}
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_event_callbacks_t cbs = {
.on_alarm = timer_fn_wrapper,
};
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
timer->interrupt_handle.arg = arg;
if (timer->timer_started) {
gptimer_stop(timer->timer_handle);
}
gptimer_disable(timer->timer_handle);
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
}
gptimer_enable(timer->timer_handle);
if (timer->timer_started) {
gptimer_start(timer->timer_handle);
}
}
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
}
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_alarm_config_t alarm_cfg = {
.alarm_count = alarm_value,
.reload_count = reload_count,
.flags = {.auto_reload_on_alarm = autoreload},
};
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
}
}
} // namespace esphome::ac_dimmer
#endif

View File

@@ -0,0 +1,17 @@
#pragma once
#ifdef USE_ESP32
#include "driver/gptimer_types.h"
namespace esphome::ac_dimmer {
struct HWTimer;
HWTimer *timer_begin(uint32_t frequency);
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
} // namespace esphome::ac_dimmer
#endif

View File

@@ -32,7 +32,6 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
)

View File

@@ -186,14 +186,17 @@ void APIServer::loop() {
}
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername()));
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
@@ -205,6 +208,11 @@ void APIServer::loop() {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_->trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -39,6 +39,24 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
}
/// Count number of varints in a packed buffer
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
uint16_t count = 0;
while (len > 0) {
// Skip varint bytes until we find one without continuation bit
while (len > 0 && (*data & 0x80)) {
data++;
len--;
}
if (len > 0) {
data++;
len--;
count++;
}
}
return count;
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -180,9 +198,10 @@ class ProtoVarInt {
uint64_t value_;
};
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
class ProtoDecodableMessage;
class ProtoMessage;
class ProtoSize;
class ProtoLengthDelimited {
public:
@@ -334,6 +353,8 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
@@ -341,9 +362,6 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_;
};
// Forward declaration
class ProtoSize;
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
@@ -792,8 +810,43 @@ class ProtoSize {
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
}
};
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
return;
// Calculate packed size
size_t packed_size = 0;
for (int value : values) {
packed_size += ProtoSize::varint(encode_zigzag32(value));
}
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
this->encode_varint_raw(packed_size);
for (int value : values) {
this->encode_varint_raw(encode_zigzag32(value));
}
}
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message

View File

@@ -101,19 +101,23 @@ async def to_code(config):
if config[CONF_COMPRESSION] == "gzip":
cg.add_define("USE_CAPTIVE_PORTAL_GZIP")
if CORE.using_arduino:
if CORE.is_esp8266:
cg.add_library("DNSServer", None)
if CORE.is_libretiny:
cg.add_library("DNSServer", None)
# All platforms now use our custom DNS server implementations
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
# Compile platform-specific DNS server implementations
# ESP32 Arduino uses IDF components, so both use dns_server_esp32_idf.cpp
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"dns_server_esp32_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"dns_server_arduino.cpp": {
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@@ -7,145 +7,162 @@ namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x95, 0x56, 0xeb, 0x6f, 0xd4, 0x38, 0x10, 0xff, 0xce,
0x5f, 0xe1, 0x33, 0x8f, 0x26, 0xd0, 0x3c, 0xb7, 0xdb, 0x96, 0x6c, 0x12, 0x04, 0xdc, 0x21, 0x90, 0x28, 0x20, 0xb5,
0x70, 0x1f, 0x10, 0x52, 0xbd, 0xc9, 0x64, 0x63, 0x9a, 0x38, 0x39, 0xdb, 0xfb, 0x62, 0xb5, 0xf7, 0xb7, 0xdf, 0x38,
0xc9, 0x6e, 0xb7, 0x15, 0x9c, 0xee, 0x5a, 0x35, 0x1d, 0xdb, 0xf3, 0xf8, 0xcd, 0x78, 0x1e, 0x8e, 0x7f, 0xcb, 0x9b,
0x4c, 0xaf, 0x5b, 0x20, 0xa5, 0xae, 0xab, 0x34, 0x36, 0x5f, 0x52, 0x31, 0x31, 0x4b, 0x40, 0xe0, 0x0a, 0x58, 0x9e,
0xc6, 0x35, 0x68, 0x46, 0xb2, 0x92, 0x49, 0x05, 0x3a, 0xf9, 0x7c, 0xf5, 0xc6, 0x39, 0x4f, 0xe3, 0x8a, 0x8b, 0x1b,
0x22, 0xa1, 0x4a, 0x78, 0xd6, 0x08, 0x52, 0x4a, 0x28, 0x92, 0x9c, 0x69, 0x16, 0xf1, 0x9a, 0xcd, 0x60, 0x10, 0x11,
0xac, 0x86, 0x64, 0xc1, 0x61, 0xd9, 0x36, 0x52, 0x13, 0xe4, 0xd3, 0x20, 0x74, 0x42, 0x97, 0x3c, 0xd7, 0x65, 0x92,
0xc3, 0x82, 0x67, 0xe0, 0x74, 0x8b, 0x63, 0x2e, 0xb8, 0xe6, 0xac, 0x72, 0x54, 0xc6, 0x2a, 0x48, 0x82, 0xe3, 0xb9,
0x02, 0xd9, 0x2d, 0xd8, 0x14, 0xd7, 0xa2, 0xa1, 0x69, 0xac, 0x32, 0xc9, 0x5b, 0x4d, 0x0c, 0xd4, 0xa4, 0x6e, 0xf2,
0x79, 0x05, 0xa9, 0xe7, 0x31, 0x85, 0x90, 0x94, 0xc7, 0x45, 0x0e, 0x2b, 0x77, 0xea, 0x67, 0x99, 0x0f, 0xe7, 0xe7,
0xee, 0x77, 0xf5, 0x00, 0x9d, 0x9a, 0xd7, 0x68, 0xcd, 0xad, 0x9a, 0x8c, 0x69, 0xde, 0x08, 0x57, 0x01, 0x93, 0x59,
0x99, 0x24, 0x09, 0x7d, 0xa1, 0xd8, 0x02, 0xe8, 0x93, 0x27, 0xd6, 0x9e, 0x69, 0x06, 0xfa, 0x8f, 0x0a, 0x0c, 0xa9,
0x5e, 0xad, 0xaf, 0xd8, 0xec, 0x03, 0x02, 0xb7, 0x28, 0x53, 0x3c, 0x07, 0x6a, 0x7f, 0xf5, 0xbf, 0xb9, 0x4a, 0xaf,
0x2b, 0x70, 0x73, 0xae, 0xda, 0x8a, 0xad, 0x13, 0x3a, 0x45, 0xad, 0x37, 0xd4, 0x9e, 0x14, 0x73, 0x91, 0x19, 0xe5,
0x44, 0x59, 0x60, 0x6f, 0x2a, 0x40, 0x78, 0xc9, 0x05, 0xd3, 0xa5, 0x5b, 0xb3, 0x95, 0xd5, 0x13, 0x5c, 0x58, 0xe1,
0x53, 0x0b, 0x9e, 0x05, 0xbe, 0x6f, 0x1f, 0x77, 0x1f, 0xdf, 0xf6, 0xf0, 0xff, 0x44, 0x82, 0x9e, 0x4b, 0x41, 0x98,
0x75, 0x1d, 0xb7, 0xc8, 0x49, 0xf2, 0x84, 0x5e, 0x04, 0x21, 0x09, 0x9e, 0xbb, 0xe1, 0xf8, 0xbd, 0x7b, 0x46, 0x4e,
0xf0, 0x7f, 0x76, 0xe6, 0x8c, 0x49, 0x70, 0x82, 0x9f, 0x30, 0x74, 0xc7, 0xc4, 0xff, 0x41, 0x49, 0xc1, 0xab, 0x2a,
0xa1, 0xa2, 0x11, 0x40, 0x89, 0xd2, 0xb2, 0xb9, 0x81, 0x84, 0x66, 0x73, 0x29, 0x11, 0xfb, 0xeb, 0xa6, 0x6a, 0x24,
0xf5, 0xd2, 0x07, 0xff, 0x4b, 0xa1, 0x96, 0x4c, 0xa8, 0xa2, 0x91, 0x75, 0x42, 0xbb, 0xe8, 0x5b, 0x8f, 0x36, 0x7a,
0x4b, 0xcc, 0xc7, 0x3e, 0x38, 0x74, 0x1a, 0xc9, 0x67, 0x5c, 0x24, 0xd4, 0x68, 0x3c, 0x47, 0x23, 0xd7, 0xf6, 0x76,
0xef, 0x3d, 0x33, 0xde, 0x0f, 0xfe, 0x34, 0xd6, 0xd7, 0xeb, 0x58, 0x2d, 0x66, 0x64, 0x55, 0x57, 0x42, 0x25, 0xb4,
0xd4, 0xba, 0x8d, 0x3c, 0x6f, 0xb9, 0x5c, 0xba, 0xcb, 0x91, 0xdb, 0xc8, 0x99, 0x17, 0xfa, 0xbe, 0xef, 0x21, 0x07,
0x25, 0x7d, 0x22, 0xd0, 0xf0, 0x84, 0x92, 0x12, 0xf8, 0xac, 0xd4, 0x1d, 0x9d, 0x3e, 0xda, 0xc0, 0x36, 0x36, 0x1c,
0xe9, 0xf5, 0xb7, 0x03, 0x2b, 0xfc, 0xc0, 0x0a, 0xbc, 0x60, 0x16, 0xdd, 0xb9, 0x79, 0xd4, 0xb9, 0x79, 0xc6, 0x42,
0x12, 0x12, 0xbf, 0xfb, 0x0d, 0x1d, 0x43, 0x0f, 0x2b, 0xe7, 0xde, 0x8a, 0x1c, 0xac, 0x0c, 0x55, 0x9f, 0x3a, 0xcf,
0xf7, 0xb2, 0x81, 0xd9, 0x59, 0x04, 0xfe, 0xed, 0x86, 0x11, 0x78, 0x7b, 0x7a, 0xb8, 0x76, 0xc2, 0x2f, 0x87, 0x0c,
0xc6, 0x5a, 0x19, 0x7c, 0x39, 0x65, 0x63, 0x32, 0x1e, 0x76, 0xc6, 0x8e, 0xa1, 0xf7, 0x2b, 0x32, 0x5e, 0x20, 0x47,
0xed, 0x9c, 0x3a, 0x63, 0x36, 0x22, 0xa3, 0x01, 0x08, 0x52, 0xb8, 0x7d, 0x8a, 0x82, 0x07, 0x7b, 0xce, 0xe8, 0xc7,
0x91, 0x97, 0x52, 0x3b, 0xa2, 0xf4, 0xd6, 0xf3, 0xe6, 0xd0, 0x73, 0xf7, 0x7b, 0x83, 0x39, 0x45, 0x29, 0x46, 0x06,
0x74, 0x56, 0x5a, 0xd4, 0xc3, 0xc2, 0x2a, 0xf8, 0x0c, 0xb3, 0xbe, 0x11, 0xd4, 0x76, 0x75, 0x09, 0xc2, 0xda, 0x89,
0x1a, 0x41, 0xe8, 0x4e, 0xac, 0xfb, 0x27, 0xda, 0xde, 0xec, 0xf3, 0x5f, 0x73, 0x8d, 0x65, 0xa6, 0x5d, 0x53, 0xb0,
0xc7, 0xfb, 0xdd, 0x69, 0x93, 0xaf, 0x7f, 0x51, 0x1a, 0x65, 0xd0, 0xd7, 0x05, 0x17, 0x02, 0xe4, 0x15, 0xac, 0xf0,
0xe6, 0x2e, 0x5e, 0xbe, 0x26, 0x2f, 0xf3, 0x5c, 0x82, 0x52, 0x11, 0xa1, 0xcf, 0x34, 0xd6, 0x40, 0xf6, 0xdf, 0x75,
0x05, 0x77, 0x74, 0xfd, 0xc9, 0xdf, 0x70, 0xf2, 0x01, 0xf4, 0xb2, 0x91, 0x37, 0x83, 0x36, 0x03, 0x6d, 0x62, 0x2a,
0x4c, 0x22, 0x4e, 0xd6, 0x2a, 0x57, 0x55, 0xd8, 0x3e, 0xac, 0xc0, 0x46, 0x3b, 0xed, 0xad, 0x57, 0x62, 0x17, 0xa8,
0xeb, 0x38, 0xe7, 0x0b, 0x92, 0x55, 0xd8, 0x21, 0xb0, 0x5c, 0x7a, 0x55, 0x94, 0x3c, 0x20, 0xdd, 0x4f, 0x23, 0x32,
0x94, 0xbe, 0x49, 0xe8, 0x4f, 0x3a, 0xc0, 0xab, 0xf5, 0xbb, 0xdc, 0x3a, 0x52, 0x58, 0xfb, 0x47, 0xb6, 0xbb, 0x60,
0xd5, 0x1c, 0x48, 0x42, 0x74, 0xc9, 0xd5, 0x2d, 0xc0, 0xc9, 0x2f, 0xc5, 0x5a, 0x75, 0x83, 0x52, 0x05, 0x1e, 0x2b,
0xcb, 0xa6, 0xe9, 0x60, 0x2e, 0x66, 0x7d, 0x83, 0xa4, 0x0f, 0xe9, 0x3d, 0x44, 0x4e, 0x05, 0x85, 0xde, 0xf3, 0x11,
0x2c, 0x3b, 0x65, 0x09, 0x57, 0xa2, 0x75, 0x7b, 0xbb, 0xdf, 0x8c, 0x55, 0xcb, 0xc4, 0x7d, 0x41, 0x03, 0xd0, 0x94,
0x0a, 0x36, 0x36, 0xa4, 0x4c, 0xbd, 0x20, 0xd3, 0xde, 0xa0, 0xc7, 0x76, 0xe4, 0xa3, 0x0d, 0x47, 0x8d, 0xa6, 0x5f,
0xed, 0x35, 0xc6, 0x1e, 0x86, 0x26, 0xbd, 0xde, 0xda, 0xb7, 0x7e, 0xfc, 0x35, 0x07, 0xb9, 0xbe, 0x84, 0x0a, 0x32,
0xdd, 0x48, 0x8b, 0x3e, 0x44, 0x2b, 0x98, 0x4a, 0x9d, 0xc3, 0x6f, 0xaf, 0x2e, 0xde, 0x27, 0x8d, 0x25, 0xed, 0xe3,
0x5f, 0x71, 0x9b, 0x51, 0xf0, 0x15, 0x47, 0xc1, 0xdf, 0xc9, 0x91, 0x19, 0x06, 0x47, 0xdf, 0x50, 0xb4, 0xf3, 0xf7,
0xfa, 0x76, 0x22, 0x98, 0x72, 0x7e, 0x86, 0x2d, 0xe1, 0xd8, 0x78, 0xe8, 0x9c, 0x8e, 0xed, 0x2d, 0xda, 0x47, 0x04,
0x88, 0xbb, 0xeb, 0xeb, 0xd8, 0xdf, 0x4d, 0x8b, 0x4d, 0x9f, 0x6e, 0xa6, 0xcd, 0xca, 0x51, 0xfc, 0x07, 0x17, 0xb3,
0x88, 0x8b, 0x12, 0x24, 0xd7, 0x5b, 0x84, 0x8b, 0x13, 0xa2, 0x9d, 0xeb, 0x4d, 0xcb, 0xf2, 0xdc, 0x9c, 0x8c, 0xdb,
0xd5, 0xa4, 0xc0, 0x79, 0x62, 0x38, 0x21, 0x0a, 0xa0, 0xde, 0xf6, 0xe7, 0x5d, 0x47, 0x89, 0x9e, 0x8f, 0x1f, 0x6f,
0x4d, 0xc2, 0x6d, 0x34, 0x5e, 0x96, 0xc3, 0x2a, 0x3e, 0x13, 0x51, 0x86, 0xc0, 0x41, 0xf6, 0x42, 0x05, 0xab, 0x79,
0xb5, 0x8e, 0x14, 0xf6, 0x36, 0x07, 0x07, 0x0d, 0x2f, 0xb6, 0xd3, 0xb9, 0xd6, 0x8d, 0x40, 0xdb, 0x32, 0x07, 0x19,
0xf9, 0x93, 0x9e, 0x70, 0x24, 0xcb, 0xf9, 0x5c, 0x45, 0xee, 0x48, 0x42, 0x3d, 0x99, 0xb2, 0xec, 0x66, 0x26, 0x9b,
0xb9, 0xc8, 0x9d, 0xcc, 0x74, 0xda, 0xe8, 0x61, 0x50, 0xb0, 0x11, 0x64, 0x93, 0x61, 0x55, 0x14, 0xc5, 0x04, 0x43,
0x01, 0x4e, 0xdf, 0xcb, 0xa2, 0xd0, 0x3d, 0x31, 0x62, 0x07, 0x30, 0xdd, 0xd0, 0x6c, 0xf4, 0x18, 0x71, 0x04, 0x3c,
0x9e, 0xec, 0xdc, 0xf1, 0x27, 0xd8, 0xc2, 0x15, 0x2a, 0x69, 0xb1, 0xb6, 0x11, 0xe6, 0xb6, 0x66, 0x5c, 0x1c, 0xa2,
0x37, 0x69, 0x32, 0x19, 0xc6, 0x0f, 0x86, 0xa5, 0x33, 0xd3, 0x0d, 0xa1, 0x09, 0x0e, 0x98, 0x7e, 0x86, 0x46, 0xe1,
0xa9, 0xdf, 0xae, 0xb6, 0xee, 0x90, 0x20, 0x9b, 0x1d, 0x77, 0x51, 0xc1, 0x6a, 0xf2, 0x7d, 0xae, 0x34, 0x2f, 0xd6,
0xce, 0x30, 0x83, 0x23, 0x4c, 0x16, 0x9c, 0xbd, 0x53, 0x64, 0x05, 0x10, 0x93, 0xce, 0x86, 0xc3, 0x35, 0xd4, 0x6a,
0x88, 0xd3, 0x5e, 0x4d, 0x97, 0xa0, 0x77, 0x75, 0xfd, 0x1b, 0xb7, 0xc9, 0xc5, 0x4d, 0xcd, 0x24, 0x8e, 0x0a, 0x67,
0xda, 0x60, 0x4c, 0xeb, 0xc8, 0x39, 0xc3, 0xbb, 0x1a, 0xb6, 0x8c, 0x32, 0xf4, 0x1c, 0x61, 0x76, 0xb3, 0x75, 0x17,
0xef, 0xa0, 0x5d, 0x11, 0xd5, 0x54, 0x3c, 0x1f, 0xf8, 0x3a, 0x16, 0xe2, 0xef, 0xc3, 0x13, 0xe0, 0x75, 0x13, 0xb3,
0xb7, 0x0b, 0xf5, 0x49, 0x71, 0xce, 0x02, 0xff, 0x27, 0x37, 0x92, 0x17, 0x45, 0x38, 0x2d, 0xf6, 0x91, 0x32, 0x63,
0xd2, 0x94, 0x46, 0x97, 0x5a, 0xb1, 0xd7, 0xbf, 0x66, 0x4c, 0x66, 0xe0, 0x03, 0x05, 0x23, 0x8c, 0xef, 0x9b, 0x80,
0xf0, 0x3c, 0xc1, 0x4e, 0x95, 0x1e, 0xb4, 0x2f, 0x64, 0x0c, 0x76, 0x47, 0x48, 0xdd, 0x69, 0x46, 0xfd, 0x59, 0x87,
0x3e, 0x7d, 0xdd, 0x60, 0x7d, 0x60, 0xdb, 0x11, 0x33, 0xa2, 0x1b, 0x32, 0x84, 0xc0, 0x75, 0xdd, 0x78, 0x2a, 0xd3,
0x4f, 0x15, 0x30, 0x05, 0x64, 0xc9, 0xb8, 0x76, 0xb1, 0x1a, 0x3b, 0xfe, 0xbe, 0x8e, 0x51, 0x29, 0xb2, 0xa6, 0x43,
0xc1, 0xc6, 0xe5, 0xa8, 0x37, 0x70, 0x09, 0xda, 0x68, 0x32, 0x06, 0x46, 0x69, 0x6c, 0x46, 0x2e, 0x61, 0x5d, 0x4b,
0x4b, 0xbc, 0x25, 0x2f, 0xb8, 0x79, 0xb2, 0xa4, 0x71, 0x97, 0xe4, 0x46, 0x83, 0x89, 0x73, 0xff, 0xbc, 0xea, 0xa8,
0x0a, 0xc4, 0x0c, 0x27, 0xe9, 0x28, 0x24, 0xe8, 0x76, 0x06, 0x65, 0x53, 0x61, 0x58, 0x93, 0xcb, 0xcb, 0x77, 0xbf,
0xa7, 0x06, 0xcc, 0xad, 0x1c, 0xf6, 0xa7, 0x5e, 0xcc, 0x10, 0x83, 0xd4, 0xe9, 0x49, 0xff, 0xa8, 0x6a, 0xb1, 0xbf,
0xa0, 0x07, 0xf9, 0x1d, 0x1d, 0x9f, 0x86, 0xcd, 0x5e, 0x4f, 0xf7, 0xd7, 0x95, 0x4a, 0x7a, 0x89, 0x80, 0x62, 0x6f,
0x58, 0xc4, 0x9e, 0x01, 0xdc, 0x9f, 0x97, 0x03, 0x1f, 0xc6, 0xe9, 0xe3, 0xd5, 0x4b, 0xf2, 0xb9, 0xc5, 0x26, 0x00,
0x7d, 0xd8, 0x3a, 0xaf, 0xf0, 0x65, 0x58, 0x36, 0x79, 0xf2, 0xe9, 0xe3, 0xe5, 0xd5, 0xde, 0xc3, 0x79, 0xc7, 0x44,
0x40, 0x64, 0xfd, 0xf3, 0x6e, 0x5e, 0x69, 0xde, 0x32, 0xa9, 0x3b, 0xb5, 0x8e, 0xe9, 0x22, 0x3b, 0x1f, 0xba, 0x73,
0x7c, 0x03, 0x41, 0xef, 0x46, 0x2f, 0x98, 0x92, 0x1d, 0xaa, 0x9d, 0xb5, 0x7b, 0xb8, 0xbc, 0xfe, 0xb6, 0xbd, 0xfe,
0xea, 0xbd, 0xee, 0xa5, 0xfb, 0x0f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x95, 0x57, 0x6d, 0x6f, 0xdb, 0x36, 0x10, 0xfe, 0xde,
0x5f, 0xc1, 0xb1, 0xdd, 0x2c, 0xad, 0x91, 0x64, 0x29, 0xb1, 0xdb, 0xda, 0x92, 0x8a, 0xae, 0xdb, 0xb0, 0x02, 0xeb,
0x5a, 0x20, 0xd9, 0xf6, 0xa1, 0x28, 0x66, 0x5a, 0x3a, 0x59, 0x6c, 0x24, 0x4a, 0x23, 0xe9, 0xb7, 0x1a, 0xde, 0x6f,
0xdf, 0x91, 0x92, 0x15, 0x27, 0x58, 0x87, 0x2d, 0x41, 0x6c, 0xbe, 0xdc, 0x3b, 0xef, 0x9e, 0xbb, 0xc4, 0x5f, 0xe5,
0x4d, 0xa6, 0xf7, 0x2d, 0x90, 0x52, 0xd7, 0x55, 0x1a, 0x9b, 0x4f, 0x52, 0x31, 0xb1, 0x4a, 0x40, 0xe0, 0x0e, 0x58,
0x9e, 0xc6, 0x35, 0x68, 0x46, 0xb2, 0x92, 0x49, 0x05, 0x3a, 0xf9, 0xf5, 0xe6, 0x47, 0xef, 0x79, 0x1a, 0x57, 0x5c,
0xdc, 0x12, 0x09, 0x55, 0xc2, 0xb3, 0x46, 0x90, 0x52, 0x42, 0x91, 0xe4, 0x4c, 0xb3, 0x19, 0xaf, 0xd9, 0x0a, 0x7a,
0x16, 0xc1, 0x6a, 0x48, 0x36, 0x1c, 0xb6, 0x6d, 0x23, 0x35, 0x41, 0x3a, 0x0d, 0x42, 0x27, 0x74, 0xcb, 0x73, 0x5d,
0x26, 0x39, 0x6c, 0x78, 0x06, 0x9e, 0xdd, 0x5c, 0x70, 0xc1, 0x35, 0x67, 0x95, 0xa7, 0x32, 0x56, 0x41, 0x12, 0x5e,
0xac, 0x15, 0x48, 0xbb, 0x61, 0x4b, 0xdc, 0x8b, 0x86, 0xa6, 0xb1, 0xca, 0x24, 0x6f, 0x35, 0x31, 0xa6, 0x26, 0x75,
0x93, 0xaf, 0x2b, 0x48, 0x83, 0x80, 0x29, 0x34, 0x49, 0x05, 0x5c, 0xe4, 0xb0, 0xf3, 0x27, 0xe1, 0x34, 0x7a, 0x3e,
0x99, 0xbe, 0xf0, 0x3f, 0xa9, 0x47, 0xe8, 0xd4, 0xba, 0x46, 0x6d, 0x7e, 0xd5, 0x64, 0x4c, 0xf3, 0x46, 0xf8, 0x0a,
0x98, 0xcc, 0xca, 0x24, 0x49, 0xe8, 0x4b, 0xc5, 0x36, 0x40, 0xbf, 0xf9, 0xc6, 0x19, 0x88, 0x56, 0xa0, 0x7f, 0xa8,
0xc0, 0x2c, 0xd5, 0x77, 0xfb, 0x1b, 0xb6, 0xfa, 0x05, 0x0d, 0x77, 0x28, 0x53, 0x3c, 0x07, 0xea, 0x7e, 0x18, 0x7f,
0xf4, 0x95, 0xde, 0x57, 0xe0, 0xe7, 0x5c, 0xb5, 0x15, 0xdb, 0x27, 0x74, 0x89, 0x52, 0x6f, 0xa9, 0x3b, 0x2f, 0xd6,
0x22, 0x33, 0xc2, 0x89, 0x74, 0xc0, 0x3d, 0x54, 0x80, 0xe6, 0x25, 0x6f, 0x99, 0x2e, 0xfd, 0x9a, 0xed, 0x9c, 0x6e,
0xc1, 0x85, 0x13, 0x7d, 0xeb, 0xc0, 0xd3, 0x70, 0x3c, 0x76, 0x2f, 0xec, 0xc7, 0xd8, 0x0d, 0xf0, 0x7b, 0x2e, 0x41,
0xaf, 0xa5, 0x20, 0x8d, 0xb3, 0x88, 0x5b, 0xa4, 0x24, 0x79, 0x42, 0xdf, 0x86, 0x11, 0x09, 0x5f, 0xf8, 0xd1, 0xe4,
0x67, 0xff, 0x19, 0xb9, 0xc2, 0xef, 0xec, 0x99, 0x37, 0x21, 0xe1, 0x15, 0x7e, 0x44, 0x91, 0x3f, 0x21, 0xe3, 0xcf,
0x94, 0x14, 0xbc, 0xaa, 0x12, 0x2a, 0x1a, 0x01, 0x94, 0x28, 0x2d, 0x9b, 0x5b, 0x48, 0x68, 0xb6, 0x96, 0x12, 0x6d,
0x7f, 0xdd, 0x54, 0x8d, 0xa4, 0x41, 0xfa, 0xe8, 0x7f, 0x09, 0xd4, 0x92, 0x09, 0x55, 0x34, 0xb2, 0x4e, 0xa8, 0x8d,
0xbe, 0xf3, 0xe4, 0xa0, 0x8f, 0xc4, 0x7c, 0xb8, 0x67, 0x97, 0x5e, 0x23, 0xf9, 0x8a, 0x8b, 0x84, 0x1a, 0x89, 0xcf,
0x51, 0xc9, 0xc2, 0x3d, 0x0e, 0xde, 0x37, 0xc6, 0xfb, 0xde, 0x1f, 0xe5, 0x7c, 0x58, 0xc4, 0x6a, 0xb3, 0x22, 0xbb,
0xba, 0x12, 0x2a, 0xa1, 0xa5, 0xd6, 0xed, 0x2c, 0x08, 0xb6, 0xdb, 0xad, 0xbf, 0xbd, 0xf4, 0x1b, 0xb9, 0x0a, 0xa2,
0xf1, 0x78, 0x1c, 0x20, 0x05, 0x25, 0x5d, 0x22, 0xd0, 0xe8, 0x8a, 0x92, 0x12, 0xf8, 0xaa, 0xd4, 0x76, 0x9d, 0x3e,
0x39, 0xc0, 0x31, 0x36, 0x14, 0xe9, 0xe2, 0xe3, 0x99, 0x16, 0x7e, 0xa6, 0x05, 0x5e, 0x36, 0x0e, 0x3d, 0xb9, 0x39,
0xb2, 0x6e, 0x3e, 0x63, 0x11, 0x89, 0xc8, 0xd8, 0xfe, 0x46, 0x9e, 0x59, 0xf7, 0x3b, 0xef, 0xc1, 0x8e, 0x9c, 0xed,
0xcc, 0xaa, 0x9e, 0x7a, 0x2f, 0x06, 0xde, 0xd0, 0x9c, 0x6c, 0xc2, 0xf1, 0xdd, 0x81, 0x61, 0xf8, 0x69, 0x7a, 0xbe,
0xf7, 0xa2, 0xdf, 0xce, 0x09, 0x8c, 0xb6, 0x32, 0xfc, 0x6d, 0xca, 0x26, 0x64, 0xd2, 0x9f, 0x4c, 0x3c, 0xb3, 0x1e,
0x76, 0x64, 0xb2, 0x41, 0x8a, 0xda, 0x9b, 0x7a, 0x13, 0x76, 0x49, 0x2e, 0x7b, 0x43, 0x70, 0x85, 0xc7, 0x53, 0x64,
0x3c, 0x3b, 0xf3, 0x2e, 0x3f, 0x8f, 0x82, 0x94, 0xba, 0x33, 0x4a, 0xef, 0x3c, 0x57, 0xe7, 0x9e, 0xfb, 0x9f, 0x1a,
0xcc, 0x29, 0x4a, 0x31, 0x32, 0xa0, 0xb3, 0xd2, 0xa1, 0x01, 0x16, 0x56, 0xc1, 0x57, 0x98, 0xf5, 0x8d, 0xa0, 0xae,
0xaf, 0x4b, 0x10, 0xce, 0x89, 0xd5, 0x30, 0x82, 0xbd, 0x71, 0x1e, 0xde, 0x68, 0xf7, 0x30, 0xe4, 0xbf, 0xe6, 0x1a,
0xcb, 0x4c, 0xfb, 0xa6, 0x60, 0x2f, 0x86, 0xd3, 0x65, 0x93, 0xef, 0xbf, 0x50, 0x1a, 0x65, 0xd8, 0xd5, 0x05, 0x17,
0x02, 0xe4, 0x0d, 0xec, 0xf0, 0xe5, 0xde, 0xbe, 0x7a, 0x4d, 0x5e, 0xe5, 0xb9, 0x04, 0xa5, 0x66, 0x84, 0x3e, 0xd5,
0x58, 0x03, 0xd9, 0x7f, 0x97, 0x15, 0xde, 0x93, 0xf5, 0x3b, 0xff, 0x91, 0x93, 0x5f, 0x40, 0x6f, 0x1b, 0x79, 0xdb,
0x4b, 0x33, 0xa6, 0xcd, 0x4d, 0x85, 0x31, 0xb4, 0x93, 0xb5, 0xca, 0x57, 0x15, 0xc2, 0x87, 0x13, 0xba, 0xa8, 0xa7,
0xbd, 0xf3, 0x4a, 0x9c, 0x02, 0xb5, 0x88, 0x73, 0xbe, 0x21, 0x59, 0x85, 0x08, 0x81, 0xe5, 0xd2, 0x89, 0xa2, 0xe4,
0x11, 0xb1, 0x3f, 0x8d, 0xc8, 0x90, 0xfb, 0x36, 0xa1, 0xff, 0x80, 0x00, 0xdf, 0xed, 0xdf, 0xe4, 0xce, 0x48, 0x61,
0xed, 0x8f, 0x5c, 0x7f, 0xc3, 0xaa, 0x35, 0x90, 0x84, 0xe8, 0x92, 0xab, 0x3b, 0x03, 0xe7, 0x5f, 0x64, 0x6b, 0xd5,
0x2d, 0x72, 0x15, 0x78, 0xad, 0x1c, 0x97, 0xa6, 0xbd, 0xba, 0x98, 0x75, 0x00, 0x49, 0x1f, 0xd3, 0x07, 0x16, 0x79,
0x15, 0x14, 0x7a, 0xa0, 0x23, 0x58, 0x76, 0xd2, 0x11, 0xbe, 0x44, 0xed, 0xee, 0x71, 0x38, 0x8c, 0x55, 0xcb, 0xc4,
0x43, 0x46, 0x63, 0xa0, 0x29, 0x15, 0x04, 0x36, 0x5c, 0x99, 0x7a, 0x41, 0xa2, 0x41, 0x61, 0xc0, 0x4e, 0xcb, 0x27,
0x07, 0x8e, 0x12, 0x0d, 0x5e, 0x0d, 0x12, 0xe3, 0x00, 0x43, 0x93, 0x2e, 0x8e, 0xee, 0x9c, 0x17, 0x77, 0x18, 0xf8,
0xe7, 0x1a, 0xe4, 0xfe, 0x1a, 0x2a, 0xc8, 0x74, 0x23, 0x1d, 0xfa, 0x18, 0x15, 0x61, 0x36, 0x59, 0x9f, 0x7f, 0xba,
0x79, 0xfb, 0x73, 0xa2, 0x1c, 0xe6, 0x5e, 0x7c, 0x89, 0xda, 0x74, 0x83, 0x0f, 0xd8, 0x0d, 0xfe, 0x4a, 0x46, 0xa6,
0x1f, 0x8c, 0x3e, 0x22, 0xab, 0x75, 0x79, 0x71, 0xd7, 0x14, 0x4c, 0x45, 0x3f, 0x45, 0x54, 0xb8, 0x30, 0x4e, 0x7a,
0xd3, 0x89, 0x7b, 0x5c, 0x5c, 0x68, 0x7f, 0x0b, 0xcb, 0x3f, 0x10, 0xec, 0x37, 0x20, 0x3b, 0x00, 0x15, 0xc9, 0xa0,
0x23, 0x93, 0xc0, 0x34, 0xf4, 0xf1, 0x75, 0x28, 0x1a, 0x8d, 0x90, 0x2b, 0xce, 0x4c, 0x5a, 0xc4, 0xe5, 0x65, 0xfa,
0xbd, 0xed, 0x23, 0xe4, 0x35, 0x76, 0x17, 0xd9, 0x54, 0x71, 0x80, 0x47, 0x43, 0xbc, 0x83, 0x97, 0x77, 0xe2, 0x93,
0x10, 0xe1, 0x8c, 0x49, 0x7c, 0xb1, 0x84, 0xfe, 0xb1, 0xc4, 0x0e, 0x87, 0xe9, 0x30, 0xe4, 0xc1, 0x16, 0x3b, 0x48,
0xb3, 0xf5, 0x9b, 0x16, 0x2b, 0x64, 0x74, 0x9f, 0x6b, 0x74, 0x31, 0xea, 0xc8, 0x47, 0xee, 0x09, 0xb3, 0x0b, 0x56,
0x29, 0x98, 0xd3, 0xf4, 0x1d, 0x92, 0x93, 0xdf, 0x61, 0x49, 0xae, 0x2d, 0xad, 0x89, 0x7a, 0xdc, 0x12, 0xdb, 0x2b,
0x12, 0x5a, 0xa0, 0x41, 0x9e, 0xe2, 0x9f, 0x61, 0x16, 0x46, 0xed, 0x6e, 0x9e, 0x19, 0x70, 0x9e, 0x3d, 0x9e, 0x4e,
0xa7, 0xc8, 0xf8, 0xa6, 0xb0, 0x19, 0x45, 0xf2, 0x06, 0x94, 0x18, 0x69, 0x62, 0xf4, 0x5e, 0x90, 0x9a, 0x89, 0x35,
0xab, 0xaa, 0x3d, 0x59, 0xca, 0x66, 0xab, 0x80, 0xe8, 0x06, 0x5f, 0xaf, 0xb7, 0x6c, 0x68, 0x63, 0x1d, 0x0e, 0x1f,
0xef, 0xdb, 0x18, 0x07, 0x2d, 0xf6, 0x6a, 0x99, 0xc6, 0x4b, 0x99, 0x2e, 0xfe, 0x7b, 0xd1, 0x45, 0x1f, 0xfd, 0x25,
0x20, 0xba, 0x03, 0x96, 0xcd, 0xf1, 0xe8, 0x62, 0x42, 0x60, 0x1a, 0xd9, 0x36, 0x8b, 0xed, 0xd6, 0x78, 0x91, 0x7e,
0x7b, 0x58, 0x36, 0x3b, 0xe3, 0x05, 0x17, 0xab, 0x19, 0x17, 0x25, 0x48, 0xae, 0x8f, 0xf8, 0x10, 0xd8, 0xb0, 0xdb,
0xb5, 0x3e, 0xb4, 0x2c, 0xcf, 0xcd, 0xcd, 0x04, 0x1d, 0x3c, 0xf3, 0x17, 0xea, 0x63, 0x77, 0x6f, 0x01, 0x7e, 0xf6,
0x62, 0xf2, 0xf5, 0xd1, 0x98, 0x72, 0xd0, 0x58, 0x3b, 0x1e, 0xab, 0xf8, 0x4a, 0xcc, 0x32, 0xb4, 0x07, 0x64, 0xc7,
0x54, 0xb0, 0x9a, 0x57, 0xfb, 0x99, 0xc2, 0x56, 0xe3, 0xa1, 0x47, 0xbc, 0x38, 0x2e, 0xd7, 0x5a, 0x37, 0x02, 0x75,
0xcb, 0x1c, 0xe4, 0x6c, 0x3c, 0xef, 0x16, 0x9e, 0x64, 0x39, 0x5f, 0xab, 0x99, 0x7f, 0x29, 0xa1, 0x9e, 0x2f, 0x59,
0x76, 0xbb, 0x92, 0xcd, 0x5a, 0xe4, 0x5e, 0x1f, 0xdb, 0xb0, 0x60, 0x97, 0x90, 0x9d, 0x22, 0x5d, 0x14, 0xc5, 0x1c,
0xd3, 0x12, 0xbc, 0xae, 0xb5, 0xcc, 0x22, 0xff, 0xca, 0xb0, 0x9d, 0x99, 0xe9, 0x47, 0xe6, 0xa0, 0xb3, 0x11, 0x3b,
0xf2, 0xd7, 0xf3, 0x93, 0x3b, 0xe3, 0x39, 0x76, 0x54, 0x85, 0x42, 0x5a, 0x84, 0x5a, 0x34, 0xf3, 0x58, 0x33, 0x2e,
0xce, 0xad, 0x37, 0x55, 0x3b, 0xef, 0xa7, 0x01, 0x0c, 0x8b, 0x55, 0x63, 0x67, 0x82, 0x39, 0xf6, 0xfb, 0x6e, 0xa4,
0x99, 0x45, 0xd3, 0x71, 0xbb, 0x3b, 0xfa, 0x7d, 0xbd, 0x1e, 0x4e, 0xd4, 0x45, 0x05, 0xbb, 0xf9, 0xa7, 0xb5, 0xd2,
0xbc, 0xd8, 0x7b, 0xfd, 0x48, 0x34, 0xc3, 0xda, 0xc5, 0x51, 0x68, 0x89, 0xa4, 0x00, 0x62, 0x6e, 0x75, 0x78, 0x5c,
0x43, 0xad, 0xfa, 0x38, 0x0d, 0x62, 0x2c, 0x5e, 0xdc, 0x97, 0xf5, 0x6f, 0xd4, 0x06, 0x1a, 0x0e, 0x35, 0x26, 0x3d,
0x5a, 0xb5, 0x6c, 0x30, 0xa6, 0xf5, 0xcc, 0x7b, 0x86, 0x6f, 0xd5, 0x1f, 0x19, 0x61, 0xe8, 0x39, 0x9a, 0x69, 0x47,
0x9d, 0x53, 0xbc, 0xc3, 0x76, 0x47, 0x54, 0x53, 0xf1, 0xbc, 0xa7, 0xb3, 0x24, 0x64, 0x3c, 0x84, 0x27, 0xc4, 0xe7,
0x26, 0xe6, 0xec, 0x14, 0xea, 0xab, 0xe2, 0x39, 0x0b, 0xc7, 0xff, 0xf0, 0x22, 0x79, 0x51, 0x44, 0xcb, 0x62, 0x88,
0x94, 0x99, 0x5a, 0x0c, 0x52, 0xd9, 0xd4, 0xc2, 0x4a, 0xb5, 0xc3, 0xa5, 0xc9, 0x0c, 0x9c, 0x17, 0x31, 0xc2, 0x98,
0xc2, 0x21, 0xe1, 0x79, 0x82, 0x8d, 0x23, 0x3d, 0xeb, 0x26, 0x48, 0x18, 0x9e, 0xae, 0x70, 0x75, 0xaf, 0x37, 0x74,
0x77, 0xd6, 0xfa, 0x14, 0x11, 0x40, 0x20, 0x1c, 0xa1, 0x85, 0xa6, 0x72, 0xfa, 0x10, 0xf8, 0xbe, 0x6f, 0x8a, 0xe2,
0x7d, 0x05, 0x0c, 0x2b, 0x6a, 0xcb, 0xb8, 0xf6, 0xb1, 0x4c, 0x2d, 0x7d, 0x07, 0xab, 0x28, 0x14, 0x49, 0xd3, 0x1e,
0x3f, 0x0d, 0xa0, 0x58, 0x05, 0xd7, 0xa0, 0x8d, 0x24, 0xd5, 0xe1, 0x89, 0x99, 0x80, 0x08, 0xb3, 0x1d, 0x26, 0x09,
0xb6, 0xbc, 0xe0, 0x66, 0x82, 0x4c, 0x63, 0x9b, 0xe4, 0x46, 0x82, 0x89, 0x73, 0x37, 0xed, 0xda, 0x55, 0x05, 0x62,
0x85, 0x83, 0xcd, 0x65, 0x44, 0xd0, 0xed, 0x0c, 0xca, 0xa6, 0xc2, 0xb0, 0x26, 0xd7, 0xd7, 0x6f, 0xbe, 0xb7, 0x15,
0x7a, 0xc7, 0x87, 0xed, 0xa2, 0x63, 0x33, 0x8b, 0x9e, 0x6b, 0x7a, 0xd5, 0xcd, 0xb8, 0x2d, 0xc2, 0x3d, 0x7a, 0x90,
0xdf, 0x93, 0xf1, 0xbe, 0x3f, 0xec, 0xe4, 0xd8, 0x3f, 0x5b, 0x2a, 0xe9, 0x35, 0x1a, 0x14, 0x07, 0xfd, 0x26, 0x0e,
0x8c, 0xc1, 0xdd, 0x7d, 0x8f, 0x0a, 0x18, 0xbf, 0xf4, 0xdd, 0xcd, 0x2b, 0xf2, 0x6b, 0x8b, 0x80, 0x0c, 0x5d, 0xd8,
0xac, 0x57, 0x38, 0xa8, 0x97, 0x4d, 0x9e, 0xbc, 0x7f, 0x77, 0x7d, 0x33, 0x78, 0xb8, 0xb6, 0x44, 0x04, 0x44, 0xd6,
0x4d, 0xdb, 0xeb, 0x4a, 0xf3, 0x96, 0x49, 0x6d, 0xc5, 0x7a, 0x06, 0xd1, 0x4f, 0x3e, 0xd8, 0x7b, 0x1c, 0x49, 0xa1,
0x73, 0xa3, 0x63, 0x4c, 0xc9, 0xc9, 0xaa, 0x93, 0xb6, 0x07, 0x76, 0x05, 0xdd, 0x6b, 0x07, 0xdd, 0xd3, 0x07, 0xf6,
0x1f, 0x8f, 0xbf, 0x01, 0x7d, 0x9a, 0x47, 0xb5, 0x88, 0x0c, 0x00, 0x00};
#else // Brotli (default, smaller)
const uint8_t INDEX_BR[] PROGMEM = {
0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b,
0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48,
0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78,
0xa5, 0x89, 0x25, 0xb3, 0xda, 0x2c, 0xa2, 0x32, 0x9c, 0x57, 0x07, 0x56, 0xbc, 0x34, 0x13, 0xff, 0x5c, 0x0a, 0xa1,
0x67, 0x82, 0xb8, 0x6b, 0x4c, 0x76, 0x31, 0x6c, 0xe3, 0x40, 0x46, 0xea, 0xb0, 0xd4, 0xf4, 0x3b, 0x02, 0x65, 0x18,
0xa4, 0xaf, 0xac, 0x6d, 0x55, 0xd6, 0xbe, 0x59, 0x66, 0x7a, 0x7c, 0x60, 0xb2, 0x83, 0x33, 0x23, 0xc9, 0x79, 0x82,
0x47, 0xb4, 0x28, 0xf4, 0x24, 0xb5, 0x23, 0x5a, 0x44, 0xe1, 0xc3, 0x27, 0x04, 0xe8, 0x0c, 0xdd, 0xb4, 0xd0, 0x8c,
0xfb, 0x10, 0x39, 0x93, 0x04, 0x2a, 0x66, 0x18, 0x4b, 0x74, 0xca, 0x31, 0x7f, 0xb2, 0xe5, 0x45, 0xc1, 0xdd, 0x72,
0x49, 0xff, 0x0e, 0xb3, 0xf0, 0x93, 0x18, 0xab, 0x68, 0xad, 0xe1, 0x9d, 0xe4, 0x29, 0xc0, 0xe3, 0x63, 0x54, 0x61,
0x1b, 0x45, 0xb9, 0x6c, 0x23, 0x0f, 0x99, 0x7f, 0x8e, 0x69, 0xaa, 0xc1, 0xb8, 0x4e, 0x42, 0x9c, 0xc5, 0x6e, 0x69,
0x40, 0x0e, 0x4f, 0x97, 0xd3, 0x23, 0x18, 0xf5, 0xc8, 0x75, 0x73, 0xb5, 0xbd, 0x46, 0x8a, 0x97, 0x7d, 0x83, 0xe4,
0x29, 0x72, 0x73, 0xc1, 0x39, 0x8e, 0x7e, 0x84, 0x39, 0x66, 0x57, 0xc6, 0x85, 0x19, 0x8b, 0xf2, 0x4d, 0xd9, 0xfe,
0x75, 0xa9, 0xe1, 0x2b, 0x21, 0x81, 0x58, 0x51, 0x99, 0xbc, 0xa4, 0x0b, 0x10, 0x6f, 0x86, 0x17, 0x0b, 0x92, 0x00,
0x11, 0x6f, 0x3b, 0xa4, 0xa4, 0x11, 0x7e, 0x0b, 0x97, 0x85, 0x23, 0x0c, 0x01, 0x6f, 0x2a, 0x18, 0xc6, 0xbe, 0x3d,
0x77, 0x1a, 0xe6, 0x00, 0x5c, 0x1a, 0x14, 0x47, 0xc6, 0xcc, 0xcc, 0x52, 0xbe, 0x04, 0x19, 0x31, 0x05, 0x46, 0xa0,
0xc3, 0x69, 0x0c, 0x60, 0xb7, 0x14, 0x57, 0xa0, 0x92, 0xbf, 0xb7, 0x0c, 0xd8, 0x3a, 0x79, 0x09, 0x99, 0xc9, 0x71,
0x88, 0x01, 0x8b, 0xa5, 0x61, 0x0a, 0xb5, 0xe8, 0xc7, 0x71, 0xe7, 0x70, 0x79, 0xb6, 0xe4, 0x01, 0xfc, 0x1a, 0x4a,
0x7b, 0x60, 0x6e, 0xef, 0x95, 0x62, 0x59, 0x28, 0xb5, 0x25, 0x56, 0x15, 0xe7, 0xca, 0xad, 0x32, 0xe6, 0xf7, 0x01,
0x31, 0x34, 0x87, 0x93, 0x0b, 0x9b, 0x9d, 0x26, 0xff, 0xe5, 0x92, 0xad, 0x6f, 0xb8, 0x3b, 0x16, 0xc1, 0xa0, 0x5a,
0x4f, 0x52, 0x0b, 0x2b, 0xc1, 0xa7, 0x95, 0x7b, 0x24, 0x51, 0xd3, 0xb3, 0x23, 0x62, 0x0b, 0xcc, 0xa0, 0x58, 0xa7,
0x64, 0x45, 0x2f, 0x0b, 0xdd, 0x1d, 0x97, 0x82, 0x1f, 0xcc, 0x64, 0xdb, 0xd3, 0xf4, 0xb0, 0x8b, 0xc8, 0xcf, 0x15,
0x81, 0x8b, 0xa1, 0x9d, 0xf8, 0xfc, 0xec, 0x49, 0x40, 0x12, 0x01, 0x09, 0x51, 0xf3, 0x73, 0x18, 0x24, 0x97, 0x55,
0x85, 0x6a, 0x92, 0x1a, 0xf5, 0x5a, 0x05, 0x54, 0x1f, 0x27, 0x0a, 0xa8, 0xa1, 0x94, 0x58, 0x78, 0x7d, 0x87, 0xa8,
0xdb, 0x13, 0x66, 0x20, 0x5e, 0x43, 0x18, 0x7a, 0xbb, 0x16, 0x16, 0x07, 0xc8, 0xab, 0x10, 0xe2, 0x50, 0xb9, 0xb1,
0xd8, 0x21, 0xc8, 0x4a, 0x2e, 0x99, 0x0e, 0x23, 0x52, 0xc6, 0xcb, 0x29, 0x84, 0x91, 0x03, 0xb1, 0xe2, 0x4c, 0x1d,
0x22, 0xd3, 0xc8, 0x79, 0x00, 0x8b, 0x8b, 0x88, 0x1e, 0x29, 0xd3, 0xae, 0x10, 0x15, 0x22, 0x6d, 0xb0, 0x87, 0x6f,
0x27, 0x2e, 0x7c, 0xc2, 0x7a, 0x61, 0xbd, 0x22, 0xe5, 0x5f, 0xdd, 0x7b, 0x00, 0x04, 0xf2, 0x7d, 0x5a, 0x03, 0x38,
0x1f, 0x69, 0x6d, 0x0b, 0xfb, 0xec, 0x45, 0xfe, 0x8b, 0x7f, 0xec, 0x7b, 0xad, 0xc2, 0x33, 0xf1, 0x9e, 0x9c, 0x71,
0xd9, 0xe8, 0x5e, 0x8f, 0xd4, 0xee, 0x87, 0x45, 0x6c, 0xe2, 0x12, 0xf8, 0xb8, 0xc5, 0xee, 0x43, 0xa6, 0x37, 0x91,
0xb5, 0x2c, 0x2f, 0xe9, 0xe8, 0x24, 0xd0, 0x45, 0xc1, 0x0c, 0x7c, 0xf0, 0xb2, 0xb5, 0x2d, 0x10, 0x36, 0x7e, 0x18,
0x7c, 0x79, 0x82, 0x69, 0x3d, 0x35, 0xca, 0x52, 0xee, 0xc9, 0xb5, 0x65, 0xa4, 0xa1, 0xfd, 0x70, 0x7e, 0xe0, 0x7d,
0x67, 0xf9, 0xa1, 0x71, 0xd2, 0x08, 0x74, 0x33, 0x5f, 0x69, 0xa4, 0x59, 0x03, 0xfd, 0xf8, 0xf0, 0x70, 0x1a, 0x50,
0x43, 0xfb, 0x61, 0xf0, 0x38, 0x18, 0x88, 0x85, 0x36, 0x23, 0x06, 0x4f, 0x02, 0xbb, 0x78, 0x1a, 0xaa, 0xd2, 0x02,
0x5e, 0xa0, 0x74, 0x30, 0xc8, 0x7a, 0x66, 0xab, 0xd9, 0x43, 0x99, 0x45, 0xb7, 0x0c, 0x5c, 0xec, 0xc8, 0x03, 0x0e,
0x0b, 0xca, 0x4a, 0x22, 0x48, 0xfb, 0xb7, 0x3d, 0x82, 0x07, 0x8d, 0x1b, 0x21, 0x87, 0x4d, 0x57, 0xa4, 0x5b, 0xd4,
0xe3, 0x88, 0x02, 0xc4, 0x81, 0xf9, 0x47, 0xe4, 0xbf, 0x3e, 0x39, 0xbb, 0x4f, 0x7e, 0x91, 0x63, 0x98, 0x97, 0xe4,
0x52, 0x01, 0x58, 0xba, 0x32, 0xbf, 0xae, 0xff, 0x45, 0xa1, 0xbc, 0x9b, 0xa4, 0x09, 0x0e, 0x79, 0xc0, 0x41, 0x86,
0x52, 0x88, 0x55, 0x39, 0x9d, 0xb6, 0xed, 0x35, 0x68, 0x29, 0xfa, 0xe6, 0x6c, 0x3d, 0x0a, 0xcd, 0x6a, 0x28, 0xfd,
0x65, 0x24, 0xce, 0x38, 0x98, 0x01, 0xd9, 0x3f, 0x1b, 0x4c, 0xc4, 0x5c, 0x1d, 0xaa, 0x21, 0x78, 0x67, 0xaf, 0x55,
0x72, 0x34, 0xf8, 0x1b, 0x03, 0x21, 0x27, 0x08, 0xbd, 0x59, 0x60, 0x48, 0x0d, 0xe2, 0x56, 0x9b, 0x30, 0x92, 0x8f,
0x67, 0x8a, 0x7f, 0x20, 0xbd, 0x2d, 0xfd, 0xc5, 0xb0, 0xa6, 0xaa, 0x77, 0x75, 0x26, 0x33, 0x2f, 0x20, 0x2a, 0xab,
0x5c, 0xd1, 0x3b, 0xda, 0xb2, 0x4c, 0xa4, 0x86, 0x25, 0x8d, 0x49, 0x05, 0xaf, 0x7a, 0xa8, 0xd4, 0x9c, 0x0d, 0xd3,
0x38, 0xa6, 0x5c, 0x29, 0x6b, 0x16, 0x27, 0x07, 0xf1, 0xbe, 0xe2, 0x24, 0xc1, 0x8d, 0x25, 0x76, 0xbc, 0xf6, 0x0d,
0xc2, 0x94, 0x25, 0xb8, 0xf3, 0x07, 0x9a, 0x49, 0xf4, 0x89, 0x82, 0x4d, 0x51, 0xb1, 0x96, 0x61, 0x62, 0x8d, 0xc8,
0x61, 0x65, 0x0d, 0x14, 0x34, 0x02, 0x65, 0x94, 0xcc, 0x1d, 0x85, 0x00, 0x0f, 0x1a, 0x57, 0x68, 0x15, 0xcf, 0xa4,
0xa2, 0x7d, 0x6d, 0x53, 0x60, 0xce, 0x5c, 0x61, 0x82, 0x17, 0x32, 0xc1, 0x87, 0x02, 0x0c, 0x91, 0x85, 0x57, 0x51,
0xbe, 0xb2, 0x38, 0x9f, 0x3d, 0x2a, 0x52, 0x5a, 0xad, 0xba, 0x46, 0x9e, 0x3c, 0x8a, 0xa0, 0x46, 0x15, 0xf4, 0x59,
0x74, 0x5f, 0x2a, 0xae, 0x96, 0x56, 0xf0, 0x54, 0x39, 0xaf, 0xac, 0x2a, 0xb9, 0xad, 0x32, 0x50, 0xc9, 0xc1, 0xee,
0xd2, 0x0d, 0x34, 0xaa, 0x98, 0x4d, 0x6d, 0x3d, 0xc6, 0xb9, 0x5b, 0x00, 0x5f, 0xea, 0xda, 0x16, 0xa6, 0x08, 0x43,
0x58, 0x4d, 0x8d, 0x07, 0x55, 0x62, 0x81, 0x44, 0xcc, 0x31, 0x04, 0x4b, 0x4c, 0x8b, 0x3e, 0xff, 0xd8, 0xf6, 0x65,
0x19, 0xa1, 0x94, 0x62, 0x65, 0x0a, 0xdd, 0x60, 0x38, 0xd3, 0xbe, 0x0d, 0xa3, 0x99, 0xd5, 0x37, 0x68, 0xa1, 0x71,
0xa3, 0x41, 0xe7, 0xbe, 0x9d, 0x72, 0x84, 0x75, 0xb6, 0x8d, 0x98, 0xd6, 0xb8, 0x2d, 0x43, 0x85, 0x5d, 0xf9, 0xca,
0xc3, 0x96, 0xa5, 0xa6, 0xe7, 0x50, 0x88, 0x6b, 0x84, 0x58, 0x44, 0x45, 0x20, 0xdf, 0x1e, 0x5a, 0xc9, 0xce, 0x42,
0x2a, 0x1f, 0x3e, 0x3c, 0x7b, 0x68, 0x3c, 0x34, 0x8b, 0x36, 0xba, 0x1f, 0xce, 0x0f, 0xa0, 0x60, 0x37, 0x5f, 0x1a,
0x03, 0x2b, 0x86, 0x29, 0x45, 0x7b, 0xb4, 0xb7, 0x06, 0x68, 0x17, 0x7e, 0x13, 0x76, 0x91, 0x4d, 0x27, 0xee, 0xbc,
0x7e, 0x80, 0xc2, 0x66, 0xac, 0xc6, 0xbf, 0xeb, 0x7f, 0xd7, 0x84, 0x79, 0xf3, 0xf1, 0xde, 0xec, 0xa6, 0x93, 0xa8,
0x13, 0x3b, 0x4a, 0x81, 0xfa, 0x11, 0x1e, 0x4a, 0xd2, 0x50, 0x2a, 0xea, 0x9a, 0xc2, 0x37, 0x08, 0xed, 0x01, 0xf5,
0xa2, 0xd5, 0x32, 0x29, 0x49, 0xc4, 0x1a, 0x11, 0xc0, 0xda, 0x24, 0x28, 0x84, 0x38, 0x60, 0x80, 0xcf, 0xd0, 0x45,
0x83, 0xa7, 0xca, 0x52, 0x5c, 0xac, 0x23, 0x01};
0x1b, 0x87, 0x0c, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0x48, 0x83, 0x8d, 0xa0, 0x60, 0x49, 0x51, 0xb8,
0x52, 0x9b, 0x44, 0x71, 0xc3, 0xe5, 0xc4, 0x8c, 0xb8, 0xd5, 0xe1, 0xfc, 0x6a, 0x2d, 0xdf, 0x92, 0x04, 0x76, 0x51,
0xb1, 0x51, 0xa9, 0x08, 0x05, 0xd6, 0xe5, 0x75, 0xf7, 0x74, 0x08, 0x41, 0xa5, 0x6a, 0xe6, 0xf6, 0xc4, 0xde, 0x86,
0x59, 0x38, 0x40, 0x45, 0xf0, 0xad, 0x23, 0xf9, 0xfd, 0x21, 0x5b, 0x7f, 0x3f, 0x73, 0xc1, 0x4a, 0x60, 0x0f, 0x75,
0xea, 0x45, 0x28, 0x87, 0x33, 0x98, 0xa0, 0x4d, 0x5b, 0xc9, 0x2e, 0x9e, 0x4b, 0x1c, 0x78, 0xa7, 0x0e, 0x7e, 0x39,
0x76, 0x04, 0x75, 0x08, 0xb1, 0xb7, 0x5b, 0xa4, 0x5a, 0xb2, 0x2b, 0x4d, 0x84, 0x1c, 0xf8, 0x60, 0x78, 0xc7, 0x2a,
0x1c, 0x73, 0x9e, 0x53, 0x90, 0xb2, 0xa0, 0xf6, 0x3c, 0xb5, 0x9b, 0x05, 0x0b, 0xea, 0x47, 0x5f, 0xa8, 0xd0, 0xb8,
0xb0, 0x1d, 0x94, 0x63, 0x43, 0x68, 0xc1, 0x1c, 0x57, 0xaa, 0x55, 0x86, 0xe1, 0x44, 0x17, 0x89, 0xf9, 0x27, 0x29,
0xcb, 0xf8, 0x26, 0xcd, 0xb6, 0xf4, 0xaf, 0x9b, 0xd4, 0x9f, 0x5b, 0x6c, 0x58, 0x08, 0x01, 0xef, 0x4c, 0x9e, 0x7e,
0xb8, 0x58, 0xa0, 0x88, 0x61, 0xa8, 0xf2, 0x62, 0xd5, 0x78, 0x24, 0xfc, 0x21, 0x8e, 0xa9, 0x86, 0xda, 0xba, 0x03,
0xe2, 0xbc, 0x75, 0x07, 0x7b, 0xa4, 0xf1, 0xf8, 0x7a, 0x71, 0x00, 0x83, 0xd8, 0xb9, 0x6e, 0x6f, 0xab, 0xae, 0x91,
0xe3, 0x7d, 0x63, 0x11, 0x3f, 0x43, 0x7a, 0xc5, 0xa2, 0x08, 0x07, 0xff, 0x34, 0x45, 0x38, 0x7c, 0x33, 0x52, 0xd8,
0xdd, 0xa1, 0xb2, 0xd0, 0xed, 0xbf, 0xae, 0x05, 0x7c, 0xc5, 0x38, 0x60, 0x5b, 0xca, 0xd3, 0x97, 0x74, 0x0d, 0x92,
0xd3, 0xd4, 0x6a, 0x4d, 0x52, 0xc0, 0x92, 0xbb, 0x64, 0xce, 0x69, 0x73, 0x74, 0x0b, 0x2f, 0x0b, 0x47, 0x6c, 0x01,
0x6f, 0x31, 0x70, 0x43, 0xdf, 0x1d, 0xb5, 0x80, 0x1c, 0x40, 0x97, 0x06, 0xc5, 0xa9, 0x85, 0x72, 0x92, 0xa8, 0x5e,
0x82, 0x8c, 0x34, 0x05, 0xa6, 0xa1, 0xd9, 0x38, 0x78, 0x70, 0xef, 0xc7, 0x11, 0x4a, 0xc9, 0xcd, 0x5b, 0x06, 0xa9,
0x2e, 0xdc, 0xc0, 0x50, 0x96, 0xd8, 0xb7, 0x40, 0x4c, 0x27, 0x75, 0x7a, 0xd4, 0xa1, 0x1f, 0x65, 0xe1, 0x2e, 0xcf,
0x2f, 0x79, 0x80, 0x3a, 0x01, 0x7b, 0x7b, 0x08, 0x77, 0xcd, 0x55, 0x2c, 0xb2, 0xd0, 0xda, 0x45, 0x61, 0x2c, 0x95,
0xca, 0xf5, 0x3c, 0x91, 0xf7, 0x1c, 0x09, 0x94, 0xb3, 0x51, 0xfb, 0x62, 0xbf, 0x2c, 0x7f, 0xe5, 0x94, 0xdb, 0x8c,
0xeb, 0x3b, 0x1d, 0x82, 0x55, 0x8d, 0x5d, 0x76, 0xae, 0x15, 0x07, 0xbf, 0x63, 0xa3, 0x6a, 0x4e, 0xdc, 0xf2, 0x92,
0x1f, 0xc9, 0x0d, 0xa2, 0xc1, 0x40, 0x9b, 0x91, 0x2d, 0x7d, 0x31, 0xf3, 0xdd, 0x77, 0xc9, 0xa2, 0xc7, 0x68, 0xb2,
0xe1, 0x69, 0xf6, 0x78, 0x80, 0xf0, 0xcf, 0x91, 0x90, 0x63, 0xe3, 0x81, 0x7d, 0xfe, 0x4a, 0x4f, 0x41, 0xda, 0x0c,
0x52, 0xe2, 0xd6, 0x97, 0x26, 0x90, 0x5e, 0xc6, 0x6a, 0x6c, 0x49, 0x16, 0x94, 0xa1, 0x62, 0x10, 0x67, 0x46, 0x24,
0x06, 0x71, 0x61, 0x46, 0xec, 0x7c, 0xff, 0x0e, 0x89, 0x6f, 0x97, 0xa1, 0x84, 0x78, 0x07, 0x61, 0xed, 0xf3, 0x38,
0x5c, 0x1c, 0x20, 0x1f, 0x42, 0x88, 0x7d, 0xa3, 0x87, 0xea, 0x80, 0x60, 0x78, 0xe4, 0x92, 0xd8, 0x46, 0x64, 0x31,
0xbe, 0x9c, 0x42, 0x0c, 0x39, 0x88, 0x4b, 0x74, 0x68, 0xbe, 0x28, 0x6a, 0x50, 0x19, 0x20, 0x75, 0xd1, 0x2c, 0x06,
0x2a, 0xb4, 0x01, 0x81, 0x12, 0x09, 0x43, 0x3c, 0x6a, 0x0f, 0xec, 0x55, 0x0f, 0xac, 0x17, 0xd7, 0x2b, 0x52, 0xdb,
0xab, 0x7b, 0x0f, 0xc6, 0xe3, 0x3d, 0xa5, 0x1d, 0x80, 0xab, 0x81, 0xb6, 0xaa, 0x4a, 0x2f, 0x2f, 0xeb, 0xfb, 0x62,
0x1e, 0x9b, 0xa1, 0x55, 0xf4, 0xc2, 0xc5, 0x2b, 0xce, 0xa5, 0x6c, 0xa6, 0x12, 0x03, 0x55, 0x27, 0x77, 0x11, 0x9b,
0x5c, 0xc2, 0x30, 0x6d, 0xd5, 0xa9, 0x66, 0xe5, 0xb6, 0xb1, 0x22, 0x4a, 0x4d, 0x27, 0x87, 0x40, 0xd7, 0x02, 0x26,
0xe0, 0x97, 0xea, 0xd6, 0xb0, 0x40, 0xd8, 0x5c, 0xe7, 0x4c, 0xbd, 0xc4, 0xd4, 0xa6, 0xa6, 0xbe, 0x54, 0x0b, 0xb9,
0xbc, 0x9c, 0xb4, 0xf1, 0xe4, 0x8e, 0x8f, 0x3a, 0x3a, 0xab, 0x16, 0xca, 0x2c, 0xbd, 0x30, 0x87, 0x94, 0xaa, 0x8c,
0xcc, 0x6b, 0x90, 0x5f, 0xdf, 0xce, 0xc6, 0x1e, 0x59, 0xda, 0x3b, 0x67, 0xb0, 0x0f, 0x70, 0x95, 0xb6, 0x21, 0x46,
0x21, 0x21, 0x2e, 0xd5, 0x96, 0xba, 0xca, 0x3c, 0x5e, 0x10, 0x3a, 0x6c, 0xb2, 0xde, 0xa4, 0x1a, 0x6d, 0xa0, 0xb3,
0xe4, 0xce, 0x80, 0x8b, 0x9a, 0xd2, 0x23, 0x01, 0xad, 0x0e, 0x75, 0x97, 0xdc, 0x97, 0x66, 0xef, 0x7a, 0x04, 0xa7,
0x56, 0x0f, 0x50, 0x91, 0xd3, 0x2d, 0x09, 0x87, 0x14, 0x0e, 0x28, 0xd0, 0x23, 0x44, 0xff, 0x68, 0xfe, 0x2f, 0x96,
0x2f, 0x55, 0xcb, 0x5f, 0xe4, 0x54, 0x6d, 0x5f, 0xd9, 0x31, 0x40, 0xaa, 0x87, 0x57, 0xd7, 0xed, 0xbf, 0xa0, 0xa2,
0x1f, 0x93, 0x2c, 0xc5, 0xbe, 0x0c, 0x06, 0x7a, 0xa5, 0xf7, 0x7f, 0x7a, 0x2e, 0x56, 0xa2, 0xcb, 0xb2, 0xc0, 0xe7,
0xf2, 0xda, 0x5c, 0x42, 0x9c, 0xdb, 0x62, 0x15, 0x97, 0xd0, 0x87, 0xd7, 0x9d, 0x93, 0xee, 0x99, 0x20, 0x81, 0xd5,
0x6b, 0xfa, 0xee, 0x64, 0xb1, 0x85, 0x60, 0x1d, 0x9e, 0xf2, 0x82, 0x73, 0x6c, 0x5c, 0x21, 0x28, 0xdb, 0x96, 0xc1,
0x12, 0x43, 0x6e, 0x86, 0xf3, 0xfe, 0x14, 0xf3, 0xd2, 0x0b, 0xbf, 0xc4, 0x8f, 0x03, 0x1f, 0xd0, 0xf2, 0x5b, 0x15,
0xa7, 0x8e, 0x18, 0x08, 0x43, 0x80, 0x61, 0x53, 0x96, 0x65, 0xc4, 0x35, 0xcf, 0x1a, 0xae, 0x92, 0x05, 0x4b, 0x18,
0x94, 0xcf, 0xd7, 0x63, 0x38, 0x1f, 0x6c, 0xd0, 0x66, 0xb3, 0x71, 0x49, 0x1d, 0xeb, 0xe2, 0x00, 0x5f, 0x4e, 0xc0,
0xc4, 0xee, 0xe2, 0xd0, 0x4b, 0x94, 0xb6, 0x4a, 0xa5, 0xe0, 0x45, 0x71, 0xc6, 0x28, 0x57, 0x21, 0x8b, 0xbd, 0xc7,
0x1e, 0x68, 0x50, 0x4d, 0xb2, 0xc8, 0xb3, 0x71, 0xdf, 0x5d, 0x0f, 0x18, 0x7b, 0xbe, 0xdb, 0xa3, 0xe9, 0x88, 0xaf,
0xcd, 0x65, 0x80, 0x2e, 0x14, 0x5b, 0x02, 0x8a, 0xd9, 0x75, 0x6e, 0xd5, 0xad, 0x8f, 0xc3, 0xf0, 0xf3, 0xb0, 0x6d,
0x3a, 0xdf, 0x1a, 0x91, 0x07, 0xe6, 0x70, 0x30, 0x88, 0x85, 0x4d, 0x4b, 0x3d, 0xfa, 0x29, 0x1b, 0x21, 0x6b, 0x6d,
0x94, 0x3b, 0xfe, 0x03, 0xe9, 0x55, 0x6d, 0x2e, 0xdc, 0x8e, 0xc6, 0xbd, 0x6e, 0x73, 0x08, 0x73, 0x0d, 0xd6, 0xa8,
0x58, 0x57, 0xa3, 0x03, 0x23, 0x72, 0x84, 0xd3, 0xcc, 0x69, 0x42, 0x6a, 0x17, 0xb5, 0x9a, 0x72, 0x4f, 0x1b, 0x35,
0x2b, 0xc4, 0x0f, 0xca, 0x67, 0x8d, 0x16, 0x7b, 0xb4, 0x15, 0x38, 0x02, 0x2a, 0x6f, 0x00, 0xd5, 0xd6, 0x1a, 0x0b,
0xf8, 0x8b, 0x39, 0xd6, 0xae, 0xf3, 0x54, 0x66, 0xcb, 0x47, 0x0a, 0x73, 0x80, 0xcb, 0x65, 0xb9, 0x89, 0x52, 0x46,
0xae, 0x86, 0x21, 0xc2, 0x6d, 0x65, 0x04, 0x59, 0x96, 0xe4, 0xf9, 0x94, 0x10, 0x1c, 0x28, 0x6d, 0x45, 0x49, 0xa9,
0xce, 0x70, 0xd3, 0x0b, 0x9f, 0x02, 0xea, 0x21, 0x60, 0x44, 0x7b, 0xc5, 0x04, 0xae, 0x3b, 0xa8, 0xb0, 0x98, 0x15,
0x95, 0xe0, 0x8e, 0x69, 0x73, 0x7b, 0xcf, 0xb0, 0x12, 0xb7, 0x74, 0x67, 0x08, 0x68, 0x13, 0x61, 0xbe, 0x01, 0xfc,
0x4d, 0x73, 0xdf, 0x28, 0x69, 0x36, 0x8a, 0x45, 0x53, 0xe5, 0xaa, 0xba, 0x69, 0xf8, 0xbe, 0xc9, 0x31, 0xbe, 0xda,
0x1e, 0xfe, 0x02, 0xa9, 0x0e, 0xcb, 0xa2, 0x55, 0x06, 0x72, 0x3e, 0xce, 0x80, 0xa9, 0x45, 0xab, 0x2a, 0xd9, 0xa3,
0x90, 0x95, 0x23, 0xc1, 0x05, 0x2e, 0xb7, 0x7f, 0xe0, 0x70, 0xaa, 0x81, 0x4f, 0x8e, 0xbb, 0x80, 0xf4, 0x48, 0x33,
0x34, 0x53, 0x4a, 0x05, 0x43, 0x90, 0x34, 0x60, 0x7f, 0x2b, 0x2d, 0x42, 0x1a, 0x6b, 0x70, 0x6e, 0xbf, 0xd5, 0x83,
0x34, 0x96, 0x6a, 0x0f, 0x71, 0x26, 0x66, 0x2d, 0x7a, 0xd2, 0x29, 0xde, 0x0b, 0x7d, 0x86, 0xc3, 0x2b, 0xd3, 0x18,
0x14, 0xe9, 0x97, 0x63, 0x75, 0xd1, 0xd6, 0x84, 0x78, 0xd9, 0xc5, 0x08, 0xfc, 0x10, 0x11, 0xf3, 0xb5, 0x74, 0xba,
0x3f, 0x7c, 0x78, 0xf6, 0x50, 0x4a, 0x6d, 0xda, 0xd6, 0x93, 0x3b, 0x3e, 0xc2, 0x21, 0xef, 0xb5, 0x59, 0x82, 0x73,
0x0d, 0xc4, 0x77, 0xd3, 0x31, 0xeb, 0xfd, 0x23, 0x2d, 0xfe, 0xcb, 0xe2, 0x52, 0x2c, 0x47, 0x1c, 0xa8, 0x79, 0x26,
0x56, 0x0c, 0xc5, 0x11, 0xcd, 0x00, 0x73, 0xb2, 0xf5, 0xcd, 0xc7, 0x7b, 0xe7, 0xfe, 0x9d, 0x9b, 0xce, 0x5d, 0x47,
0x08, 0xe6, 0xa1, 0xd6, 0x92, 0xe7, 0x11, 0xb6, 0x68, 0x5b, 0x95, 0x86, 0x2c, 0x2d, 0x18, 0x21, 0x94, 0x74, 0x31,
0xd1, 0xea, 0xae, 0x5d, 0x08, 0xd3, 0x87, 0xab, 0xc2, 0xc4, 0xf5, 0x12, 0x94, 0x8c, 0x72, 0xf4, 0xf8, 0x2a, 0x0d,
0xa7, 0xb8, 0x6b, 0x20, 0x02};
// Backwards compatibility alias
#define INDEX_GZ INDEX_BR

View File

@@ -43,7 +43,11 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
scan.get_with_auth());
#endif
}
#ifdef USE_WEBSERVER
stream->print(ESPHOME_F("],\"web_server\":true}"));
#else
stream->print(ESPHOME_F("]}"));
#endif
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
@@ -76,15 +80,9 @@ void CaptivePortal::start() {
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
#if defined(USE_ESP32)
// Create DNS server instance for ESP-IDF
// Create DNS server instance with domain allowlisting support
this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->start(ip);
#elif defined(USE_ARDUINO)
this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, ESPHOME_F("*"), ip);
#endif
this->initialized_ = true;
this->active_ = true;
@@ -127,6 +125,10 @@ float CaptivePortal::get_setup_priority() const {
}
void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); }
bool CaptivePortal::canHandle(AsyncWebServerRequest *request) const {
return this->active_ && request->method() == HTTP_GET;
}
CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace captive_portal

View File

@@ -4,8 +4,8 @@
#include <memory>
#if defined(USE_ESP32)
#include "dns_server_esp32_idf.h"
#elif defined(USE_ARDUINO)
#include <DNSServer.h>
#elif defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
#include "dns_server_arduino.h"
#endif
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
@@ -22,15 +22,9 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void setup() override;
void dump_config() override;
void loop() override {
#if defined(USE_ESP32)
if (this->dns_server_ != nullptr) {
this->dns_server_->process_next_request();
}
#elif defined(USE_ARDUINO)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
}
#endif
}
float get_setup_priority() const override;
void start();
@@ -45,12 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
}
}
bool canHandle(AsyncWebServerRequest *request) const override {
// Handle all GET requests when captive portal is active
// This allows us to respond with the portal page for any URL,
// triggering OS captive portal detection
return this->active_ && request->method() == HTTP_GET;
}
bool canHandle(AsyncWebServerRequest *request) const override;
void handle_config(AsyncWebServerRequest *request);

View File

@@ -0,0 +1,130 @@
#include "dns_server_arduino.h"
#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
#include "dns_server_common.h"
#include "esphome/core/log.h"
#include <lwip/def.h>
namespace esphome::captive_portal {
static const char *const TAG = "captive_portal.dns";
void DNSServer::start(const network::IPAddress &ip) {
this->server_ip_ = ip;
ESP_LOGV(TAG, "Starting DNS server");
this->udp_ = make_unique<WiFiUDP>();
if (!this->udp_->begin(DNS_PORT)) {
ESP_LOGE(TAG, "Failed to start UDP on port %d", DNS_PORT);
this->udp_ = nullptr;
return;
}
ESP_LOGV(TAG, "Bound to port %d", DNS_PORT);
}
void DNSServer::stop() {
if (this->udp_ != nullptr) {
this->udp_->stop();
this->udp_ = nullptr;
}
ESP_LOGV(TAG, "Stopped");
}
void DNSServer::process_next_request() {
if (this->udp_ == nullptr) {
return;
}
int packet_size = this->udp_->parsePacket();
if (packet_size == 0) {
return;
}
if (packet_size > static_cast<int>(sizeof(this->buffer_))) {
// Packet too large, skip it
while (this->udp_->available()) {
this->udp_->read();
}
return;
}
int len = this->udp_->read(this->buffer_, sizeof(this->buffer_));
if (len < static_cast<int>(sizeof(DNSHeader) + 1)) {
ESP_LOGV(TAG, "Request too short: %d", len);
return;
}
// Parse DNS header
DNSHeader *header = (DNSHeader *) this->buffer_;
uint16_t flags = ntohs(header->flags);
uint16_t qd_count = ntohs(header->qd_count);
// Check if it's a standard query
if ((flags & DNS_QR_FLAG) || (flags & DNS_OPCODE_MASK) || qd_count != 1) {
ESP_LOGV(TAG, "Not a standard query: flags=0x%04X, qd_count=%d", flags, qd_count);
return;
}
// Parse domain name
uint8_t *ptr = this->buffer_ + sizeof(DNSHeader);
uint8_t *end = this->buffer_ + len;
char domain[128];
ptr = parse_dns_domain(ptr, end, domain, sizeof(domain));
if (ptr == nullptr) {
return; // Invalid domain name
}
// Check allowlist and send REFUSED if needed
if (is_allowlisted_domain(domain)) {
ESP_LOGV(TAG, "Allowlisted domain, sending REFUSED: %s", domain);
build_dns_refused_header(header);
this->udp_->beginPacket(this->udp_->remoteIP(), this->udp_->remotePort());
this->udp_->write(this->buffer_, len);
this->udp_->endPacket();
return;
}
ESP_LOGV(TAG, "Redirecting DNS query for: %s", domain);
// Check we have room for the question
if (ptr + sizeof(DNSQuestion) > end) {
return;
}
// Parse DNS question
DNSQuestion *question = (DNSQuestion *) ptr;
uint16_t qtype = ntohs(question->type);
uint16_t qclass = ntohs(question->dns_class);
if (qtype != DNS_QTYPE_A || qclass != DNS_QCLASS_IN) {
ESP_LOGV(TAG, "Not an A query: type=0x%04X, class=0x%04X", qtype, qclass);
return;
}
// Build DNS response
build_dns_response_header(header);
// Add answer section after the question
size_t question_end_offset = (ptr + sizeof(DNSQuestion)) - this->buffer_;
if (build_dns_answer(this->buffer_, sizeof(this->buffer_), question_end_offset,
static_cast<uint32_t>(this->server_ip_)) == nullptr) {
ESP_LOGW(TAG, "Response too large");
return;
}
size_t response_len = question_end_offset + sizeof(DNSAnswer);
// Send response
this->udp_->beginPacket(this->udp_->remoteIP(), this->udp_->remotePort());
this->udp_->write(this->buffer_, response_len);
if (!this->udp_->endPacket()) {
ESP_LOGV(TAG, "Send failed");
} else {
ESP_LOGV(TAG, "Sent %d bytes", response_len);
}
}
} // namespace esphome::captive_portal
#endif // USE_ESP8266 || USE_RP2040 || USE_LIBRETINY

View File

@@ -0,0 +1,26 @@
#pragma once
#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
#include <memory>
#include "dns_server_common.h"
#include "esphome/core/helpers.h"
#include "esphome/components/network/ip_address.h"
#include <WiFiUdp.h>
namespace esphome::captive_portal {
class DNSServer {
public:
void start(const network::IPAddress &ip);
void stop();
void process_next_request();
protected:
std::unique_ptr<WiFiUDP> udp_{nullptr};
network::IPAddress server_ip_;
uint8_t buffer_[DNS_BUFFER_SIZE];
};
} // namespace esphome::captive_portal
#endif // USE_ESP8266 || USE_RP2040 || USE_LIBRETINY

View File

@@ -0,0 +1,133 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <lwip/def.h>
#include "esphome/core/defines.h"
#include "esphome/core/progmem.h"
namespace esphome::captive_portal {
// DNS constants
static constexpr uint16_t DNS_PORT = 53;
static constexpr uint16_t DNS_QR_FLAG = 1 << 15;
static constexpr uint16_t DNS_OPCODE_MASK = 0x7800;
static constexpr uint16_t DNS_QTYPE_A = 0x0001;
static constexpr uint16_t DNS_QCLASS_IN = 0x0001;
static constexpr uint16_t DNS_ANSWER_TTL = 300;
static constexpr size_t DNS_BUFFER_SIZE = 192;
// DNS Header structure
struct DNSHeader {
uint16_t id;
uint16_t flags;
uint16_t qd_count;
uint16_t an_count;
uint16_t ns_count;
uint16_t ar_count;
} __attribute__((packed));
// DNS Question structure
struct DNSQuestion {
uint16_t type;
uint16_t dns_class;
} __attribute__((packed));
// DNS Answer structure
struct DNSAnswer {
uint16_t ptr_offset;
uint16_t type;
uint16_t dns_class;
uint32_t ttl;
uint16_t addr_len;
uint32_t ip_addr;
} __attribute__((packed));
/// Check if domain is allowlisted (web_server CDN domains)
/// Returns true if the domain should NOT be redirected
inline bool is_allowlisted_domain([[maybe_unused]] const char *domain) {
#ifdef USE_WEBSERVER
#ifdef WEBSERVER_CDN_DOMAIN_0
if (ESPHOME_strcasecmp_P(domain, ESPHOME_F(WEBSERVER_CDN_DOMAIN_0)) == 0)
return true;
#endif
#ifdef WEBSERVER_CDN_DOMAIN_1
if (ESPHOME_strcasecmp_P(domain, ESPHOME_F(WEBSERVER_CDN_DOMAIN_1)) == 0)
return true;
#endif
#endif
return false;
}
/// Parse DNS domain name from packet into string buffer
/// Returns pointer past the domain name (after null terminator), or nullptr on error
inline uint8_t *parse_dns_domain(uint8_t *ptr, const uint8_t *end, char *domain, size_t domain_size) {
size_t domain_len = 0;
while (ptr < end && *ptr != 0) {
uint8_t label_len = *ptr;
if (label_len > 63) { // Check for invalid label length
return nullptr;
}
// Check if we have room for this label plus the length byte
if (ptr + label_len + 1 > end) {
return nullptr; // Would overflow
}
// Add dot separator if not first label
if (domain_len > 0 && domain_len < domain_size - 1) {
domain[domain_len++] = '.';
}
// Copy label to domain string
for (uint8_t i = 0; i < label_len && domain_len < domain_size - 1; i++) {
domain[domain_len++] = ptr[1 + i];
}
ptr += label_len + 1;
}
domain[domain_len] = '\0';
// Check if we reached a proper null terminator
if (ptr >= end || *ptr != 0) {
return nullptr; // Name not terminated or truncated
}
return ptr + 1; // Skip the null terminator
}
/// Build DNS response header for A record query
/// Modifies header in place, returns true if successful
inline void build_dns_response_header(DNSHeader *header) {
header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative
header->an_count = htons(1); // One answer
}
/// Build DNS REFUSED response header
/// Used when domain is allowlisted to trigger fallback to other DNS
inline void build_dns_refused_header(DNSHeader *header) {
header->flags = htons(DNS_QR_FLAG | 0x8005); // Response + REFUSED
header->an_count = 0;
header->ns_count = 0;
header->ar_count = 0;
}
/// Build DNS answer section
/// Returns the answer pointer, or nullptr if buffer too small
inline DNSAnswer *build_dns_answer(uint8_t *buffer, size_t buffer_size, size_t question_end_offset, uint32_t ip_addr) {
size_t answer_offset = question_end_offset;
// Check if we have room for the answer
if (answer_offset + sizeof(DNSAnswer) > buffer_size) {
return nullptr;
}
DNSAnswer *answer = reinterpret_cast<DNSAnswer *>(buffer + answer_offset);
// Pointer to name in question (offset from start of packet)
answer->ptr_offset = htons(0xC000 | sizeof(DNSHeader));
answer->type = htons(DNS_QTYPE_A);
answer->dns_class = htons(DNS_QCLASS_IN);
answer->ttl = htonl(DNS_ANSWER_TTL);
answer->addr_len = htons(4);
answer->ip_addr = ip_addr;
return answer;
}
} // namespace esphome::captive_portal

View File

@@ -1,8 +1,8 @@
#include "dns_server_esp32_idf.h"
#ifdef USE_ESP32
#include "dns_server_common.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/components/socket/socket.h"
#include <lwip/sockets.h>
#include <lwip/inet.h>
@@ -11,40 +11,6 @@ namespace esphome::captive_portal {
static const char *const TAG = "captive_portal.dns";
// DNS constants
static constexpr uint16_t DNS_PORT = 53;
static constexpr uint16_t DNS_QR_FLAG = 1 << 15;
static constexpr uint16_t DNS_OPCODE_MASK = 0x7800;
static constexpr uint16_t DNS_QTYPE_A = 0x0001;
static constexpr uint16_t DNS_QCLASS_IN = 0x0001;
static constexpr uint16_t DNS_ANSWER_TTL = 300;
// DNS Header structure
struct DNSHeader {
uint16_t id;
uint16_t flags;
uint16_t qd_count;
uint16_t an_count;
uint16_t ns_count;
uint16_t ar_count;
} __attribute__((packed));
// DNS Question structure
struct DNSQuestion {
uint16_t type;
uint16_t dns_class;
} __attribute__((packed));
// DNS Answer structure
struct DNSAnswer {
uint16_t ptr_offset;
uint16_t type;
uint16_t dns_class;
uint32_t ttl;
uint16_t addr_len;
uint32_t ip_addr;
} __attribute__((packed));
void DNSServer::start(const network::IPAddress &ip) {
this->server_ip_ = ip;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -126,27 +92,28 @@ void DNSServer::process_next_request() {
return; // Not a standard query
}
// Parse domain name (we don't actually care about it - redirect everything)
// Parse domain name
uint8_t *ptr = this->buffer_ + sizeof(DNSHeader);
uint8_t *end = this->buffer_ + len;
char domain[128];
while (ptr < end && *ptr != 0) {
uint8_t label_len = *ptr;
if (label_len > 63) { // Check for invalid label length
return;
}
// Check if we have room for this label plus the length byte
if (ptr + label_len + 1 > end) {
return; // Would overflow
}
ptr += label_len + 1;
ptr = parse_dns_domain(ptr, end, domain, sizeof(domain));
if (ptr == nullptr) {
return; // Invalid domain name
}
// Check if we reached a proper null terminator
if (ptr >= end || *ptr != 0) {
return; // Name not terminated or truncated
// Check allowlist and send REFUSED if needed
if (is_allowlisted_domain(domain)) {
ESP_LOGV(TAG, "Allowlisted domain, sending REFUSED: %s", domain);
build_dns_refused_header(header);
ssize_t sent = this->socket_->sendto(this->buffer_, len, 0, (struct sockaddr *) &client_addr, client_addr_len);
if (sent < 0) {
ESP_LOGV(TAG, "Send REFUSED failed: %d", errno);
}
return;
}
ptr++; // Skip the null terminator
ESP_LOGV(TAG, "Redirecting DNS query for: %s", domain);
// Check we have room for the question
if (ptr + sizeof(DNSQuestion) > end) {
@@ -164,34 +131,18 @@ void DNSServer::process_next_request() {
return; // Not an A query
}
// Build DNS response by modifying the request in-place
header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative
header->an_count = htons(1); // One answer
// Build DNS response
build_dns_response_header(header);
// Add answer section after the question
size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader);
size_t answer_offset = sizeof(DNSHeader) + question_len;
// Check if we have room for the answer
if (answer_offset + sizeof(DNSAnswer) > sizeof(this->buffer_)) {
size_t question_end_offset = (ptr + sizeof(DNSQuestion)) - this->buffer_;
ip4_addr_t addr = this->server_ip_;
if (build_dns_answer(this->buffer_, sizeof(this->buffer_), question_end_offset, addr.addr) == nullptr) {
ESP_LOGW(TAG, "Response too large");
return;
}
DNSAnswer *answer = (DNSAnswer *) (this->buffer_ + answer_offset);
// Pointer to name in question (offset from start of packet)
answer->ptr_offset = htons(0xC000 | sizeof(DNSHeader));
answer->type = htons(DNS_QTYPE_A);
answer->dns_class = htons(DNS_QCLASS_IN);
answer->ttl = htonl(DNS_ANSWER_TTL);
answer->addr_len = htons(4);
// Get the raw IP address
ip4_addr_t addr = this->server_ip_;
answer->ip_addr = addr.addr;
size_t response_len = answer_offset + sizeof(DNSAnswer);
size_t response_len = question_end_offset + sizeof(DNSAnswer);
// Send response
ssize_t sent =

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32
#include <memory>
#include "dns_server_common.h"
#include "esphome/core/helpers.h"
#include "esphome/components/network/ip_address.h"
#include "esphome/components/socket/socket.h"
@@ -15,8 +16,6 @@ class DNSServer {
void process_next_request();
protected:
static constexpr size_t DNS_BUFFER_SIZE = 192;
std::unique_ptr<socket::Socket> socket_{nullptr};
network::IPAddress server_ip_;
uint8_t buffer_[DNS_BUFFER_SIZE];

View File

@@ -17,7 +17,7 @@ from esphome.core import CORE, HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["socket"]
AUTO_LOAD = ["network", "socket"]
byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)

View File

@@ -149,12 +149,6 @@ bool ESPNowComponent::is_wifi_enabled() {
}
void ESPNowComponent::setup() {
#ifndef USE_WIFI
// Initialize LwIP stack for wake_loop_threadsafe() socket support
// When WiFi component is present, it handles esp_netif_init()
ESP_ERROR_CHECK(esp_netif_init());
#endif
if (this->enable_on_boot_) {
this->enable_();
} else {
@@ -174,8 +168,6 @@ void ESPNowComponent::enable() {
void ESPNowComponent::enable_() {
if (!this->is_wifi_enabled()) {
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

View File

@@ -102,11 +102,6 @@ void EthernetComponent::setup() {
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);

View File

@@ -550,12 +550,12 @@ async def to_code(config: ConfigType) -> None:
ref="0.2.2",
)
# Set compile-time configuration via defines
# Set compile-time configuration via build flags (so external library sees them)
if CONF_BIT_DEPTH in config:
cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
cg.add_build_flag(f"-DHUB75_BIT_DEPTH={config[CONF_BIT_DEPTH]}")
if CONF_GAMMA_CORRECT in config:
cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
cg.add_build_flag(f"-DHUB75_GAMMA_MODE={config[CONF_GAMMA_CORRECT].enum_value}")
# Await all pin expressions
pin_expressions = {

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import contextlib
from dataclasses import dataclass
import hashlib
import io
import logging
@@ -37,11 +38,21 @@ image_ns = cg.esphome_ns.namespace("image")
ImageType = image_ns.enum("ImageType")
@dataclass(frozen=True)
class ImageMetaData:
width: int
height: int
image_type: str
transparency: str
CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
KEY_METADATA = "metadata"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
@@ -723,10 +734,38 @@ async def write_image(config, all_frames=False):
return prog_arr, width, height, image_type, trans_value, frame_count
async def _image_to_code(entry):
"""
Convert a single image entry to code and return its metadata.
:param entry: The config entry for the image.
:return: An ImageMetaData object
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value)
return ImageMetaData(
width,
height,
entry[CONF_TYPE],
entry[CONF_TRANSPARENCY],
)
async def to_code(config):
# By now the config should be a simple list.
for entry in config:
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
)
cg.add_define("USE_IMAGE")
# By now the config will be a simple list.
# Use a subkey to allow for other data in the future
CORE.data[DOMAIN] = {
KEY_METADATA: {
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
}
}
def get_all_image_metadata() -> dict[str, ImageMetaData]:
"""Get all image metadata."""
return CORE.data.get(DOMAIN, {}).get(KEY_METADATA, {})
def get_image_metadata(image_id: str) -> ImageMetaData | None:
"""Get image metadata by ID for use by other components."""
return get_all_image_metadata().get(image_id)

View File

@@ -174,9 +174,9 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 1), "libretiny"),
"recommended": (cv.Version(1, 9, 1), None),
"dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 2), "libretiny"),
"recommended": (cv.Version(1, 9, 2), None),
}

View File

@@ -197,8 +197,8 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
#endif
#ifdef USE_ESP32
// Start with loop disabled when using task buffer (unless using USB CDC)
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
// The loop will be enabled automatically when messages arrive
this->disable_loop_when_buffer_empty_();
#endif
@@ -247,7 +247,7 @@ void Logger::process_messages_() {
}
#endif
}
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
else {
// No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity

View File

@@ -609,8 +609,8 @@ class Logger : public Component {
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
}
#ifdef USE_ESP32
// Disable loop when task buffer is empty (with USB CDC check)
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
// concurrently. If that happens between our check and disable_loop(), the enable request

View File

@@ -77,6 +77,13 @@ CONF_DISCOVER_IP = "discover_ip"
CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_WAIT_FOR_CONNECTION = "wait_for_connection"
# Max lengths for stack-based topic building.
# These values are used in cv.Length() validators below to ensure the C++ code
# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow.
# If you change these, update the corresponding constants in mqtt_component.cpp.
TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short
DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars)
def validate_message_just_topic(value):
value = cv.publish_topic(value)
@@ -106,6 +113,7 @@ MQTT_MESSAGE_SCHEMA = cv.Any(
mqtt_ns = cg.esphome_ns.namespace("mqtt")
MQTTMessage = mqtt_ns.struct("MQTTMessage")
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action)
MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
@@ -117,9 +125,11 @@ MQTTMessageTrigger = mqtt_ns.class_(
MQTTJsonMessageTrigger = mqtt_ns.class_(
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
)
MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template())
MQTTConnectTrigger = mqtt_ns.class_(
"MQTTConnectTrigger", automation.Trigger.template(cg.bool_)
)
MQTTDisconnectTrigger = mqtt_ns.class_(
"MQTTDisconnectTrigger", automation.Trigger.template()
"MQTTDisconnectTrigger", automation.Trigger.template(MQTTClientDisconnectReason)
)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
@@ -253,9 +263,9 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean,
cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean,
cv.Optional(
CONF_DISCOVERY_PREFIX, default="homeassistant"
): cv.publish_topic,
cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.All(
cv.publish_topic, cv.Length(max=DISCOVERY_PREFIX_MAX_LEN)
),
cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum(
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS
),
@@ -266,7 +276,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic,
cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.All(
cv.publish_topic, cv.Length(max=TOPIC_PREFIX_MAX_LEN)
),
cv.Optional(CONF_LOG_TOPIC): cv.Any(
None,
MQTT_MESSAGE_BASE.extend(
@@ -466,11 +478,15 @@ async def to_code(config):
for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_automation(
trigger, [(cg.bool_, "session_present")], conf
)
for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_automation(
trigger, [(MQTTClientDisconnectReason, "reason")], conf
)
cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE]))

View File

@@ -79,7 +79,7 @@ void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendD
root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm();
}
std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; }
MQTT_COMPONENT_TYPE(MQTTAlarmControlPanelComponent, "alarm_control_panel")
const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; }
bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }

View File

@@ -25,7 +25,7 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent {
void dump_config() override;
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
alarm_control_panel::AlarmControlPanel *alarm_control_panel_;

View File

@@ -10,7 +10,7 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt.binary_sensor";
std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; }
MQTT_COMPONENT_TYPE(MQTTBinarySensorComponent, "binary_sensor")
const EntityBase *MQTTBinarySensorComponent::get_entity() const { return this->binary_sensor_; }
void MQTTBinarySensorComponent::setup() {

View File

@@ -29,7 +29,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent {
bool publish_state(bool state);
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
binary_sensor::BinarySensor *binary_sensor_;

View File

@@ -39,7 +39,7 @@ void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
std::string MQTTButtonComponent::component_type() const { return "button"; }
MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button")
const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; }
} // namespace esphome::mqtt

View File

@@ -26,7 +26,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent {
protected:
/// "button" component type.
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
button::Button *button_;

View File

@@ -378,17 +378,17 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
}
};
class MQTTConnectTrigger : public Trigger<> {
class MQTTConnectTrigger : public Trigger<bool> {
public:
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
client->set_on_connect([this](bool session_present) { this->trigger(); });
client->set_on_connect([this](bool session_present) { this->trigger(session_present); });
}
};
class MQTTDisconnectTrigger : public Trigger<> {
class MQTTDisconnectTrigger : public Trigger<MQTTClientDisconnectReason> {
public:
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); });
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); });
}
};

View File

@@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() {
}
MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {}
bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); }
std::string MQTTClimateComponent::component_type() const { return "climate"; }
MQTT_COMPONENT_TYPE(MQTTClimateComponent, "climate")
const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; }
bool MQTTClimateComponent::publish_state_() {

View File

@@ -15,7 +15,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent {
MQTTClimateComponent(climate::Climate *device);
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
std::string component_type() const override;
const char *component_type() const override;
void setup() override;
MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state)

View File

@@ -13,6 +13,34 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt.component";
// Helper functions for building topic strings on stack
inline char *append_str(char *p, const char *s, size_t len) {
memcpy(p, s, len);
return p + len;
}
inline char *append_char(char *p, char c) {
*p = c;
return p + 1;
}
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough.
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer sizes - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
@@ -21,8 +49,23 @@ void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
std::string sanitized_name = str_sanitize(App.get_name());
return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" +
this->get_default_object_id_() + "/config";
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DISCOVERY_TOPIC_MAX_LEN];
char *p = buf;
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, sanitized_name.data(), sanitized_name.size());
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_str(p, "/config", 7);
return std::string(buf, p - buf);
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
@@ -32,7 +75,22 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
return "";
}
return topic_prefix + "/" + this->component_type() + "/" + this->get_default_object_id_() + "/" + suffix;
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DEFAULT_TOPIC_MAX_LEN];
char *p = buf;
p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size());
return std::string(buf, p - buf);
}
std::string MQTTComponent::get_state_topic_() const {
@@ -123,6 +181,8 @@ bool MQTTComponent::send_discovery_() {
}
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
@@ -131,12 +191,12 @@ bool MQTTComponent::send_discovery_() {
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_();
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
}
const std::string &node_name = App.get_name();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
@@ -194,10 +254,6 @@ bool MQTTComponent::is_discovery_enabled() const {
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
}
std::string MQTTComponent::get_default_object_id_() const {
return str_sanitize(str_snake_case(this->friendly_name_()));
}
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
global_mqtt_client->subscribe(topic, std::move(callback), qos);
}
@@ -280,6 +336,9 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec
// Pull these properties from EntityBase if not overridden
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
return this->get_entity()->get_object_id_to(buf);
}
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::is_internal() {

View File

@@ -19,6 +19,10 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
if (state_topic) { \
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
@@ -27,7 +31,18 @@ struct SendDiscoveryConfig {
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
// Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
#define MQTT_COMPONENT_TYPE(class_name, type_str) \
const char *class_name::component_type() const { return type_str; } \
static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \
#class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN");
// Macro to define custom topic getter/setter with compile-time suffix length verification
#define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \
static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \
"topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \
\
protected: \
std::string custom_##name##_##type##_topic_{}; \
\
@@ -92,7 +107,7 @@ class MQTTComponent : public Component {
void set_subscribe_qos(uint8_t qos);
/// Override this method to return the component type (e.g. "light", "sensor", ...)
virtual std::string component_type() const = 0;
virtual const char *component_type() const = 0;
/// Set a custom state topic. Set to "" for default behavior.
void set_custom_state_topic(const char *custom_state_topic);
@@ -185,8 +200,8 @@ class MQTTComponent : public Component {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
std::string get_default_object_id_() const;
/// Get the object ID for this MQTT component, writing to the provided buffer.
StringRef get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const;
StringRef custom_state_topic_{};
StringRef custom_command_topic_{};

View File

@@ -90,7 +90,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
}
}
std::string MQTTCoverComponent::component_type() const { return "cover"; }
MQTT_COMPONENT_TYPE(MQTTCoverComponent, "cover")
const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; }
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }

View File

@@ -29,7 +29,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
void dump_config() override;
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
cover::Cover *cover_;

View File

@@ -39,7 +39,7 @@ void MQTTDateComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTDateComponent::component_type() const { return "date"; }
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; }
void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -31,7 +31,7 @@ class MQTTDateComponent : public mqtt::MQTTComponent {
bool publish_state(uint16_t year, uint8_t month, uint8_t day);
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
datetime::DateEntity *date_;

View File

@@ -50,7 +50,7 @@ void MQTTDateTimeComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTDateTimeComponent::component_type() const { return "datetime"; }
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")
const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; }
void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -31,7 +31,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
datetime::DateTimeEntity *datetime_;

View File

@@ -50,7 +50,7 @@ bool MQTTEventComponent::publish_event_(const std::string &event_type) {
});
}
std::string MQTTEventComponent::component_type() const { return "event"; }
MQTT_COMPONENT_TYPE(MQTTEventComponent, "event")
const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; }
} // namespace esphome::mqtt

View File

@@ -25,7 +25,7 @@ class MQTTEventComponent : public mqtt::MQTTComponent {
protected:
bool publish_event_(const std::string &event_type);
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
event::Event *event_;

View File

@@ -15,7 +15,7 @@ using namespace esphome::fan;
MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {}
Fan *MQTTFanComponent::get_state() const { return this->state_; }
std::string MQTTFanComponent::component_type() const { return "fan"; }
MQTT_COMPONENT_TYPE(MQTTFanComponent, "fan")
const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; }
void MQTTFanComponent::setup() {

View File

@@ -36,7 +36,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent {
bool send_initial_state() override;
bool publish_state();
/// 'fan' component type for discovery.
std::string component_type() const override;
const char *component_type() const override;
fan::Fan *get_state() const;

View File

@@ -14,7 +14,7 @@ static const char *const TAG = "mqtt.light";
using namespace esphome::light;
std::string MQTTJSONLightComponent::component_type() const { return "light"; }
MQTT_COMPONENT_TYPE(MQTTJSONLightComponent, "light")
const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; }
void MQTTJSONLightComponent::setup() {

View File

@@ -28,7 +28,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRe
void on_light_remote_values_update() override;
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
bool publish_state_();

View File

@@ -34,7 +34,7 @@ void MQTTLockComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTLockComponent::component_type() const { return "lock"; }
MQTT_COMPONENT_TYPE(MQTTLockComponent, "lock")
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson

View File

@@ -27,7 +27,7 @@ class MQTTLockComponent : public mqtt::MQTTComponent {
protected:
/// "lock" component type.
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
lock::Lock *lock_;

View File

@@ -33,7 +33,7 @@ void MQTTNumberComponent::dump_config() {
LOG_MQTT_COMPONENT(true, false)
}
std::string MQTTNumberComponent::component_type() const { return "number"; }
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")
const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_; }
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -32,7 +32,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent {
protected:
/// Override for MQTTComponent, returns "number".
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
number::Number *number_;

View File

@@ -28,7 +28,7 @@ void MQTTSelectComponent::dump_config() {
LOG_MQTT_COMPONENT(true, false)
}
std::string MQTTSelectComponent::component_type() const { return "select"; }
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")
const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_; }
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -32,7 +32,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent {
protected:
/// Override for MQTTComponent, returns "select".
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
select::Select *select_;

View File

@@ -31,7 +31,7 @@ void MQTTSensorComponent::dump_config() {
LOG_MQTT_COMPONENT(true, false)
}
std::string MQTTSensorComponent::component_type() const { return "sensor"; }
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
const EntityBase *MQTTSensorComponent::get_entity() const { return this->sensor_; }
uint32_t MQTTSensorComponent::get_expire_after() const {

View File

@@ -43,7 +43,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent {
protected:
/// Override for MQTTComponent, returns "sensor".
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
sensor::Sensor *sensor_;

View File

@@ -41,7 +41,7 @@ void MQTTSwitchComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
MQTT_COMPONENT_TYPE(MQTTSwitchComponent, "switch")
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson

View File

@@ -27,7 +27,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent {
protected:
/// "switch" component type.
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
switch_::Switch *switch_;

View File

@@ -29,7 +29,7 @@ void MQTTTextComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTTextComponent::component_type() const { return "text"; }
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -32,7 +32,7 @@ class MQTTTextComponent : public mqtt::MQTTComponent {
protected:
/// Override for MQTTComponent, returns "text".
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
text::Text *text_;

View File

@@ -39,7 +39,7 @@ bool MQTTTextSensor::send_initial_state() {
return true;
}
}
std::string MQTTTextSensor::component_type() const { return "sensor"; }
MQTT_COMPONENT_TYPE(MQTTTextSensor, "sensor")
const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; }
} // namespace esphome::mqtt

View File

@@ -25,7 +25,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent {
bool send_initial_state() override;
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
text_sensor::TextSensor *sensor_;

View File

@@ -39,7 +39,7 @@ void MQTTTimeComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTTimeComponent::component_type() const { return "time"; }
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")
const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; }
void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -31,7 +31,7 @@ class MQTTTimeComponent : public mqtt::MQTTComponent {
bool publish_state(uint8_t hour, uint8_t minute, uint8_t second);
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
datetime::TimeEntity *time_;

View File

@@ -52,7 +52,7 @@ void MQTTUpdateComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTUpdateComponent::component_type() const { return "update"; }
MQTT_COMPONENT_TYPE(MQTTUpdateComponent, "update")
const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; }
} // namespace esphome::mqtt

View File

@@ -27,7 +27,7 @@ class MQTTUpdateComponent : public mqtt::MQTTComponent {
protected:
/// "update" component type.
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
update::UpdateEntity *update_;

View File

@@ -65,7 +65,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
}
}
std::string MQTTValveComponent::component_type() const { return "valve"; }
MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve")
const EntityBase *MQTTValveComponent::get_entity() const { return this->valve_; }
bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }

View File

@@ -27,7 +27,7 @@ class MQTTValveComponent : public mqtt::MQTTComponent {
void dump_config() override;
protected:
std::string component_type() const override;
const char *component_type() const override;
const EntityBase *get_entity() const override;
valve::Valve *valve_;

View File

@@ -5,7 +5,7 @@ import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
from esphome.const import CONF_ENABLE_IPV6, CONF_ID, CONF_MIN_IPV6_ADDR_COUNT
from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
@@ -20,6 +20,7 @@ CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
network_ns = cg.esphome_ns.namespace("network")
IPAddress = network_ns.class_("IPAddress")
NetworkComponent = network_ns.class_("NetworkComponent", cg.Component)
def ip_address_literal(ip: str | int | None) -> cg.MockObj:
@@ -107,6 +108,7 @@ def has_high_performance_networking() -> bool:
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(NetworkComponent),
cv.SplitDefault(
CONF_ENABLE_IPV6,
esp8266=False,
@@ -140,6 +142,12 @@ async def to_code(config):
if CORE.using_arduino and CORE.is_esp32:
cg.add_library("Networking", None)
# Register NetworkComponent to initialize network stack early (ESP32 only)
# This ensures esp_netif_init() is called before web_server binds
if CORE.is_esp32:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Apply high performance networking settings
# Config can explicitly enable/disable, or default to component-driven behavior
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)

View File

@@ -0,0 +1,32 @@
#include "network_component.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "esp_event.h"
#include "esp_netif.h"
namespace esphome::network {
static const char *const TAG = "network";
void NetworkComponent::setup() {
// Initialize network stack early - required before web_server can bind.
// This must run before WiFi/Ethernet setup so web_server can register
// its handlers before captive_portal.
esp_err_t err = esp_netif_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err));
}
err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
// ESP_ERR_INVALID_STATE means it was already created
ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err));
}
}
float NetworkComponent::get_setup_priority() const {
// Run before web_server (WIFI + 0.5) and WiFi (WIFI)
return setup_priority::WIFI + 2.0f;
}
} // namespace esphome::network
#endif

View File

@@ -0,0 +1,16 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/component.h"
namespace esphome::network {
/// Component that initializes the network stack early.
/// This allows web_server to bind before WiFi/Ethernet setup.
class NetworkComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override;
};
} // namespace esphome::network
#endif

View File

@@ -35,8 +35,6 @@ void OpenThreadComponent::setup() {
.max_fds = 3,
};
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
xTaskCreate(

View File

@@ -128,6 +128,39 @@ void RemoteReceiverBase::call_dumpers_() {
void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); }
/* RemoteTransmitData */
void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count) {
this->data_.clear();
this->data_.reserve(count);
while (len > 0) {
// Parse varint (inline, no dependency on api component)
uint32_t raw = 0;
uint32_t shift = 0;
uint32_t consumed = 0;
for (; consumed < len && consumed < 5; consumed++) {
uint8_t byte = data[consumed];
raw |= (byte & 0x7F) << shift;
if ((byte & 0x80) == 0) {
consumed++;
break;
}
shift += 7;
}
if (consumed == 0)
break; // Parse error
// Zigzag decode: (n >> 1) ^ -(n & 1)
int32_t decoded = static_cast<int32_t>((raw >> 1) ^ (~(raw & 1) + 1));
this->data_.push_back(decoded);
data += consumed;
len -= consumed;
}
}
/* RemoteTransmitterBase */
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();

View File

@@ -31,6 +31,11 @@ class RemoteTransmitData {
uint32_t get_carrier_frequency() const { return this->carrier_frequency_; }
const RawTimings &get_data() const { return this->data_; }
void set_data(const RawTimings &data) { this->data_ = data; }
/// Set data from packed protobuf sint32 buffer (zigzag + varint encoded)
/// @param data Pointer to packed zigzag-varint-encoded sint32 values
/// @param len Length of the buffer in bytes
/// @param count Number of values (for reserve optimization)
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;

View File

@@ -153,6 +153,19 @@ def generate_comparable_preset(config, name):
return comparable_preset
def validate_heat_cool_mode(value) -> list:
"""Validate heat_cool_mode - accepts either True or an automation."""
if value is True:
# Convert True to empty automation list
return []
if value is False:
raise cv.Invalid(
"heat_cool_mode cannot be 'false'. Specify 'true' to enable the mode or provide an automation"
)
# Otherwise validate as automation
return automation.validate_automation(single=True)(value)
def validate_thermostat(config):
# verify corresponding action(s) exist(s) for any defined climate mode or action
requirements = {
@@ -554,9 +567,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(
single=True
),
cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation(
single=True
),
cv.Optional(CONF_HEAT_COOL_MODE): validate_heat_cool_mode,
cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True),
cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True),
cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(
@@ -828,9 +839,11 @@ async def to_code(config):
)
cg.add(var.set_supports_heat(True))
if CONF_HEAT_COOL_MODE in config:
await automation.build_automation(
var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE]
)
# Build automation only if user provided actions (not just `true`)
if config[CONF_HEAT_COOL_MODE]:
await automation.build_automation(
var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE]
)
cg.add(var.set_supports_heat_cool(True))
if CONF_OFF_MODE in config:
await automation.build_automation(

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import gzip
from urllib.parse import urlparse
import esphome.codegen as cg
from esphome.components import web_server_base
@@ -42,6 +43,8 @@ CONF_SORTING_GROUP_ID = "sorting_group_id"
CONF_SORTING_GROUPS = "sorting_groups"
CONF_SORTING_WEIGHT = "sorting_weight"
# CDN host for web_server assets - used for default URLs and DNS allowlisting
CDN_HOST = "oi.esphome.io"
web_server_ns = cg.esphome_ns.namespace("web_server")
WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller)
@@ -53,19 +56,19 @@ def default_url(config: ConfigType) -> ConfigType:
config = config.copy()
if config[CONF_VERSION] == 1:
if CONF_CSS_URL not in config:
config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css"
config[CONF_CSS_URL] = f"https://{CDN_HOST}/v1/webserver-v1.min.css"
if CONF_JS_URL not in config:
config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js"
config[CONF_JS_URL] = f"https://{CDN_HOST}/v1/webserver-v1.min.js"
if config[CONF_VERSION] == 2:
if CONF_CSS_URL not in config:
config[CONF_CSS_URL] = ""
if CONF_JS_URL not in config:
config[CONF_JS_URL] = "https://oi.esphome.io/v2/www.js"
config[CONF_JS_URL] = f"https://{CDN_HOST}/v2/www.js"
if config[CONF_VERSION] == 3:
if CONF_CSS_URL not in config:
config[CONF_CSS_URL] = ""
if CONF_JS_URL not in config:
config[CONF_JS_URL] = "https://oi.esphome.io/v3/www.js"
config[CONF_JS_URL] = f"https://{CDN_HOST}/v3/www.js"
return config
@@ -335,6 +338,24 @@ async def to_code(config):
if config[CONF_COMPRESSION] == "gzip":
cg.add_define("USE_WEBSERVER_GZIP")
# Extract domains from CDN URLs for DNS allowlisting (used by captive_portal)
# This handles both default URLs (using CDN_HOST) and custom user URLs
cdn_domains: set[str] = set()
for url_key in (CONF_CSS_URL, CONF_JS_URL):
url = config.get(url_key, "")
if url:
try:
parsed = urlparse(url)
if parsed.netloc:
cdn_domains.add(parsed.netloc.lower())
except Exception: # pylint: disable=broad-except
pass
# Generate defines for each CDN domain (used by captive_portal DNS allowlist)
for i, domain in enumerate(sorted(cdn_domains)):
cg.add_define(f"WEBSERVER_CDN_DOMAIN_{i}", domain)
cg.add_define("WEBSERVER_CDN_DOMAIN_COUNT", len(cdn_domains))
if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None:
cg.add_define("USE_WEBSERVER_SORTING")
add_sorting_groups(var, sorting_group_config)

View File

@@ -24,6 +24,10 @@
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/components/captive_portal/captive_portal.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
@@ -413,7 +417,10 @@ void WebServer::dump_config() {
" Address: %s:%u",
network::get_use_address(), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
float WebServer::get_setup_priority() const {
// Run before WiFi so handlers are registered before captive_portal
return setup_priority::WIFI + 0.5f;
}
#ifdef USE_WEBSERVER_LOCAL
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
@@ -498,14 +505,16 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
// Build id into stack buffer - ArduinoJson copies the string
// Format: {prefix}/{device?}/{name}
// Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120):
// With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin
// Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin
// Buffer sizes use constants from entity_base.h validated in core/config.py
// Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN
// (hostname)
#ifdef USE_DEVICES
char id_buf[280];
static constexpr size_t ID_BUF_SIZE =
ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1;
#else
char id_buf[150];
static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1;
#endif
char id_buf[ID_BUF_SIZE];
char *p = id_buf;
memcpy(p, prefix, prefix_len);
p += prefix_len;
@@ -1957,9 +1966,20 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
const auto &url = request->url();
const auto method = request->method();
// Static URL checks
// Handle root URL
if (url == ESPHOME_F("/")) {
#ifdef USE_CAPTIVE_PORTAL
// When captive portal is active, only handle "/" if ?web_server param is present
// This lets captive_portal show its page at "/" while web_server handles /?web_server
if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) {
return request->hasParam(ESPHOME_F("web_server"));
}
#endif
return true;
}
// Other static URL checks
static const char *const STATIC_URLS[] = {
"/",
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
"/events",
#endif

View File

@@ -12,8 +12,6 @@ static const char *const TAG = "web_server_base";
WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WebServerBase::add_handler(AsyncWebHandler *handler) {
// remove all handlers
#ifdef USE_WEBSERVER_AUTH
if (!credentials_.username.empty()) {
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);

View File

@@ -238,12 +238,21 @@ def _apply_min_auth_mode_default(config):
def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_ap = CONF_AP in config
has_improv = "esp32_improv" in fv.full_config.get()
has_improv_serial = "improv_serial" in fv.full_config.get()
full_config = fv.full_config.get()
has_improv = "esp32_improv" in full_config
has_improv_serial = "improv_serial" in full_config
has_captive_portal = "captive_portal" in full_config
has_web_server = "web_server" in full_config
if not (has_sta or has_ap or has_improv or has_improv_serial):
raise cv.Invalid(
"Please specify at least an SSID or an Access Point to create."
)
if has_ap and not has_captive_portal and not has_web_server:
_LOGGER.warning(
"WiFi AP is configured but neither captive_portal nor web_server is enabled. "
"The AP will not be usable for configuration or monitoring. "
"Add 'captive_portal:' or 'web_server:' to your configuration."
)
FINAL_VALIDATE_SCHEMA = cv.All(

View File

@@ -151,48 +151,51 @@ static const char *const TAG = "wifi";
/// │ Purpose: Handle AP reboot or power loss scenarios where device │
/// │ connects to suboptimal AP and never switches back │
/// │ │
/// │ Loop call site: roaming enabled && attempts < 3 && 5 min elapsed
/// │ ↓ │
/// │ ┌─────────────────┐ Hidden? ┌──────────────────────────┐ │
/// │ │ check_roaming_ ├───────────→│ attempts = MAX, stop │ │
/// │ └────────┬────────┘ └──────────────────────────┘ │
/// │ ↓ │
/// │ attempts++, update last_check │
/// │ ↓ │
/// │ RSSI > -49 dBm? ────Yes────→ Skip scan (excellent signal)─┐ │
/// │ ↓ No │ │
/// │ ┌─────────────────┐ │ │
/// │ │ Start scan │ │ │
/// │ └────────┬────────┘ │ │
/// │ ↓ │ │
/// │ ┌────────────────────────┐ │ │
/// │ │ process_roaming_scan_ │ │ │
/// │ └────────┬───────────────┘ │ │
/// │ ↓ │ │
/// │ ┌─────────────────┐ No ┌───────────────┐ │ │
/// │ │ +10 dB better AP├────────→│ Stay connected│───────────────┤ │
/// │ └────────┬────────┘ └───────────────┘ │ │
/// │ │ Yes │ │
/// │ ↓ │ │
/// │ ┌─────────────────┐ │ │
/// │ │ start_connecting│ (roaming_connect_active_ = true) │ │
/// │ └────────┬────────┘ │ │
/// │ ↓ │ │
/// │ ┌────┴────┐ │ │
/// │ ↓ ↓ │ │
/// │ ┌───────┐ ┌───────┐ │ │
/// │ │SUCCESS│ │FAILED │ │ │
/// │ └───┬───┘ └───┬───┘ │ │
/// │ ↓ ↓ │ │
/// │ Keep counter retry_connect() → normal reconnect flow │ │
/// │ (no reset) (keeps counter, handles retries) │ │
/// │ │ │ │ │
/// │ └──────────────┴────────────────────────────────────────┘ │
/// │ State Machine (RoamingState):
/// │ │
/// │ After 3 checks: attempts >= 3, stop checking
/// │ Non-roaming disconnect: clear_roaming_state_() resets counter
/// │ Roaming success: counter preserved (prevents ping-pong)
/// │ Roaming fail: normal flow handles reconnection, counter preserved
/// │ ┌─────────────────────────────────────────────────────────────┐
/// │ │ IDLE │
/// │ │ (waiting for 5 min timer, attempts < 3)
/// │ └─────────────────────────┬───────────────────────────────────┘
/// │ │ 5 min elapsed, RSSI < -49 dBm │
/// │ ↓ │
/// │ ┌─────────────────────────────────────────────────────────────┐ │
/// │ │ SCANNING │ │
/// │ │ (attempts++ in check_roaming_ before entering this state) │ │
/// │ └─────────────────────────┬───────────────────────────────────┘ │
/// │ │ │
/// │ ┌──────────────┼──────────────┐ │
/// │ ↓ ↓ ↓ │
/// │ scan error no better AP +10 dB better AP │
/// │ │ │ │ │
/// │ ↓ ↓ ↓ │
/// │ ┌──────────────────────────────┐ ┌──────────────────────────┐ │
/// │ │ → IDLE │ │ CONNECTING │ │
/// │ │ (counter preserved) │ │ (process_roaming_scan_) │ │
/// │ └──────────────────────────────┘ └────────────┬─────────────┘ │
/// │ │ │
/// │ ┌───────────────────┴───────────────┐ │
/// │ ↓ ↓ │
/// │ SUCCESS FAILED │
/// │ │ │ │
/// │ ↓ ↓ │
/// │ ┌──────────────────────────────────┐ ┌─────────────────────────┐
/// │ │ → IDLE │ │ RECONNECTING │
/// │ │ (counter reset to 0) │ │ (retry_connect called) │
/// │ └──────────────────────────────────┘ └───────────┬─────────────┘
/// │ │ │
/// │ ↓ │
/// │ ┌───────────────────────┐ │
/// │ │ → IDLE │ │
/// │ │ (counter preserved!) │ │
/// │ └───────────────────────┘ │
/// │ │
/// │ Key behaviors: │
/// │ - After 3 checks: attempts >= 3, stop checking │
/// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │
/// │ - Scan error (SCANNING→IDLE): counter preserved │
/// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │
/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │
/// └──────────────────────────────────────────────────────────────────────┘
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
@@ -411,9 +414,6 @@ void WiFiComponent::setup() {
if (this->enable_on_boot_) {
this->start();
} else {
#ifdef USE_ESP32
esp_netif_init();
#endif
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
}
}
@@ -574,12 +574,12 @@ void WiFiComponent::loop() {
// Post-connect roaming: check for better AP
if (this->post_connect_roaming_) {
if (this->roaming_scan_active_) {
if (this->roaming_state_ == RoamingState::SCANNING) {
if (this->scan_done_) {
this->process_roaming_scan_();
}
// else: scan in progress, wait
} else if (this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
} else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL) {
this->check_roaming_(now);
}
@@ -1302,12 +1302,20 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
// Reset roaming state on successful connection
this->roaming_last_check_ = now;
// Only reset attempts if this wasn't a roaming-triggered connection
// (prevents ping-pong between APs)
if (!this->roaming_connect_active_) {
// Only preserve attempts if reconnecting after a failed roam attempt
// This prevents ping-pong between APs when a roam target is unreachable
if (this->roaming_state_ == RoamingState::CONNECTING) {
// Successful roam to better AP - reset attempts so we can roam again later
ESP_LOGD(TAG, "Roam successful");
this->roaming_attempts_ = 0;
} else if (this->roaming_state_ == RoamingState::RECONNECTING) {
// Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong
ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
} else {
// Normal connection (boot, credentials changed, etc.)
this->roaming_attempts_ = 0;
}
this->roaming_connect_active_ = false;
this->roaming_state_ = RoamingState::IDLE;
// Clear all priority penalties - the next reconnect will happen when an AP disconnects,
// which means the landscape has likely changed and previous tracked failures are stale
@@ -1733,16 +1741,21 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() {
}
void WiFiComponent::retry_connect() {
// If this was a roaming attempt, preserve roaming_attempts_ count
// (so we stop roaming after ROAMING_MAX_ATTEMPTS failures)
// Otherwise reset all roaming state
if (this->roaming_connect_active_) {
this->roaming_connect_active_ = false;
this->roaming_scan_active_ = false;
// Keep roaming_attempts_ - will prevent further roaming after max failures
} else {
// Handle roaming state transitions - preserve attempts counter to prevent ping-pong
// to unreachable APs after ROAMING_MAX_ATTEMPTS failures
if (this->roaming_state_ == RoamingState::CONNECTING) {
// Roam connection failed - transition to reconnecting
ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->roaming_state_ = RoamingState::RECONNECTING;
} else if (this->roaming_state_ == RoamingState::SCANNING) {
// Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter
ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->roaming_state_ = RoamingState::IDLE;
} else if (this->roaming_state_ == RoamingState::IDLE) {
// Not a roaming-triggered reconnect, reset state
this->clear_roaming_state_();
}
// RECONNECTING: keep state and counter, still trying to reconnect
this->log_and_adjust_priority_for_failed_connect_();
@@ -1989,8 +2002,7 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->
void WiFiComponent::clear_roaming_state_() {
this->roaming_attempts_ = 0;
this->roaming_last_check_ = 0;
this->roaming_scan_active_ = false;
this->roaming_connect_active_ = false;
this->roaming_state_ = RoamingState::IDLE;
}
void WiFiComponent::release_scan_results_() {
@@ -2018,17 +2030,21 @@ void WiFiComponent::check_roaming_(uint32_t now) {
// Guard: skip scan if signal is already good (no meaningful improvement possible)
int8_t rssi = this->wifi_rssi();
if (rssi > ROAMING_GOOD_RSSI)
if (rssi > ROAMING_GOOD_RSSI) {
ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_,
ROAMING_MAX_ATTEMPTS);
return;
}
ESP_LOGD(TAG, "Roam scan (%d dBm)", rssi);
this->roaming_scan_active_ = true;
ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->roaming_state_ = RoamingState::SCANNING;
this->wifi_scan_start_(this->passive_scan_);
}
void WiFiComponent::process_roaming_scan_() {
this->scan_done_ = false;
this->roaming_scan_active_ = false;
// Default to IDLE - will be set to CONNECTING if we find a better AP
this->roaming_state_ = RoamingState::IDLE;
// Get current connection info
int8_t current_rssi = this->wifi_rssi();
@@ -2066,7 +2082,8 @@ void WiFiComponent::process_roaming_scan_() {
const WiFiAP *selected = this->get_selected_sta_();
int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi;
if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) {
ESP_LOGV(TAG, "Roam best %+d dB (need +%d)", improvement, ROAMING_MIN_IMPROVEMENT);
ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT,
this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->release_scan_results_();
return;
}
@@ -2079,7 +2096,7 @@ void WiFiComponent::process_roaming_scan_() {
this->release_scan_results_();
// Mark as roaming attempt - affects retry behavior if connection fails
this->roaming_connect_active_ = true;
this->roaming_state_ = RoamingState::CONNECTING;
// Connect directly - wifi_sta_connect_ handles disconnect internally
this->error_from_callback_ = false;

View File

@@ -112,6 +112,18 @@ enum class WiFiRetryPhase : uint8_t {
RESTARTING_ADAPTER,
};
/// Tracks post-connect roaming state machine
enum class RoamingState : uint8_t {
/// Not roaming, waiting for next check interval
IDLE,
/// Scanning for better AP
SCANNING,
/// Attempting to connect to better AP found in scan
CONNECTING,
/// Roam connection failed, reconnecting to any available AP
RECONNECTING,
};
/// Struct for setting static IPs in WiFiComponent.
struct ManualIP {
network::IPAddress static_ip;
@@ -667,8 +679,7 @@ class WiFiComponent : public Component {
bool did_scan_this_cycle_{false};
bool skip_cooldown_next_cycle_{false};
bool post_connect_roaming_{true}; // Enabled by default
bool roaming_scan_active_{false};
bool roaming_connect_active_{false}; // True during roaming connection attempt (preserves roaming_attempts_)
RoamingState roaming_state_{RoamingState::IDLE};
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
bool is_high_performance_mode_{false};

View File

@@ -137,11 +137,6 @@ void WiFiComponent::wifi_pre_setup_() {
get_mac_address_raw(mac);
set_mac_address(mac);
}
esp_err_t err = esp_netif_init();
if (err != ERR_OK) {
ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err));
return;
}
s_wifi_event_group = xEventGroupCreate();
if (s_wifi_event_group == nullptr) {
ESP_LOGE(TAG, "xEventGroupCreate failed");
@@ -153,11 +148,7 @@ void WiFiComponent::wifi_pre_setup_() {
ESP_LOGE(TAG, "xQueueCreate failed");
return;
}
err = esp_event_loop_create_default();
if (err != ERR_OK) {
ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err));
return;
}
esp_err_t err;
esp_event_handler_instance_t instance_wifi_id, instance_ip_id;
err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id);
if (err != ERR_OK) {

View File

@@ -86,6 +86,14 @@ enum class LTWiFiSTAState : uint8_t {
static LTWiFiSTAState s_sta_state = LTWiFiSTAState::IDLE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Count of ignored disconnect events during connection - too many indicates real failure
static uint8_t s_ignored_disconnect_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Threshold for ignored disconnect events before treating as connection failure
// LibreTiny sends spurious "Association Leave" events, but more than this many
// indicates the connection is failing repeatedly. Value of 3 balances fast failure
// detection with tolerance for occasional spurious events on successful connections.
static constexpr uint8_t IGNORED_DISCONNECT_THRESHOLD = 3;
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
uint8_t current_mode = WiFi.getMode();
bool current_sta = current_mode & 0b01;
@@ -201,8 +209,9 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
this->wifi_apply_hostname_();
// Reset state machine before connecting
// Reset state machine and disconnect counter before connecting
s_sta_state = LTWiFiSTAState::CONNECTING;
s_ignored_disconnect_count = 0;
WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel(), // 0 = auto
@@ -474,10 +483,22 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
// causing wifi_sta_connect_status_() to return an error. The main loop would then
// call retry_connect(), aborting a connection that may succeed moments later.
// Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed.
// However, if we get too many of these events (IGNORED_DISCONNECT_THRESHOLD), treat it
// as a real connection failure to avoid waiting the full timeout for a failing connection.
if (it.ssid_len == 0 && s_sta_state == LTWiFiSTAState::CONNECTING && it.reason != WIFI_REASON_NO_AP_FOUND) {
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)",
get_disconnect_reason_str(it.reason));
break;
s_ignored_disconnect_count++;
if (s_ignored_disconnect_count >= IGNORED_DISCONNECT_THRESHOLD) {
ESP_LOGW(TAG, "Too many disconnect events (%u) while connecting, treating as failure (reason=%s)",
s_ignored_disconnect_count, get_disconnect_reason_str(it.reason));
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
WiFi.disconnect();
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)",
get_disconnect_reason_str(it.reason), s_ignored_disconnect_count);
break;
}
}
if (it.reason == WIFI_REASON_NO_AP_FOUND) {

View File

@@ -215,8 +215,13 @@ void Application::loop() {
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
// Suggest optimization for chips that don't need the PSRAM cache workaround
if (chip_info.revision >= 300) {
#ifdef USE_PSRAM
ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100,
chip_info.revision % 100);
#else
ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100,
chip_info.revision % 100);
#endif
}
#endif
#endif

View File

@@ -76,6 +76,7 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
def validate_hostname(config):
# Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h
max_length = 31
if config[CONF_NAME_ADD_MAC_SUFFIX]:
max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used
@@ -183,17 +184,24 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ:
else:
_compile_process_limit_default = cv.UNDEFINED
# Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h
FRIENDLY_NAME_MAX_LEN = 120
AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
}
)
DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Device),
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
)
@@ -207,8 +215,9 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.valid_name,
# Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
cv.string_no_slash, cv.Length(max=120)
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),

View File

@@ -13,7 +13,17 @@
namespace esphome {
// Maximum size for object_id buffer (friendly_name max ~120 + margin)
// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py
static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
// Maximum friendly name length for entities and sub-devices - keep in sync with FRIENDLY_NAME_MAX_LEN in
// esphome/core/config.py
static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120;
// Maximum domain length (longest: "alarm_control_panel" = 19)
static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20;
// Maximum size for object_id buffer (friendly_name + null + margin)
static constexpr size_t OBJECT_ID_MAX_LEN = 128;
enum EntityCategory : uint8_t {

View File

@@ -10,9 +10,11 @@
#define ESPHOME_PGM_P PGM_P
#define ESPHOME_strncpy_P strncpy_P
#define ESPHOME_strncat_P strncat_P
#define ESPHOME_strcasecmp_P strcasecmp_P
#else
#define ESPHOME_F(string_literal) (string_literal)
#define ESPHOME_PGM_P const char *
#define ESPHOME_strncpy_P strncpy
#define ESPHOME_strncat_P strncat
#define ESPHOME_strcasecmp_P strcasecmp
#endif

View File

@@ -67,14 +67,14 @@ class CoroPriority(enum.IntEnum):
# Examples: esp32, esp8266, rp2040
PLATFORM = 1000
# Network infrastructure setup
# Examples: network (201)
NETWORK = 201
# Network transport layer
# Examples: async_tcp (200)
NETWORK_TRANSPORT = 200
# Network infrastructure setup - must run after CORE which adds 'using namespace esphome;'
# Examples: network (99)
NETWORK = 99
# Core system components
# Examples: esphome core, most entity base components (cover, update, datetime,
# valve, alarm_control_panel, lock, event, binary_sensor, button, climate, fan,

View File

@@ -212,7 +212,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino]
extends = common:arduino
platform = libretiny@1.9.1
platform = libretiny@1.9.2
framework = arduino
lib_compat_mode = soft
lib_deps =

View File

@@ -11,7 +11,7 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
esphome-dashboard==20260110.0
aioesphomeapi==43.10.1
zeroconf==0.148.0
puremagic==1.30

View File

@@ -1,6 +1,6 @@
pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.10 # also change in .pre-commit-config.yaml when updating
ruff==0.14.11 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -9,8 +9,14 @@ from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.image import CONF_TRANSPARENCY, CONFIG_SCHEMA
from esphome.components.image import (
CONF_TRANSPARENCY,
CONFIG_SCHEMA,
get_all_image_metadata,
get_image_metadata,
)
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.core import CORE
@pytest.mark.parametrize(
@@ -235,3 +241,112 @@ def test_image_generation(
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
in main_cpp
)
def test_image_to_code_defines_and_core_data(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that to_code() sets USE_IMAGE define and stores image metadata."""
# Generate the main cpp which will call to_code
generate_main(component_config_path("image_test.yaml"))
# Verify USE_IMAGE define was added
assert any(d.name == "USE_IMAGE" for d in CORE.defines), (
"USE_IMAGE define should be set when images are configured"
)
# Use the public API to get image metadata
# The test config has an image with id 'cat_img'
cat_img_metadata = get_image_metadata("cat_img")
assert cat_img_metadata is not None, (
"Image metadata should be retrievable via get_image_metadata()"
)
# Verify the metadata has the expected attributes
assert hasattr(cat_img_metadata, "width"), "Metadata should have width attribute"
assert hasattr(cat_img_metadata, "height"), "Metadata should have height attribute"
assert hasattr(cat_img_metadata, "image_type"), (
"Metadata should have image_type attribute"
)
assert hasattr(cat_img_metadata, "transparency"), (
"Metadata should have transparency attribute"
)
# Verify the values are correct (from the test image)
assert cat_img_metadata.width == 32, "Width should be 32"
assert cat_img_metadata.height == 24, "Height should be 24"
assert cat_img_metadata.image_type == "RGB565", "Type should be RGB565"
assert cat_img_metadata.transparency == "opaque", "Transparency should be opaque"
def test_image_to_code_multiple_images(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that to_code() stores metadata for multiple images."""
generate_main(component_config_path("image_test.yaml"))
# Use the public API to get all image metadata
all_metadata = get_all_image_metadata()
assert isinstance(all_metadata, dict), (
"get_all_image_metadata() should return a dictionary"
)
# Verify that at least one image is present
assert len(all_metadata) > 0, "Should have at least one image metadata entry"
# Each image ID should map to an ImageMetaData object
for image_id, metadata in all_metadata.items():
assert isinstance(image_id, str), "Image IDs should be strings"
# Verify it's an ImageMetaData object with all required attributes
assert hasattr(metadata, "width"), (
f"Metadata for '{image_id}' should have width"
)
assert hasattr(metadata, "height"), (
f"Metadata for '{image_id}' should have height"
)
assert hasattr(metadata, "image_type"), (
f"Metadata for '{image_id}' should have image_type"
)
assert hasattr(metadata, "transparency"), (
f"Metadata for '{image_id}' should have transparency"
)
# Verify values are valid
assert isinstance(metadata.width, int), (
f"Width for '{image_id}' should be an integer"
)
assert isinstance(metadata.height, int), (
f"Height for '{image_id}' should be an integer"
)
assert isinstance(metadata.image_type, str), (
f"Type for '{image_id}' should be a string"
)
assert isinstance(metadata.transparency, str), (
f"Transparency for '{image_id}' should be a string"
)
assert metadata.width > 0, f"Width for '{image_id}' should be positive"
assert metadata.height > 0, f"Height for '{image_id}' should be positive"
def test_get_image_metadata_nonexistent() -> None:
"""Test that get_image_metadata returns None for non-existent image IDs."""
# This should return None when no images are configured or ID doesn't exist
metadata = get_image_metadata("nonexistent_image_id")
assert metadata is None, (
"get_image_metadata should return None for non-existent IDs"
)
def test_get_all_image_metadata_empty() -> None:
"""Test that get_all_image_metadata returns empty dict when no images configured."""
# When CORE hasn't been initialized with images, should return empty dict
all_metadata = get_all_image_metadata()
assert isinstance(all_metadata, dict), (
"get_all_image_metadata should always return a dict"
)
# Length could be 0 or more depending on what's in CORE at test time

View File

@@ -1,5 +0,0 @@
substitutions:
gate_pin: GPIO4
zero_cross_pin: GPIO5
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
gate_pin: GPIO18
zero_cross_pin: GPIO19
<<: !include common.yaml

View File

@@ -6,6 +6,7 @@ display:
panel_height: 32
double_buffer: true
brightness: 128
gamma_correct: gamma_2_2
pages:
- id: page1
lambda: |-

View File

@@ -5,6 +5,7 @@ display:
panel_height: 32
double_buffer: true
brightness: 128
gamma_correct: cie1931
r1_pin: GPIO42
g1_pin: GPIO41
b1_pin: GPIO40

View File

@@ -7,6 +7,7 @@ display:
platform: ili9xxx
id: main_lcd
model: ili9342
data_rate: 31.25MHz
cs_pin: 20
dc_pin: 21
reset_pin: 22

View File

@@ -42,4 +42,3 @@ status_led:
# Include API at priority 40
api:
password: "apipassword"

Some files were not shown because too many files have changed in this diff Show More