1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-17 15:26:01 +00:00

Compare commits

...

65 Commits

Author SHA1 Message Date
Jesse Hills
d6b222c370 Merge pull request #9933 from esphome/bump-2025.7.4
2025.7.4
2025-07-28 19:33:19 +12:00
Jesse Hills
573dad1736 Bump version to 2025.7.4 2025-07-28 15:55:07 +12:00
Jimmy Hedman
3a6cc0ea3d Fail with old lerp (#9914)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-28 15:55:07 +12:00
cryptk
2f9475a927 Add seed flag when running setup with uv present (#9932) 2025-07-28 15:55:07 +12:00
Jesse Hills
8dce7b0905 [logger] Don't allow `logger.log actions without configuring the logger` (#9821) 2025-07-28 15:55:07 +12:00
Eric Hoffmann
8b0ad3072f fix: non-optional x/y target calculation for ld2450 (#9849) 2025-07-28 15:55:07 +12:00
Clyde Stubbs
93028a4d90 [gt911] i2c fixes (#9822) 2025-07-28 15:55:07 +12:00
Jonathan Swoboda
c9793f3741 [remote_receiver] Fix idle validation (#9819) 2025-07-28 15:55:07 +12:00
Jesse Hills
2b5cceda58 Merge pull request #9796 from esphome/bump-2025.7.3
2025.7.3
2025-07-23 08:09:40 +12:00
Jesse Hills
dc26ed9c46 Bump version to 2025.7.3 2025-07-23 00:34:13 +12:00
Keith Burzinski
8674012406 [bme680_bsec] Add suggested alternate when using IDF (#9785) 2025-07-23 00:34:12 +12:00
Keith Burzinski
ae12deff87 [neopixelbus] Add suggested alternate when using IDF (#9783) 2025-07-23 00:34:12 +12:00
Keith Burzinski
cb6acfe24b [fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) 2025-07-23 00:34:12 +12:00
J. Nick Koston
fc8c5a7438 [core] Process pending loop enables during setup blocking phase (#9787) 2025-07-23 00:34:06 +12:00
Keith Burzinski
f8777d3b66 [config_validation] Add support for suggesting alternate component/platform (#9757)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-23 00:30:36 +12:00
Jesse Hills
76e75f4cdc [tuya] Update use of fan_schema (#9762) 2025-07-23 00:29:40 +12:00
Jonathan Swoboda
896d7f8f76 [esp32_touch] Fix setup mode in v1 driver (#9725) 2025-07-23 00:29:40 +12:00
JonasB2497
d92ee563f2 [sdl][mipi_spi] Respect clipping when drawing (#9722)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-23 00:29:34 +12:00
tmpeh
d6ff790823 Fix format string error in ota_web_server.cpp (#9711) 2025-07-23 00:25:51 +12:00
J. Nick Koston
7ac60c15dc [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) 2025-07-23 00:25:51 +12:00
Jesse Hills
6fe4ffa0cf Merge pull request #9691 from esphome/bump-2025.7.2
2025.7.2
2025-07-19 12:04:51 +12:00
Jesse Hills
576ce7ee35 Bump version to 2025.7.2 2025-07-19 09:56:08 +12:00
J. Nick Koston
8a45e877bb [gpio] Disable interrupt mode by default for LibreTiny platforms (#9687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:56:08 +12:00
Kevin Ahrendt
84607c1255 [voice_assistant] Use media player callbacks to track TTS response status (#9670) 2025-07-19 09:56:01 +12:00
Kevin Ahrendt
8664ec0a3b [speaker] Media player's pipeline properly returns playing state near end of file (#9668) 2025-07-19 09:54:15 +12:00
J. Nick Koston
32d8c60a0b Fix AsyncTCP version mismatch between platformio.ini and async_tcp component (#9676) 2025-07-19 09:54:00 +12:00
Jesse Hills
976a1e27b4 [lvgl] Prevent keyerror on min/max value widgets with no default (#9660) 2025-07-19 09:53:47 +12:00
J. Nick Koston
cc2c1b1d89 [libretiny] Remove unsupported lock-free queue and event pool implementations (#9653) 2025-07-19 09:53:47 +12:00
Clyde Stubbs
85495d38b7 [lvgl] Fix meter rotation (#9605)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-19 09:53:47 +12:00
J. Nick Koston
84a77ee427 [scheduler] Fix DelayAction cancellation in restart mode scripts (#9646) 2025-07-19 09:53:47 +12:00
@RubenKelevra
11a4115e30 esp32_camera: deprecate i2c_pins; throw error if combined with i2c: block (#9615) 2025-07-19 09:53:47 +12:00
Samuel Sieb
121ed687f3 [logger] fix on_message (#9642)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-19 09:53:47 +12:00
J. Nick Koston
c602f3082e [scheduler] Fix cancellation of timers with empty string names (#9641) 2025-07-19 09:53:39 +12:00
J. Nick Koston
4a43f922c6 [wireguard] Fix boot loop when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled (#9637) 2025-07-19 09:50:36 +12:00
J. Nick Koston
21e66b76e4 [api] Fix compilation error with char* lambdas in HomeAssistant services (#9638)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:50:36 +12:00
Flo
cdeed7afa7 Fix template event web_server crash (#9618) 2025-07-19 09:50:36 +12:00
Jesse Hills
1a9f02fa63 Merge pull request #9596 from esphome/bump-2025.7.1
2025.7.1
2025-07-17 21:54:35 +12:00
Jesse Hills
7ad1b039f9 Bump version to 2025.7.1 2025-07-17 19:40:03 +12:00
J. Nick Koston
e255d73c29 Fix lwIP thread safety assertion failures on ESP32 (#9570) 2025-07-17 19:39:57 +12:00
Jesse Hills
46f5c44b37 [esp32] Add missing include for helpers (#9579)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-17 19:39:39 +12:00
J. Nick Koston
9d80889bc9 Allow disabling OTA for web_server while keeping it enabled for captive_portal (#9583) 2025-07-17 19:39:39 +12:00
J. Nick Koston
08a5ba6ef1 Add helpful error message when ESP32+Arduino runs out of flash space (#9580) 2025-07-17 19:39:39 +12:00
J. Nick Koston
28128c65e5 Fix format string warnings in Web Server OTA component (#9569)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-17 19:39:39 +12:00
J. Nick Koston
efcad565ee Fix compilation error when using string lambdas with homeassistant services (#9543) 2025-07-17 19:39:39 +12:00
Vladimir Kuznetsov
cd987feb5b [lvgl]: fix missing await keyword in meter tick_style width processing (#9538) 2025-07-17 19:39:12 +12:00
Jesse Hills
5707389faa Merge pull request #9534 from esphome/bump-2025.7.0
2025.7.0
2025-07-16 20:46:26 +12:00
Jesse Hills
3f78db5c63 Bump version to 2025.7.0 2025-07-16 12:31:13 +12:00
Jesse Hills
de0656a188 Merge pull request #9532 from esphome/bump-2025.7.0b5
2025.7.0b5
2025-07-16 11:58:12 +12:00
Jesse Hills
90a16ffa89 Bump version to 2025.7.0b5 2025-07-16 10:45:20 +12:00
Samuel Sieb
4182076f64 [as3935_spi] remove unnecessary includes (#9528) 2025-07-16 10:45:19 +12:00
J. Nick Koston
8c8c08d40c Fix timing overflow when components disable themselves during loop (#9529)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-16 10:45:19 +12:00
Jesse Hills
18e2f41424 Merge pull request #9518 from esphome/bump-2025.7.0b4
2025.7.0b4
2025-07-16 07:34:42 +12:00
Christian Glombek
bd0fe34b14 [ms8607] Fix humidity calc (#9499) 2025-07-16 07:33:49 +12:00
Jesse Hills
37982290f7 Bump version to 2025.7.0b4 2025-07-15 23:35:55 +12:00
Jesse Hills
02b7db7311 [component] Fix `is_ready` flag when loop disabled (#9501)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-15 23:35:55 +12:00
Clyde Stubbs
9bc3ff5f53 [core] Don't issue -Wno-volatile for host platform (#9511) 2025-07-15 23:35:55 +12:00
J. Nick Koston
786cb7ded5 Add missing clang-tidy NOLINT comments for ArduinoJson v7 in IDF webserver (#9508) 2025-07-15 23:35:55 +12:00
Keith Burzinski
7f01c25782 [servo] Fix `lerp` (#9507) 2025-07-15 23:35:55 +12:00
Keith Burzinski
321f2f87b0 [opentherm.output] Fix `lerp` (#9506) 2025-07-15 23:35:55 +12:00
Clyde Stubbs
11a051401f [captive_portal] Add test case for libretiny (#9457)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-15 23:35:55 +12:00
J. Nick Koston
6148dd7e41 Fix LibreTiny compilation error by updating ESPAsyncWebServer and dependencies (#9492)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 23:35:55 +12:00
skyegecko
42b6939e90 [fan] Do not save state for fan if configured as NO_RESTORE (#9472) 2025-07-15 23:35:55 +12:00
Kevin Ahrendt
35b3f75f7c [json] Bump ArduinoJson library to 7.4.2 (#8857)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 23:35:55 +12:00
Clyde Stubbs
78e8001aa8 [online_image] Support byte_order (#9502) 2025-07-15 23:35:55 +12:00
J. Nick Koston
84fc6ff71a Suppress spurious volatile and Python syntax warnings during builds (#9488) 2025-07-15 23:35:55 +12:00
103 changed files with 1429 additions and 359 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.7.0b3
PROJECT_NUMBER = 2025.7.4
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -11,6 +11,18 @@ namespace esphome {
namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {
return val ? std::string(val) : std::string();
} // For lambdas returning char* (e.g., itoa)
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
static std::string value_to_string(const std::string &val) { return val; }
static std::string value_to_string(std::string &&val) { return std::move(val); }
public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
@@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
TemplatableStringValue(F f)
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
};
template<typename... Ts> class TemplatableKeyValuePair {

View File

@@ -3,8 +3,6 @@
#include "esphome/core/component.h"
#include "esphome/components/as3935/as3935.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace as3935_spi {

View File

@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
elif CORE.is_esp8266:
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import esp32, i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
@@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
): cv.positive_time_period_minutes,
}
).extend(i2c.i2c_device_schema(0x76)),
cv.only_with_arduino,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"bme68x_bsec2_i2c",
"sensor/bme68x_bsec2",
)
},
),
cv.Any(
cv.only_on_esp8266,
cv.All(

View File

@@ -4,6 +4,7 @@
#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/helpers.h"
#include <lwip/igmp.h>
#include <lwip/init.h>
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
ip4_addr_t multicast_addr =
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
err_t err;
{
LwIPLock lock;
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
}
if (err) {
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
@@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
if (listen_method_ == E131_MULTICAST) {
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
LwIPLock lock;
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
}

View File

@@ -1,4 +1,5 @@
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
#include "lwip/priv/tcpip_priv.h"
#endif
LwIPLock::LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
// its internal state. Any thread can take this lock to safely access lwIP APIs.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
// already holds the lwIP core lock. This prevents recursive locking attempts and
// allows nested LwIPLock instances to work correctly.
//
// If we don't already hold the lock, acquire it. This will block until the lock
// is available if another thread currently holds it.
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
LOCK_TCPIP_CORE();
}
#endif
}
LwIPLock::~LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// Only release the lwIP core lock if this thread currently holds it.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
// ownership tracking. It returns true only if the current thread is registered
// as the lock holder.
//
// This check is essential because:
// 1. We may not have acquired the lock in the constructor (if we already held it)
// 2. The lock might have been released by other means between constructor and destructor
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
UNLOCK_TCPIP_CORE();
}
#endif
}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
@@ -8,6 +10,7 @@ from esphome.const import (
CONF_CONTRAST,
CONF_DATA_PINS,
CONF_FREQUENCY,
CONF_I2C,
CONF_I2C_ID,
CONF_ID,
CONF_PIN,
@@ -20,6 +23,9 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)
def _final_validate(config):
if CONF_I2C_PINS not in config:
return
fconf = fv.full_config.get()
if fconf.get(CONF_I2C):
raise cv.Invalid(
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
)
_LOGGER.warning(
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
)
FINAL_VALIDATE_SCHEMA = _final_validate
SETTERS = {
# pin assignment
CONF_DATA_PINS: "set_data_pins",

View File

@@ -16,6 +16,8 @@ namespace esp32_touch {
static const char *const TAG = "esp32_touch";
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
void ESP32TouchComponent::setup() {
// Create queue for touch events
// Queue size calculation: children * 4 allows for burst scenarios where ISR
@@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() {
// Configure each touch pad
for (auto *child : this->children_) {
touch_pad_config(child->get_touch_pad(), child->get_threshold());
if (this->setup_mode_) {
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
} else {
touch_pad_config(child->get_touch_pad(), child->get_threshold());
}
}
// Register ISR handler
@@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), event.value, child->get_threshold());
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
}
break; // Exit inner loop after processing matching pad
}
@@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched
@@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
}
// Skip pads that arent in the trigger mask
bool is_touched = (mask >> pad) & 1;
if (!is_touched) {
if (((mask >> pad) & 1) == 0) {
continue;
}
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple)

View File

@@ -22,6 +22,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
// ESP8266 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac);
}

View File

@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1 = dns_getserver(0);
const ip_addr_t *dns_ip2 = dns_getserver(1);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"

View File

@@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
return {};
}
void Fan::save_state_() {
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
return;
}
FanRestoreState state{};
state.state = this->state;
state.oscillating = this->oscillating;

View File

@@ -2,7 +2,13 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import fastled_base
import esphome.config_validation as cv
from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER
from esphome.const import (
CONF_CHIPSET,
CONF_NUM_LEDS,
CONF_PIN,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"]
@@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
}
),
_validate,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0),
max_version=True,
extra_message="Please see note on documentation for FastLED",
),
_validate,
)

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_NUM_LEDS,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"]
@@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DATA_RATE): cv.frequency,
}
),
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"spi_led_strip",
"light/spi_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0),

View File

@@ -4,7 +4,13 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_NAME,
CONF_NUMBER,
CONF_PIN,
)
from esphome.core import CORE
from .. import gpio_ns
@@ -29,7 +35,21 @@ CONFIG_SCHEMA = (
.extend(
{
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
# due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior.
cv.SplitDefault(
CONF_USE_INTERRUPT,
bk72xx=False,
esp32=True,
esp8266=True,
host=True,
ln882x=False,
nrf52=True,
rp2040=True,
rtl87xx=False,
): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True
),
@@ -62,6 +82,18 @@ async def to_code(config):
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@@ -8,6 +8,8 @@ namespace gt911 {
static const char *const TAG = "gt911.touchscreen";
static const uint8_t PRIMARY_ADDRESS = 0x5D; // default I2C address for GT911
static const uint8_t SECONDARY_ADDRESS = 0x14; // secondary I2C address for GT911
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
@@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \
ESP_LOGE(TAG, "Failed to communicate!"); \
this->status_set_warning(); \
this->status_set_warning("Communication failure"); \
return; \
}
@@ -30,31 +31,31 @@ void GT911Touchscreen::setup() {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
if (this->interrupt_pin_ != nullptr) {
// The interrupt pin is used as an input during reset to select the I2C address.
// temporarily set the interrupt pin to output to control address selection
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->interrupt_pin_->setup();
this->interrupt_pin_->digital_write(false);
}
delay(2);
this->reset_pin_->digital_write(true);
delay(50); // NOLINT
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
}
}
if (this->interrupt_pin_ != nullptr) {
// set pre-configured input mode
this->interrupt_pin_->setup();
}
// check the configuration of the int line.
uint8_t data[4];
err = this->write(GET_SWITCHES, 2);
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
this->address_ = SECONDARY_ADDRESS;
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
}
if (err == i2c::ERROR_OK) {
err = this->read(data, 1);
if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]);
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
if (this->interrupt_pin_ != nullptr) {
// datasheet says NOT to use pullup/down on the int line.
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
}
@@ -63,7 +64,7 @@ void GT911Touchscreen::setup() {
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
// no calibration? Attempt to read the max values from the touchscreen.
if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2);
err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES));
if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) {
@@ -75,15 +76,12 @@ void GT911Touchscreen::setup() {
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!");
this->mark_failed();
this->mark_failed("Failed to read calibration");
return;
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!");
this->mark_failed();
return;
this->mark_failed("Failed to communicate");
}
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
@@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() {
uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false);
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE));
ERROR_CHECK(err);
err = this->read(&touch_state, 1);
ERROR_CHECK(err);
@@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() {
return;
}
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES));
ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
@@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
} // namespace gt911

View File

@@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
@@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
this_update->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) {
if (!build["chipFamily"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) {
if (!build["ota"].is<JsonObject>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
auto ota = build["ota"];
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
JsonObject ota = build["ota"].as<JsonObject>();
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
this_update->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary"))
if (ota["summary"].is<const char *>())
this_update->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url"))
if (ota["release_url"].is<const char *>())
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
return true;

View File

@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0)
async def to_code(config):
cg.add_library("bblanchon/ArduinoJson", "6.18.5")
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON")
cg.add_global(json_ns.using)

View File

@@ -1,83 +1,76 @@
#include "json_util.h"
#include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
namespace esphome {
namespace json {
static const char *const TAG = "json";
static std::vector<char> global_json_build_buffer; // NOLINT
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
// Build an allocator for the JSON Library using the RAMAllocator class
struct SpiRamAllocator : ArduinoJson::Allocator {
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
void deallocate(void *pointer) override {
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
// RAMAllocator::deallocate implementation just calls free() regardless of whether
// the memory was allocated with heap_caps_malloc or malloc.
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
// and routes free() to the appropriate heap.
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
}
void *reallocate(void *ptr, size_t new_size) override {
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
}
protected:
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
};
std::string build_json(const json_build_t &f) {
// Here we are allocating up to 5kb of memory,
// with the heap size minus 2kb to be safe if less than 5kb
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) 512);
while (true) {
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
request_size, free_heap);
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
if (request_size == free_heap) {
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
free_heap);
return "{}";
}
request_size = std::min(request_size * 2, free_heap);
continue;
}
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
}
std::string output;
serializeJson(json_document, output);
return output;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
bool parse_json(const std::string &data, const json_parse_t &f) {
// Here we are allocating 1.5 times the data size,
// with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
while (true) {
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
free_heap);
return false;
}
DeserializationError err = deserializeJson(json_document, data);
json_document.shrinkToFit();
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return false;
}
DeserializationError err = deserializeJson(json_document, data);
JsonObject root = json_document.as<JsonObject>();
JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) {
return f(root);
} else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return false;
}
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
}
};
if (err == DeserializationError::Ok) {
return f(root);
} else if (err == DeserializationError::NoMemory) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return false;
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
} // namespace json

View File

@@ -477,10 +477,11 @@ void LD2450Component::handle_periodic_data_() {
// X
start = TARGET_X + index * 8;
is_moving = false;
// tx is used for further calculations, so always needs to be populated
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val;
sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) {
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val;
if (this->cached_target_data_[index].x != val) {
sx->publish_state(val);
this->cached_target_data_[index].x = val;
@@ -488,10 +489,11 @@ void LD2450Component::handle_periodic_data_() {
}
// Y
start = TARGET_Y + index * 8;
// ty is used for further calculations, so always needs to be populated
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val;
sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) {
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val;
if (this->cached_target_data_[index].y != val) {
sy->publish_state(val);
this->cached_target_data_[index].y = val;

View File

@@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
// LibreTiny doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
WiFi.macAddress(mac);
}

View File

@@ -9,6 +9,7 @@ namespace light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects())
root["effect"] = state.get_effect_name();
@@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
root["brightness"] = uint8_t(values.get_brightness() * 255);
JsonObject color = root.createNestedObject("color");
JsonObject color = root["color"].to<JsonObject>();
if (values.get_color_mode() & ColorCapability::RGB) {
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
@@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
}
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
if (root.containsKey("state")) {
if (root["state"].is<const char *>()) {
auto val = parse_on_off(root["state"]);
switch (val) {
case PARSE_ON:
@@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
}
}
if (root.containsKey("brightness")) {
if (root["brightness"].is<uint8_t>()) {
call.set_brightness(float(root["brightness"]) / 255.0f);
}
if (root.containsKey("color")) {
if (root["color"].is<JsonObject>()) {
JsonObject color = root["color"];
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
float max_rgb = 0.0f;
if (color.containsKey("r")) {
if (color["r"].is<uint8_t>()) {
float r = float(color["r"]) / 255.0f;
max_rgb = fmaxf(max_rgb, r);
call.set_red(r);
}
if (color.containsKey("g")) {
if (color["g"].is<uint8_t>()) {
float g = float(color["g"]) / 255.0f;
max_rgb = fmaxf(max_rgb, g);
call.set_green(g);
}
if (color.containsKey("b")) {
if (color["b"].is<uint8_t>()) {
float b = float(color["b"]) / 255.0f;
max_rgb = fmaxf(max_rgb, b);
call.set_blue(b);
}
if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) {
if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) {
call.set_color_brightness(max_rgb);
}
if (color.containsKey("c")) {
if (color["c"].is<uint8_t>()) {
call.set_cold_white(float(color["c"]) / 255.0f);
}
if (color.containsKey("w")) {
if (color["w"].is<uint8_t>()) {
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
// white channel in RGBWW.
if (color.containsKey("c")) {
if (color["c"].is<uint8_t>()) {
call.set_warm_white(float(color["w"]) / 255.0f);
} else {
call.set_white(float(color["w"]) / 255.0f);
@@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
}
}
if (root.containsKey("white_value")) { // legacy API
if (root["white_value"].is<uint8_t>()) { // legacy API
call.set_white(float(root["white_value"]) / 255.0f);
}
if (root.containsKey("color_temp")) {
if (root["color_temp"].is<uint16_t>()) {
call.set_color_temperature(float(root["color_temp"]));
}
}
@@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
LightJSONSchema::parse_color_json(state, call, root);
if (root.containsKey("flash")) {
if (root["flash"].is<uint32_t>()) {
auto length = uint32_t(float(root["flash"]) * 1000);
call.set_flash_length(length);
}
if (root.containsKey("transition")) {
if (root["transition"].is<uint16_t>()) {
auto length = uint32_t(float(root["transition"]) * 1000);
call.set_transition_length(length);
}
if (root.containsKey("effect")) {
if (root["effect"].is<const char *>()) {
const char *effect = root["effect"];
call.set_effect(effect);
}

View File

@@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value):
Logger = logger_ns.class_("Logger", cg.Component)
LoggerMessageTrigger = logger_ns.class_(
"LoggerMessageTrigger",
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr),
)
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
@@ -368,7 +368,7 @@ async def to_code(config):
await automation.build_automation(
trigger,
[
(cg.int_, "level"),
(cg.uint8, "level"),
(cg.const_char_ptr, "tag"),
(cg.const_char_ptr, "message"),
],
@@ -400,6 +400,7 @@ CONF_LOGGER_LOG = "logger.log"
LOGGER_LOG_ACTION_SCHEMA = cv.All(
cv.maybe_simple_value(
{
cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
cv.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(

View File

@@ -192,7 +192,7 @@ class WidgetType:
class NumberType(WidgetType):
def get_max(self, config: dict):
return int(config[CONF_MAX_VALUE] or 100)
return int(config.get(CONF_MAX_VALUE, 100))
def get_min(self, config: dict):
return int(config[CONF_MIN_VALUE] or 0)
return int(config.get(CONF_MIN_VALUE, 0))

View File

@@ -14,6 +14,7 @@ from esphome.const import (
CONF_VALUE,
CONF_WIDTH,
)
from esphome.cpp_generator import IntLiteral
from ..automation import action_to_code
from ..defines import (
@@ -188,6 +189,8 @@ class MeterType(WidgetType):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf:
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
if isinstance(rotation, IntLiteral):
rotation = int(str(rotation)) // 10
with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var:
@@ -264,7 +267,7 @@ class MeterType(WidgetType):
color_start,
color_end,
v[CONF_LOCAL],
size.process(v[CONF_WIDTH]),
await size.process(v[CONF_WIDTH]),
),
)
if t == CONF_IMAGE:

View File

@@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
}
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>();
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
supported_features.add("arm_away");

View File

@@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
}
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->binary_sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
if (this->binary_sensor_->is_status_binary_sensor())

View File

@@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() {
}
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
config.state_topic = false;
if (!this->button_->get_device_class().empty())
if (!this->button_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
std::string MQTTButtonComponent::component_type() const { return "button"; }

View File

@@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
std::string topic = "esphome/discover/";
topic.append(App.get_name());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
this->publish_json(
topic,
[](JsonObject root) {
@@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
#endif
},
2, this->discovery_info_.retain);
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
void MQTTClientComponent::dump_config() {
@@ -191,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
this->dns_resolve_error_ = false;
this->dns_resolved_ = false;
ip_addr_t addr;
err_t err;
{
LwIPLock lock;
#if USE_NETWORK_IPV6
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
#else
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
this, LWIP_DNS_ADDRTYPE_IPV4);
#endif /* USE_NETWORK_IPV6 */
}
switch (err) {
case ERR_OK: {
// Got IP immediately

View File

@@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
using namespace esphome::climate;
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits();
// current_temperature_topic
if (traits.get_supports_current_temperature()) {
@@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// mode_state_topic
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
// modes
JsonArray modes = root.createNestedArray(MQTT_MODES);
JsonArray modes = root[MQTT_MODES].to<JsonArray>();
// sort array for nice UI in HA
if (traits.supports_mode(CLIMATE_MODE_AUTO))
modes.add("auto");
@@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// preset_mode_state_topic
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
// presets
JsonArray presets = root.createNestedArray("preset_modes");
JsonArray presets = root["preset_modes"].to<JsonArray>();
if (traits.supports_preset(CLIMATE_PRESET_HOME))
presets.add("home");
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
@@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// fan_mode_state_topic
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
// fan_modes
JsonArray fan_modes = root.createNestedArray("fan_modes");
JsonArray fan_modes = root["fan_modes"].to<JsonArray>();
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
fan_modes.add("on");
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
@@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// swing_mode_state_topic
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
// swing_modes
JsonArray swing_modes = root.createNestedArray("swing_modes");
JsonArray swing_modes = root["swing_modes"].to<JsonArray>();
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
swing_modes.add("off");
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
@@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
config.state_topic = false;
config.command_topic = false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits();

View File

@@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() {
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return global_mqtt_client->publish_json(
this->get_discovery_topic_(discovery_info),
[this](JsonObject root) {
@@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
}
std::string node_area = App.get_area();
JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
const auto mac = get_mac_address();
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
@@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
},
this->qos_, discovery_info.retain);
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
uint8_t MQTTComponent::get_qos() const { return this->qos_; }

View File

@@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() {
}
}
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->cover_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();

View File

@@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
void MQTTDateComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->date_->make_call();
if (root.containsKey("year")) {
if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]);
}
if (root.containsKey("month")) {
if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]);
}
if (root.containsKey("day")) {
if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]);
}
call.perform();
@@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
}
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["year"] = year;
root["month"] = month;
root["day"] = day;

View File

@@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
void MQTTDateTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->datetime_->make_call();
if (root.containsKey("year")) {
if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]);
}
if (root.containsKey("month")) {
if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]);
}
if (root.containsKey("day")) {
if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]);
}
if (root.containsKey("hour")) {
if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]);
}
if (root.containsKey("minute")) {
if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]);
}
if (root.containsKey("second")) {
if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]);
}
call.perform();
@@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() {
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["year"] = year;
root["month"] = month;
root["day"] = day;

View File

@@ -16,7 +16,8 @@ using namespace esphome::event;
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>();
for (const auto &event_type : this->event_->get_event_types())
event_types.add(event_type);
@@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
}
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
return this->publish_json(this->get_state_topic_(),
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; });
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});
}
std::string MQTTEventComponent::component_type() const { return "event"; }

View File

@@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() {
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->state_->get_traits().supports_direction()) {
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();

View File

@@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(),
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); });
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});
}
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["schema"] = "json";
auto traits = this->state_->get_traits();
root[MQTT_COLOR_MODE] = true;
JsonArray color_modes = root.createNestedArray("supported_color_modes");
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray color_modes = root["supported_color_modes"].to<JsonArray>();
if (traits.supports_color_mode(ColorMode::ON_OFF))
color_modes.add("onoff");
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
@@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (this->state_->supports_effects()) {
root["effect"] = true;
JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST);
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
for (auto *effect : this->state_->get_effects())
effect_list.add(effect->get_name());
effect_list.add("None");

View File

@@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
std::string MQTTLockComponent::component_type() const { return "lock"; }
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->lock_->traits.get_assumed_state())
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->lock_->traits.get_assumed_state()) {
root[MQTT_OPTIMISTIC] = true;
}
if (this->lock_->traits.get_supports_open())
root[MQTT_PAYLOAD_OPEN] = "OPEN";
}

View File

@@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = number_->traits;
// https://www.home-assistant.io/integrations/number.mqtt/
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_MIN] = traits.get_min_value();
root[MQTT_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step();

View File

@@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = select_->traits;
// https://www.home-assistant.io/integrations/select.mqtt/
JsonArray options = root.createNestedArray(MQTT_OPTIONS);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonArray options = root[MQTT_OPTIONS].to<JsonArray>();
for (const auto &option : traits.get_options())
options.add(option);

View File

@@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty())
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
if (!this->sensor_->get_unit_of_measurement().empty())
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();

View File

@@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() {
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->switch_->assumed_state())
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (this->switch_->assumed_state()) {
root[MQTT_OPTIMISTIC] = true;
}
}
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }

View File

@@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; }
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
switch (this->text_->traits.get_mode()) {
case TEXT_MODE_TEXT:
root[MQTT_MODE] = "text";

View File

@@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty())
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
config.command_topic = false;
}
void MQTTTextSensor::setup() {

View File

@@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
void MQTTTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->time_->make_call();
if (root.containsKey("hour")) {
if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]);
}
if (root.containsKey("minute")) {
if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]);
}
if (root.containsKey("second")) {
if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]);
}
call.perform();
@@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["hour"] = hour;
root["minute"] = minute;
root["second"] = second;

View File

@@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
}
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["schema"] = "json";
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
}

View File

@@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
}
}
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->valve_->get_device_class().empty())
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->valve_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
}
auto traits = this->valve_->get_traits();
if (traits.get_is_assumed_state()) {

View File

@@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) {
// map 16 bit humidity value into range [-6%, 118%]
float const humidity_partial = double(humidity) / (1 << 16);
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial);
float const compensated_humidity_percentage =
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);

View File

@@ -15,6 +15,7 @@ from esphome.const import (
CONF_PIN,
CONF_TYPE,
CONF_VARIANT,
Framework,
)
from esphome.core import CORE
@@ -162,7 +163,15 @@ def _validate_method(value):
CONFIG_SCHEMA = cv.All(
cv.only_with_arduino,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 4, 0),
esp32_arduino=cv.Version(0, 0, 0),

View File

@@ -2,7 +2,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import (
CONF_INVERT_ALPHA,
@@ -11,6 +11,7 @@ from esphome.components.image import (
Image_,
get_image_type_enum,
get_transparency_enum,
validate_settings,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
validate_settings,
)
)
@@ -213,6 +215,7 @@ async def to_code(config):
get_image_type_enum(config[CONF_TYPE]),
transparent,
config[CONF_BUFFER_SIZE],
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])

View File

@@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) {
}
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
image::Transparency transparency, uint32_t download_buffer_size)
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
: Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr),
download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size),
format_(format),
fixed_width_(width),
fixed_height_(height) {
fixed_height_(height),
is_big_endian_(is_big_endian) {
this->set_url(url);
}
@@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
break;
}
case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
@@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
if (this->is_big_endian_) {
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
} else {
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
}
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w;
}

View File

@@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent,
* @param buffer_size Size of the buffer used to download the image.
*/
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
image::Transparency transparency, uint32_t buffer_size);
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
@@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent,
const int fixed_width_;
/** height requested on configuration, or 0 if non specified. */
const int fixed_height_;
/**
* Whether the image is stored in big-endian format.
* This is used to determine how to store 16 bit colors in the buffer.
*/
bool is_big_endian_;
/**
* Actual width of the current image. If fixed_width_ is specified,
* this will be equal to it; otherwise it will be set once the decoding

View File

@@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) {
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
this->state = state < 0.003 && this->zero_means_zero_
? 0.0
: clamp(lerp(state, min_value_, max_value_), min_value_, max_value_);
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
this->has_state_ = true;
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
}

View File

@@ -60,6 +60,20 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
)
def validate_config(config):
if CORE.is_esp32:
variant = esp32.get_esp32_variant()
if variant in (esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S2):
max_idle = 65535
else:
max_idle = 32767
if CONF_CLOCK_RESOLUTION in config:
max_idle = int(max_idle * 1000000 / config[CONF_CLOCK_RESOLUTION])
if config[CONF_IDLE].total_microseconds > max_idle:
raise cv.Invalid(f"config 'idle' exceeds the maximum value of {max_idle}us")
return config
def validate_tolerance(value):
if isinstance(value, dict):
return TOLERANCE_SCHEMA(value)
@@ -136,7 +150,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.boolean,
),
}
).extend(cv.COMPONENT_SCHEMA)
)
.extend(cv.COMPONENT_SCHEMA)
.add_extra(validate_config)
)

View File

@@ -86,10 +86,9 @@ void RemoteReceiverComponent::setup() {
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
uint32_t max_idle_ns = 65535u * 1000;
memset(&this->store_.config, 0, sizeof(this->store_.config));
this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
this->store_.config.signal_range_max_ns = std::min(this->idle_us_ * 1000, max_idle_ns);
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
this->store_.filter_symbols = this->filter_symbols_;
this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t);
this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_);

View File

@@ -44,6 +44,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// RP2040 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#ifdef USE_WIFI
WiFi.macAddress(mac);

View File

@@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
}
void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
SDL_Rect rect{x, y, 1, 1};
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
SDL_UpdateTexture(this->texture_, &rect, &data, 2);

View File

@@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
value = clamp(value, -1.0f, 1.0f);
float level;
if (value < 0.0) {
level = lerp(-value, this->idle_level_, this->min_level_);
level = std::lerp(this->idle_level_, this->min_level_, -value);
} else {
level = lerp(value, this->idle_level_, this->max_level_);
level = std::lerp(this->idle_level_, this->max_level_, value);
}
this->output_->set_level(level);
this->current_value_ = value;

View File

@@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() {
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
this->delete_tasks_();
if (this->hard_stop_) {
// Stop command was sent, so immediately end of the playback
// Stop command was sent, so immediately end the playback
this->speaker_->stop();
this->hard_stop_ = false;
} else {
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
}
}
this->is_playing_ = false;
return AudioPipelineState::STOPPED;
if (!this->speaker_->is_running()) {
return AudioPipelineState::STOPPED;
} else {
this->is_finishing_ = true;
}
}
if (this->pause_state_) {
return AudioPipelineState::PAUSED;
}
if (this->is_finishing_) {
if (!this->speaker_->is_running()) {
this->is_finishing_ = false;
} else {
return AudioPipelineState::PLAYING;
}
}
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
// No tasks are running, so the pipeline is stopped.
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);

View File

@@ -114,6 +114,7 @@ class AudioPipeline {
bool hard_stop_{false};
bool is_playing_{false};
bool is_finishing_{false};
bool pause_state_{false};
bool task_stack_in_psram_;

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import fan
import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from .. import CONF_TUYA_ID, Tuya, tuya_ns
@@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
CONFIG_SCHEMA = cv.All(
fan.FAN_SCHEMA.extend(
fan.fan_schema(TuyaFan)
.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
@@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
).extend(cv.COMPONENT_SCHEMA),
)
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
)
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
parent = await cg.get_variable(config[CONF_TUYA_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT])
var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
await cg.register_component(var, config)
await fan.register_fan(var, config)

View File

@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
temp_ring_buffer->write((void *) data.data(), data.size());
}
});
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
this->media_player_->add_on_state_callback([this]() {
switch (this->media_player_->state) {
case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING:
if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) {
// State changed to announcing after receiving the url
this->media_player_response_state_ = MediaPlayerResponseState::PLAYING;
}
break;
default:
if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) {
// No longer announcing the TTS response
this->media_player_response_state_ = MediaPlayerResponseState::FINISHED;
}
break;
}
});
}
#endif
}
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@@ -223,6 +244,13 @@ void VoiceAssistant::loop() {
msg.wake_word_phrase = this->wake_word_;
this->wake_word_ = "";
// Reset media player state tracking
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
}
#endif
if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
ESP_LOGW(TAG, "Could not request start");
this->error_trigger_->trigger("not-connected", "Could not request start");
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
#endif
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING);
playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING);
if (playing && this->media_player_wait_for_announcement_start_) {
// Announcement has started playing, wait for it to finish
this->media_player_wait_for_announcement_start_ = false;
this->media_player_wait_for_announcement_end_ = true;
}
if (!playing && this->media_player_wait_for_announcement_end_) {
// Announcement has finished playing
this->media_player_wait_for_announcement_end_ = false;
if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
this->cancel_timeout("playing");
ESP_LOGD(TAG, "Announcement finished playing");
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
break;
case State::AWAITING_RESPONSE:
this->signal_stop_();
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
break;
case State::STREAMING_RESPONSE:
#ifdef USE_MEDIA_PLAYER
// Stop any ongoing media player announcement
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
.set_announcement(true)
.perform();
}
if (this->started_streaming_tts_) {
// Haven't reached the TTS_END stage, so send the stop signal to HA.
this->signal_stop_();
}
#endif
break;
case State::RESPONSE_FINISHED:
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (this->media_player_ != nullptr) {
for (const auto &arg : msg.data) {
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;
this->media_player_wait_for_announcement_end_ = false;
this->started_streaming_tts_ = true;
this->start_playback_timeout_();
tts_url_for_trigger = this->tts_response_url_;
this->tts_response_url_.clear(); // Reset streaming URL
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
}
}
}
@@ -713,18 +741,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
this->defer([this, url]() {
#ifdef USE_MEDIA_PLAYER
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;
this->media_player_wait_for_announcement_end_ = false;
// Start the playback timeout, as the media player state isn't immediately updated
this->start_playback_timeout_();
}
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
#endif
this->tts_end_trigger_->trigger(url);
});
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE;
this->set_state_(new_state, new_state);
if (new_state != this->state_) {
// Don't needlessly change the state. The intent progress stage may have already changed the state to streaming
// response.
this->set_state_(new_state, new_state);
}
break;
}
case api::enums::VOICE_ASSISTANT_RUN_END: {
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
this->tts_start_trigger_->trigger(msg.text);
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
if (!msg.preannounce_media_id.empty()) {
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
}
@@ -886,9 +921,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
.perform();
this->continue_conversation_ = msg.start_conversation;
this->media_player_wait_for_announcement_start_ = true;
this->media_player_wait_for_announcement_end_ = false;
// Start the playback timeout, as the media player state isn't immediately updated
this->start_playback_timeout_();
if (this->continuous_) {

View File

@@ -90,6 +90,15 @@ struct Configuration {
uint32_t max_active_wake_words;
};
#ifdef USE_MEDIA_PLAYER
enum class MediaPlayerResponseState {
IDLE,
URL_SENT,
PLAYING,
FINISHED,
};
#endif
class VoiceAssistant : public Component {
public:
VoiceAssistant();
@@ -272,8 +281,8 @@ class VoiceAssistant : public Component {
media_player::MediaPlayer *media_player_{nullptr};
std::string tts_response_url_{""};
bool started_streaming_tts_{false};
bool media_player_wait_for_announcement_start_{false};
bool media_player_wait_for_announcement_end_{false};
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
#endif
bool local_output_{false};

View File

@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
return config
def validate_ota_removed(config: ConfigType) -> ConfigType:
# Only raise error if OTA is explicitly enabled (True)
# If it's False or not specified, we can safely ignore it
if config.get(CONF_OTA):
def validate_ota(config: ConfigType) -> ConfigType:
# The OTA option only accepts False to explicitly disable OTA for web_server
# IMPORTANT: Setting ota: false ONLY affects the web_server component
# The captive_portal component will still be able to perform OTA updates
if CONF_OTA in config and config[CONF_OTA] is not False:
raise cv.Invalid(
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
f"Please use the new OTA platform structure instead:\n\n"
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
f"ota:\n"
f" - platform: web_server\n\n"
f"See https://esphome.io/components/ota for more information."
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
web_server_base.WebServerBase
),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
cv.Optional(CONF_OTA, default=False): cv.boolean,
cv.Optional(CONF_OTA): cv.boolean,
cv.Optional(CONF_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean,
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
default_url,
validate_local,
validate_sorting_groups,
validate_ota_removed,
validate_ota,
)
@@ -288,7 +289,11 @@ async def to_code(config):
cg.add(var.set_css_url(config[CONF_CSS_URL]))
cg.add(var.set_js_url(config[CONF_JS_URL]))
# OTA is now handled by the web_server OTA platform
# The CONF_OTA option is kept only for backwards compatibility validation
# The CONF_OTA option is kept to allow explicitly disabling OTA for web_server
# IMPORTANT: This ONLY affects the web_server component, NOT captive_portal
# Captive portal will still be able to perform OTA updates even when this is set
if config.get(CONF_OTA) is False:
cg.add_define("USE_WEBSERVER_OTA_DISABLED")
cg.add(var.set_expose_log(config[CONF_LOG]))
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")

View File

@@ -5,6 +5,10 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/components/captive_portal/captive_portal.h"
#endif
#ifdef USE_ARDUINO
#ifdef USE_ESP8266
#include <Updater.h>
@@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler {
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override;
bool canHandle(AsyncWebServerRequest *request) const override {
return request->url() == "/update" && request->method() == HTTP_POST;
// Check if this is an OTA update request
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
// Captive portal can still perform OTA updates - check if request is from active captive portal
// Note: global_captive_portal is the standard way components communicate in ESPHome
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
captive_portal::global_captive_portal->is_active();
#elif defined(USE_WEBSERVER_OTA_DISABLED)
// OTA disabled for web_server and no captive portal compiled in
return false;
#else
// OTA enabled for web_server
return is_ota_request;
#endif
}
// NOLINTNEXTLINE(readability-identifier-naming)
@@ -57,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
}
#ifdef USE_OTA_STATE_CALLBACK
// Report progress - use call_deferred since we're in web server task
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// Finalize
if (final) {
ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
this->ota_read_length_, request->contentLength());
// For Arduino framework, the Update library tracks expected size from firmware header

View File

@@ -268,10 +268,10 @@ std::string WebServer::get_config_json() {
return json::build_json([this](JsonObject root) {
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root["comment"] = App.get_comment();
#ifdef USE_WEBSERVER_OTA
root["ota"] = true; // web_server OTA platform is configured
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
#else
root["ota"] = false;
root["ota"] = true;
#endif
root["log"] = this->expose_log_;
root["lang"] = "en";
@@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
light::LightJSONSchema::dump_json(*obj, root);
if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("effects");
JsonArray opt = root["effects"].to<JsonArray>();
opt.add("None");
for (auto const &option : obj->get_effects()) {
opt.add(option->get_name());
@@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option");
JsonArray opt = root["option"].to<JsonArray>();
for (auto &option : obj->traits.get_options()) {
opt.add(option);
}
@@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
}
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
const auto traits = obj->get_traits();
@@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
char buf[16];
if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("modes");
JsonArray opt = root["modes"].to<JsonArray>();
for (climate::ClimateMode m : traits.get_supported_modes())
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
if (!traits.get_supported_custom_fan_modes().empty()) {
JsonArray opt = root.createNestedArray("fan_modes");
JsonArray opt = root["fan_modes"].to<JsonArray>();
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
}
if (!traits.get_supported_custom_fan_modes().empty()) {
JsonArray opt = root.createNestedArray("custom_fan_modes");
JsonArray opt = root["custom_fan_modes"].to<JsonArray>();
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
opt.add(custom_fan_mode);
}
if (traits.get_supports_swing_modes()) {
JsonArray opt = root.createNestedArray("swing_modes");
JsonArray opt = root["swing_modes"].to<JsonArray>();
for (auto swing_mode : traits.get_supported_swing_modes())
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
}
if (traits.get_supports_presets() && obj->preset.has_value()) {
JsonArray opt = root.createNestedArray("presets");
JsonArray opt = root["presets"].to<JsonArray>();
for (climate::ClimatePreset m : traits.get_supported_presets())
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
}
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
JsonArray opt = root.createNestedArray("custom_presets");
JsonArray opt = root["custom_presets"].to<JsonArray>();
for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset);
}
@@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
root["state"] = root["target_temperature"];
}
});
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
#endif
@@ -1618,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
request->send(404);
}
static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; }
static std::string get_event_type(event::Event *event) {
return (event && event->last_event_type) ? *event->last_event_type : "";
}
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source);
@@ -1635,7 +1639,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
root["event_type"] = event_type;
}
if (start_config == DETAIL_ALL) {
JsonArray event_types = root.createNestedArray("event_types");
JsonArray event_types = root["event_types"].to<JsonArray>();
for (auto const &event_type : obj->get_event_types()) {
event_types.add(event_type);
}
@@ -1682,6 +1686,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
}
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
root["value"] = obj->update_info.latest_version;
@@ -1707,6 +1712,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
this->add_sorting_info_(root, obj);
}
});
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
#endif

View File

@@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
#ifdef USE_WEBSERVER_OTA
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
// Show OTA form only if web_server OTA is not explicitly disabled
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
#endif

View File

@@ -40,4 +40,4 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")

View File

@@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
message = json::build_json([group](JsonObject root) {
root["name"] = group.second.name;
root["sorting_weight"] = group.second.weight;
});
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
// a (very) large number of these should be able to be queued initially without defer
// since the only thing in the send buffer at this point is the initial ping/config

View File

@@ -20,10 +20,6 @@
#include "lwip/dns.h"
#include "lwip/err.h"
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
#include "lwip/priv/tcpip_priv.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -295,25 +291,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
}
if (!manual_ip.has_value()) {
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
LOCK_TCPIP_CORE();
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526
{
LwIPLock lock;
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
}
#endif
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
UNLOCK_TCPIP_CORE();
}
#endif
// No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {

View File

@@ -8,6 +8,7 @@
#include "esphome/core/log.h"
#include "esphome/core/time.h"
#include "esphome/components/network/util.h"
#include "esphome/core/helpers.h"
#include <esp_wireguard.h>
#include <esp_wireguard_err.h>
@@ -42,7 +43,10 @@ void Wireguard::setup() {
this->publish_enabled_state();
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
{
LwIPLock lock;
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
}
if (this->wg_initialized_ == ESP_OK) {
ESP_LOGI(TAG, "Initialized");
@@ -249,7 +253,10 @@ void Wireguard::start_connection_() {
}
ESP_LOGD(TAG, "Starting connection");
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
{
LwIPLock lock;
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
}
if (this->wg_connected_ == ESP_OK) {
ESP_LOGI(TAG, "Connection started");
@@ -280,7 +287,10 @@ void Wireguard::start_connection_() {
void Wireguard::stop_connection_() {
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
ESP_LOGD(TAG, "Stopping connection");
esp_wireguard_disconnect(&(this->wg_ctx_));
{
LwIPLock lock;
esp_wireguard_disconnect(&(this->wg_ctx_));
}
this->wg_connected_ = ESP_FAIL;
}
}

View File

@@ -73,6 +73,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
VALID_SUBSTITUTIONS_CHARACTERS,
Framework,
__version__ as ESPHOME_VERSION,
)
from esphome.core import (
@@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid):
"""Represents an invalid value in the final validation phase where the path should not be prepended."""
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
extra: str = ""
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
extra = match[4] or ""
return Version(major=major, minor=minor, patch=patch, extra=extra)
@property
def is_beta(self) -> bool:
"""Check if this version is a beta version."""
return self.extra.startswith("b")
@property
def is_dev(self) -> bool:
"""Check if this version is a development version."""
return self.extra.startswith("dev")
def check_not_templatable(value):
if isinstance(value, Lambda):
raise Invalid("This option is not templatable!")
@@ -619,16 +652,35 @@ def only_on(platforms):
return validator_
def only_with_framework(frameworks):
def only_with_framework(
frameworks: Framework | str | list[Framework | str], suggestions=None
):
"""Validate that this option can only be specified on the given frameworks."""
if not isinstance(frameworks, list):
frameworks = [frameworks]
frameworks = [Framework(framework) for framework in frameworks]
if suggestions is None:
suggestions = {}
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/components/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/components/{path}"
else:
docs_format = "https://esphome.io/components/{path}"
def validator_(obj):
if CORE.target_framework not in frameworks:
raise Invalid(
f"This feature is only available with frameworks {frameworks}"
)
err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
if suggestion := suggestions.get(CORE.target_framework, None):
(component, docs_path) = suggestion
err_str += f"\nPlease use '{component}'"
if docs_path:
err_str += f": {docs_format.format(path=docs_path)}"
raise Invalid(err_str)
return obj
return validator_
@@ -637,8 +689,8 @@ def only_with_framework(frameworks):
only_on_esp32 = only_on(PLATFORM_ESP32)
only_on_esp8266 = only_on(PLATFORM_ESP8266)
only_on_rp2040 = only_on(PLATFORM_RP2040)
only_with_arduino = only_with_framework("arduino")
only_with_esp_idf = only_with_framework("esp-idf")
only_with_arduino = only_with_framework(Framework.ARDUINO)
only_with_esp_idf = only_with_framework(Framework.ESP_IDF)
# Adapted from:
@@ -1965,26 +2017,6 @@ def source_refresh(value: str):
return positive_time_period_seconds(value)
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
return Version(major=major, minor=minor, patch=patch)
def version_number(value):
value = string_strict(value)
try:

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.7.0b3"
__version__ = "2025.7.4"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -68,8 +68,11 @@ void Application::setup() {
do {
uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call();
this->feed_wdt();
uint32_t now = millis();
// Process pending loop enables to handle GPIO interrupts during setup
this->before_loop_tasks_(now);
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis();
@@ -78,6 +81,8 @@ void Application::setup() {
this->app_state_ |= new_app_state;
this->feed_wdt();
}
this->after_loop_tasks_();
this->app_state_ = new_app_state;
yield();
} while (!component->can_proceed());
@@ -94,30 +99,10 @@ void Application::setup() {
void Application::loop() {
uint8_t new_app_state = 0;
this->scheduler.call();
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
// Feed WDT with time
this->feed_wdt(last_op_end_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
this->before_loop_tasks_(last_op_end_time);
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
@@ -138,7 +123,7 @@ void Application::loop() {
this->feed_wdt(last_op_end_time);
}
this->in_loop_ = false;
this->after_loop_tasks_();
this->app_state_ = new_app_state;
// Use the last component's end time instead of calling millis() again
@@ -309,6 +294,12 @@ void Application::disable_component_loop_(Component *component) {
if (this->in_loop_ && i == this->current_loop_index_) {
// Decrement so we'll process the swapped component next
this->current_loop_index_--;
// Update the loop start time to current time so the swapped component
// gets correct timing instead of inheriting stale timing.
// This prevents integer underflow in timing calculations by ensuring
// the swapped component starts with a fresh timing reference, avoiding
// errors caused by stale or wrapped timing values.
this->loop_component_start_time_ = millis();
}
}
return;
@@ -394,6 +385,36 @@ void Application::enable_pending_loops_() {
}
}
void Application::before_loop_tasks_(uint32_t loop_start_time) {
// Process scheduled tasks
this->scheduler.call();
// Feed the watchdog timer
this->feed_wdt(loop_start_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
}
void Application::after_loop_tasks_() {
// Clear the in_loop_ flag to indicate we're done processing components
this->in_loop_ = false;
}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Application::register_socket_fd(int fd) {
// WARNING: This function is NOT thread-safe and must only be called from the main loop

View File

@@ -504,6 +504,8 @@ class Application {
void enable_component_loop_(Component *component);
void enable_pending_loops_();
void activate_looping_component_(uint16_t index);
void before_loop_tasks_(uint32_t loop_start_time);
void after_loop_tasks_();
void feed_wdt_arch_();

View File

@@ -158,14 +158,14 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
void play_complex(Ts... x) override {
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
this->num_running_++;
this->set_timeout(this->delay_.value(x...), f);
this->set_timeout("delay", this->delay_.value(x...), f);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void play(Ts... x) override { /* ignore - see play_complex */
}
void stop() override { this->cancel_timeout(""); }
void stop() override { this->cancel_timeout("delay"); }
};
template<typename... Ts> class LambdaAction : public Action<Ts...> {

View File

@@ -252,10 +252,10 @@ void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));
}
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, "", timeout, std::move(f));
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
}
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, "", interval, std::move(f));
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
}
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
float backoff_increase_factor) { // NOLINT
@@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
}
bool Component::can_proceed() { return true; }

View File

@@ -1,6 +1,6 @@
#pragma once
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#if defined(USE_ESP32)
#include <atomic>
#include <cstddef>
@@ -78,4 +78,4 @@ template<class T, uint8_t SIZE> class EventPool {
} // namespace esphome
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
#endif // defined(USE_ESP32)

View File

@@ -67,7 +67,10 @@ To bit_cast(const From &src) {
return dst;
}
#endif
using std::lerp;
// clang-format off
inline float lerp(float completion, float start, float end) = delete; // Please use std::lerp. Notice that it has different order on arguments!
// clang-format on
// std::byteswap from C++23
template<typename T> constexpr T byteswap(T n) {
@@ -683,6 +686,23 @@ class InterruptLock {
#endif
};
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
*
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
* It ensures thread-safe access to lwIP APIs.
*
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
*/
class LwIPLock {
public:
LwIPLock();
~LwIPLock();
// Delete copy constructor and copy assignment operator to prevent accidental copying
LwIPLock(const LwIPLock &) = delete;
LwIPLock &operator=(const LwIPLock &) = delete;
};
/** Helper class to request `loop()` to be called as fast as possible.
*
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher

View File

@@ -1,17 +1,12 @@
#pragma once
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#if defined(USE_ESP32)
#include <atomic>
#include <cstddef>
#if defined(USE_ESP32)
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#elif defined(USE_LIBRETINY)
#include <FreeRTOS.h>
#include <task.h>
#endif
/*
* Lock-free queue for single-producer single-consumer scenarios.
@@ -148,4 +143,4 @@ template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQu
} // namespace esphome
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
#endif // defined(USE_ESP32)

View File

@@ -446,7 +446,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
// Helper to cancel items by name - must be called with lock held
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
// Early return if name is invalid - no items to cancel
if (name_cstr == nullptr || name_cstr[0] == '\0') {
if (name_cstr == nullptr) {
return false;
}

View File

@@ -114,16 +114,17 @@ class Scheduler {
name_is_dynamic = false;
}
if (!name || !name[0]) {
if (!name) {
// nullptr case - no name provided
name_.static_name = nullptr;
} else if (make_copy) {
// Make a copy for dynamic strings
// Make a copy for dynamic strings (including empty strings)
size_t len = strlen(name);
name_.dynamic_name = new char[len + 1];
memcpy(name_.dynamic_name, name, len + 1);
name_is_dynamic = true;
} else {
// Use static string directly
// Use static string directly (including empty strings)
name_.static_name = name;
}
}

View File

@@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
os.environ.setdefault(
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
)
# Suppress Python syntax warnings from third-party scripts during compilation
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
cmd = ["platformio"] + list(args)
if not CORE.verbose:

View File

@@ -147,6 +147,13 @@ class RedirectText:
continue
self._write_color_replace(line)
# Check for flash size error and provide helpful guidance
if (
"Error: The program size" in line
and "is greater than maximum allowed" in line
and (help_msg := get_esp32_arduino_flash_error_help())
):
self._write_color_replace(help_msg)
else:
self._write_color_replace(s)
@@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
result.sort(key=lambda x: x.path)
return result
def get_esp32_arduino_flash_error_help() -> str | None:
"""Returns helpful message when ESP32 with Arduino runs out of flash space."""
from esphome.core import CORE
if not (CORE.is_esp32 and CORE.using_arduino):
return None
from esphome.log import AnsiFore, color
return (
"\n"
+ color(
AnsiFore.YELLOW,
"💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n",
)
+ "\n"
+ "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n"
+ "\n"
+ "1. In your YAML configuration, modify the framework section:\n"
+ "\n"
+ " esp32:\n"
+ " framework:\n"
+ " type: esp-idf\n"
+ "\n"
+ "2. Clean build files and compile again\n"
+ "\n"
+ "Note: ESP-IDF uses less flash space and provides better performance.\n"
+ "Some Arduino-specific libraries may need alternatives.\n\n"
)

View File

@@ -162,6 +162,9 @@ def get_ini_content():
# Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
# Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n"
@@ -222,6 +225,9 @@ def write_platformio_project():
write_gitignore()
write_platformio_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
#pragma once
@@ -394,3 +400,20 @@ def write_gitignore():
if not os.path.isfile(path):
with open(file=path, mode="w", encoding="utf-8") as f:
f.write(GITIGNORE_CONTENT)
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
Import("env")
# Add C++ specific flags
"""
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)

View File

@@ -35,7 +35,7 @@ build_flags =
lib_deps =
esphome/noise-c@0.1.10 ; api
improv/Improv@1.2.4 ; improv_serial / esp32_improv
bblanchon/ArduinoJson@6.18.5 ; json
bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier
@@ -138,7 +138,7 @@ lib_deps =
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps}
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
NetworkClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)
@@ -235,7 +235,7 @@ build_flags =
-DUSE_ZEPHYR
-DUSE_NRF52
lib_deps =
bblanchon/ArduinoJson@7.0.0 ; json
bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
pavlodn/HaierProtocol@0.9.31 ; haier
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393

View File

@@ -6,7 +6,7 @@ set -e
cd "$(dirname "$0")/.."
if [ ! -n "$VIRTUAL_ENV" ]; then
if [ -x "$(command -v uv)" ]; then
uv venv venv
uv venv --seed venv
else
python3 -m venv venv
fi

View File

@@ -4,6 +4,8 @@ esphome:
esp32:
board: esp32dev
logger:
text:
- platform: template
name: "test 1 text"

View File

@@ -8,31 +8,31 @@ from esphome.types import ConfigType
def test_web_server_ota_true_fails_validation() -> None:
"""Test that web_server with ota: true fails validation with helpful message."""
from esphome.components.web_server import validate_ota_removed
from esphome.components.web_server import validate_ota
# Config with ota: true should fail
config: ConfigType = {"ota": True}
with pytest.raises(cv.Invalid) as exc_info:
validate_ota_removed(config)
validate_ota(config)
# Check error message contains migration instructions
error_msg = str(exc_info.value)
assert "has been removed from 'web_server'" in error_msg
assert "only accepts 'false' to disable OTA" in error_msg
assert "platform: web_server" in error_msg
assert "ota:" in error_msg
def test_web_server_ota_false_passes_validation() -> None:
"""Test that web_server with ota: false passes validation."""
from esphome.components.web_server import validate_ota_removed
from esphome.components.web_server import validate_ota
# Config with ota: false should pass
config: ConfigType = {"ota": False}
result = validate_ota_removed(config)
result = validate_ota(config)
assert result == config
# Config without ota should also pass
config: ConfigType = {}
result = validate_ota_removed(config)
result = validate_ota(config)
assert result == config

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1,18 @@
logger:
id: logger_id
level: DEBUG
on_message:
- level: DEBUG
then:
- lambda: |-
ESP_LOGD("test", "Got message level %d: %s - %s", level, tag, message);
- level: WARN
then:
- lambda: |-
ESP_LOGW("test", "Warning level %d from %s", level, tag);
- level: ERROR
then:
- lambda: |-
// Test that level is uint8_t by using it in calculations
uint8_t adjusted_level = level + 1;
ESP_LOGE("test", "Error with adjusted level %d", adjusted_level);

View File

@@ -928,6 +928,12 @@ lvgl:
angle_range: 360
rotation: !lambda return 2700;
indicators:
- tick_style:
start_value: 0
end_value: 60
color_start: 0x0000bd
color_end: 0xbd0000
width: !lambda return 1;
- line:
opa: 50%
id: minute_hand

View File

@@ -0,0 +1,87 @@
esphome:
name: api-string-lambda-test
host:
api:
actions:
# Service that tests string lambda functionality
- action: test_string_lambda
variables:
input_string: string
then:
# Log the input to verify service was called
- logger.log:
format: "Service called with string: %s"
args: [input_string.c_str()]
# This is the key test - using a lambda that returns x.c_str()
# where x is already a string. This would fail to compile in 2025.7.0b5
# with "no matching function for call to 'to_string(std::string)'"
# This is the exact case from issue #9539
- homeassistant.tag_scanned: !lambda 'return input_string.c_str();'
# Also test with homeassistant.event to verify our fix works with data fields
- homeassistant.event:
event: esphome.test_string_lambda
data:
value: !lambda 'return input_string.c_str();'
# Service that tests int lambda functionality
- action: test_int_lambda
variables:
input_number: int
then:
# Log the input to verify service was called
- logger.log:
format: "Service called with int: %d"
args: [input_number]
# Test that int lambdas still work correctly with to_string
# The TemplatableStringValue should automatically convert int to string
- homeassistant.event:
event: esphome.test_int_lambda
data:
value: !lambda 'return input_number;'
# Service that tests float lambda functionality
- action: test_float_lambda
variables:
input_float: float
then:
# Log the input to verify service was called
- logger.log:
format: "Service called with float: %.2f"
args: [input_float]
# Test that float lambdas still work correctly with to_string
# The TemplatableStringValue should automatically convert float to string
- homeassistant.event:
event: esphome.test_float_lambda
data:
value: !lambda 'return input_float;'
# Service that tests char* lambda functionality (e.g., from itoa or sprintf)
- action: test_char_ptr_lambda
variables:
input_number: int
input_string: string
then:
# Log the input to verify service was called
- logger.log:
format: "Service called with number for char* test: %d"
args: [input_number]
# Test that char* lambdas work correctly
# This would fail in issue #9628 with "invalid conversion from 'char*' to 'long long unsigned int'"
- homeassistant.event:
event: esphome.test_char_ptr_lambda
data:
# Test snprintf returning char*
decimal_value: !lambda 'static char buffer[20]; snprintf(buffer, sizeof(buffer), "%d", input_number); return buffer;'
# Test strdup returning char* (dynamically allocated)
string_copy: !lambda 'return strdup(input_string.c_str());'
# Test string literal (const char*)
literal: !lambda 'return "test literal";'
logger:
level: DEBUG

View File

@@ -0,0 +1,24 @@
esphome:
name: test-delay-action
host:
api:
actions:
- action: start_delay_then_restart
then:
- logger.log: "Starting first script execution"
- script.execute: test_delay_script
- delay: 250ms # Give first script time to start delay
- logger.log: "Restarting script (should cancel first delay)"
- script.execute: test_delay_script
logger:
level: DEBUG
script:
- id: test_delay_script
mode: restart
then:
- logger.log: "Script started, beginning delay"
- delay: 500ms # Long enough that it won't complete before restart
- logger.log: "Delay completed successfully"

View File

@@ -1,7 +1,7 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
CODEOWNERS = ["@esphome/tests"]
@@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
LoopTestISRComponent = loop_test_component_ns.class_(
"LoopTestISRComponent", cg.Component
)
LoopTestUpdateComponent = loop_test_component_ns.class_(
"LoopTestUpdateComponent", cg.PollingComponent
)
CONF_DISABLE_AFTER = "disable_after"
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
CONF_ISR_COMPONENTS = "isr_components"
CONF_UPDATE_COMPONENTS = "update_components"
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
COMPONENT_CONFIG_SCHEMA = cv.Schema(
{
@@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
}
)
UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent),
cv.Required(CONF_NAME): cv.string,
cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_,
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LoopTestComponent),
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list(
UPDATE_COMPONENT_CONFIG_SCHEMA
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -94,3 +111,12 @@ async def to_code(config):
var = cg.new_Pvariable(isr_config[CONF_ID])
await cg.register_component(var, isr_config)
cg.add(var.set_name(isr_config[CONF_NAME]))
# Create update test components
for update_config in config.get(CONF_UPDATE_COMPONENTS, []):
var = cg.new_Pvariable(update_config[CONF_ID])
await cg.register_component(var, update_config)
cg.add(var.set_name(update_config[CONF_NAME]))
cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER]))
cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL]))

View File

@@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() {
this->disable_loop();
}
// LoopTestUpdateComponent implementation
void LoopTestUpdateComponent::setup() {
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str());
}
void LoopTestUpdateComponent::loop() {
this->loop_count_++;
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_);
// Disable loop after specified count to test component.update when loop is disabled
if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) {
ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_);
this->disable_loop();
}
}
void LoopTestUpdateComponent::update() {
this->update_count_++;
// Check if loop is disabled by testing component state
bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE;
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(),
this->update_count_, loop_disabled ? "YES" : "NO");
}
} // namespace loop_test_component
} // namespace esphome

View File

@@ -4,6 +4,7 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace loop_test_component {
@@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
LoopTestComponent *parent_;
};
// Component with update() method to test component.update action
class LoopTestUpdateComponent : public PollingComponent {
public:
LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval
void set_name(const std::string &name) { this->name_ = name; }
void set_disable_loop_after(int count) { this->disable_loop_after_ = count; }
void setup() override;
void loop() override;
void update() override;
int get_update_count() const { return this->update_count_; }
int get_loop_count() const { return this->loop_count_; }
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
std::string name_;
int loop_count_{0};
int update_count_{0};
int disable_loop_after_{0};
};
} // namespace loop_test_component
} // namespace esphome

View File

@@ -40,6 +40,13 @@ loop_test_component:
- id: isr_test
name: "isr_test"
# Update test component to test component.update when loop is disabled
update_components:
- id: update_test_component
name: "update_test"
disable_loop_after: 3 # Disable loop after 3 iterations
update_interval: 0.1s # Fast update interval for testing
# Interval to re-enable the self_disable_10 component after some time
interval:
- interval: 0.5s
@@ -51,3 +58,28 @@ interval:
- logger.log: "Re-enabling self_disable_10 via service"
- loop_test_component.enable:
id: self_disable_10
# Test component.update on a component with disabled loop
- interval: 0.1s
then:
- lambda: |-
static bool manual_update_done = false;
if (!manual_update_done &&
id(update_test_component).get_loop_count() == 3 &&
id(update_test_component).get_update_count() >= 3) {
ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop");
manual_update_done = true;
}
- if:
condition:
lambda: |-
static bool manual_update_triggered = false;
if (!manual_update_triggered &&
id(update_test_component).get_loop_count() == 3 &&
id(update_test_component).get_update_count() >= 3) {
manual_update_triggered = true;
return true;
}
return false;
then:
- component.update: update_test_component

View File

@@ -4,9 +4,7 @@ esphome:
priority: -100
then:
- logger.log: "Starting scheduler string tests"
platformio_options:
build_flags:
- "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging
debug_scheduler: true # Enable scheduler debug logging
host:
api:
@@ -32,6 +30,12 @@ globals:
- id: results_reported
type: bool
initial_value: 'false'
- id: edge_tests_done
type: bool
initial_value: 'false'
- id: empty_cancel_failed
type: bool
initial_value: 'false'
script:
- id: test_static_strings
@@ -147,12 +151,106 @@ script:
static TestDynamicDeferComponent test_dynamic_defer_component;
test_dynamic_defer_component.test_dynamic_defer();
- id: test_cancellation_edge_cases
then:
- logger.log: "Testing cancellation edge cases"
- lambda: |-
auto *component1 = id(test_sensor1);
// Use a different component for empty string tests to avoid interference
auto *component2 = id(test_sensor2);
// Test 12: Cancel with empty string - regression test for issue #9599
// First create a timeout with empty name on component2 to avoid interference
App.scheduler.set_timeout(component2, "", 500, []() {
ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!");
id(empty_cancel_failed) = true;
});
// Now cancel it - this should work after our fix
bool cancelled_empty = App.scheduler.cancel_timeout(component2, "");
ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false");
if (!cancelled_empty) {
ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!");
id(empty_cancel_failed) = true;
}
// Test 13: Cancel non-existent timeout
bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist");
ESP_LOGI("test", "Cancel non-existent timeout result: %s",
cancelled_nonexistent ? "true (unexpected!)" : "false (expected)");
// Test 14: Multiple timeouts with same name - only last should execute
for (int i = 0; i < 5; i++) {
App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() {
ESP_LOGI("test", "Duplicate timeout %d fired", i);
id(timeout_counter) += 1;
});
}
ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'");
// Test 15: Multiple intervals with same name - only last should run
for (int i = 0; i < 3; i++) {
App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() {
ESP_LOGI("test", "Duplicate interval %d fired", i);
id(interval_counter) += 10; // Large increment to detect multiple
// Cancel after first execution
App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval");
});
}
ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'");
// Test 16: Cancel with nullptr protection (via empty const char*)
const char* null_name = "";
App.scheduler.set_timeout(component2, null_name, 600, []() {
ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!");
id(empty_cancel_failed) = true;
});
bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name);
ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)",
cancelled_const_empty ? "true" : "false");
if (!cancelled_const_empty) {
ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!");
id(empty_cancel_failed) = true;
}
// Test 17: Rapid create/cancel/create with same name
App.scheduler.set_timeout(component1, "rapid_test", 5000, []() {
ESP_LOGI("test", "First rapid timeout - should not fire");
id(timeout_counter) += 100;
});
App.scheduler.cancel_timeout(component1, "rapid_test");
App.scheduler.set_timeout(component1, "rapid_test", 250, []() {
ESP_LOGI("test", "Second rapid timeout - should fire");
id(timeout_counter) += 1;
});
// Test 18: Cancel all with a specific name (multiple instances)
// Create multiple with same name
App.scheduler.set_timeout(component1, "multi_cancel", 300, []() {
ESP_LOGI("test", "Multi-cancel timeout 1");
});
App.scheduler.set_timeout(component1, "multi_cancel", 350, []() {
ESP_LOGI("test", "Multi-cancel timeout 2");
});
App.scheduler.set_timeout(component1, "multi_cancel", 400, []() {
ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire");
id(timeout_counter) += 1;
});
// Note: Each set_timeout with same name cancels the previous one automatically
- id: report_results
then:
- lambda: |-
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d",
id(timeout_counter), id(interval_counter));
// Check if empty string cancellation test passed
if (id(empty_cancel_failed)) {
ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!");
} else {
ESP_LOGI("test", "Empty string cancellation test PASSED");
}
sensor:
- platform: template
name: Test Sensor 1
@@ -189,12 +287,23 @@ interval:
- delay: 0.2s
- script.execute: test_dynamic_strings
# Run cancellation edge case tests after dynamic tests
- interval: 0.2s
then:
- if:
condition:
lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);'
then:
- lambda: 'id(edge_tests_done) = true;'
- delay: 0.5s
- script.execute: test_cancellation_edge_cases
# Report results after all tests
- interval: 0.2s
then:
- if:
condition:
lambda: 'return id(dynamic_tests_done) && !id(results_reported);'
lambda: 'return id(edge_tests_done) && !id(results_reported);'
then:
- lambda: 'id(results_reported) = true;'
- delay: 1s

View File

@@ -0,0 +1,100 @@
"""Integration test for TemplatableStringValue with string lambdas."""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_api_string_lambda(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test TemplatableStringValue works with lambdas that return different types."""
loop = asyncio.get_running_loop()
# Track log messages for all four service calls
string_called_future = loop.create_future()
int_called_future = loop.create_future()
float_called_future = loop.create_future()
char_ptr_called_future = loop.create_future()
# Patterns to match in logs - confirms the lambdas compiled and executed
string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA")
int_pattern = re.compile(r"Service called with int: 42")
float_pattern = re.compile(r"Service called with float: 3\.14")
char_ptr_pattern = re.compile(r"Service called with number for char\* test: 123")
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if not string_called_future.done() and string_pattern.search(line):
string_called_future.set_result(True)
if not int_called_future.done() and int_pattern.search(line):
int_called_future.set_result(True)
if not float_called_future.done() and float_pattern.search(line):
float_called_future.set_result(True)
if not char_ptr_called_future.done() and char_ptr_pattern.search(line):
char_ptr_called_future.set_result(True)
# Run with log monitoring
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "api-string-lambda-test"
# List services to find our test services
_, services = await client.list_entities_services()
# Find all test services
string_service = next(
(s for s in services if s.name == "test_string_lambda"), None
)
assert string_service is not None, "test_string_lambda service not found"
int_service = next((s for s in services if s.name == "test_int_lambda"), None)
assert int_service is not None, "test_int_lambda service not found"
float_service = next(
(s for s in services if s.name == "test_float_lambda"), None
)
assert float_service is not None, "test_float_lambda service not found"
char_ptr_service = next(
(s for s in services if s.name == "test_char_ptr_lambda"), None
)
assert char_ptr_service is not None, "test_char_ptr_lambda service not found"
# Execute all four services to test different lambda return types
client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"})
client.execute_service(int_service, {"input_number": 42})
client.execute_service(float_service, {"input_float": 3.14})
client.execute_service(
char_ptr_service, {"input_number": 123, "input_string": "test_string"}
)
# Wait for all service log messages
# This confirms the lambdas compiled successfully and executed
try:
await asyncio.wait_for(
asyncio.gather(
string_called_future,
int_called_future,
float_called_future,
char_ptr_called_future,
),
timeout=5.0,
)
except TimeoutError:
pytest.fail(
"One or more service log messages not received - lambda may have failed to compile or execute"
)

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