1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-09 19:41:49 +00:00

Compare commits

...

58 Commits

Author SHA1 Message Date
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
Jesse Hills
16292a9f13 Merge pull request #9483 from esphome/bump-2025.7.0b3
2025.7.0b3
2025-07-14 15:41:59 +12:00
Jesse Hills
90f0ebb22b Dont autofix PR 2025-07-14 14:12:26 +12:00
Jesse Hills
4153380f99 Remove hook that doesnt exist on beta 2025-07-14 13:48:41 +12:00
J. Nick Koston
740c0ef9d7 Fix pre-commit CI failures by skipping local hooks that require virtual environment (#9476) 2025-07-14 13:46:14 +12:00
Jesse Hills
b4521e1d8c Bump version to 2025.7.0b3 2025-07-14 13:08:24 +12:00
J. Nick Koston
10ca7ed85b Fix dormant bug in RAMAllocator::reallocate() manual_size calculation (#9482) 2025-07-14 13:08:23 +12:00
J. Nick Koston
e43efdaaec Follow logging best practices by removing redundant component prefix (#9481) 2025-07-14 13:08:23 +12:00
Clyde Stubbs
9207bf97f3 [esp_ldo] Component schema; default priority (#9479) 2025-07-14 13:08:23 +12:00
Javier Peletier
c13317f807 [substitutions] Fix #7189 (#9469) 2025-07-14 13:08:23 +12:00
J. Nick Koston
77d1d0414d Automatically disable interrupts for ESP8266 GPIO16 binary sensors (#9467) 2025-07-14 13:08:23 +12:00
Peter Zich
8f42bc6aac [lvgl] Post-process size arguments in meter config (#9466)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-14 13:08:23 +12:00
Peter Zich
9beb4e2cd4 (Maybe?) fix I2S speaker internal DAC mode (#9435) 2025-07-14 13:08:23 +12:00
Keith Burzinski
097aac2183 [ld2420] Memory optimization, code clean-up (#9426) 2025-07-14 13:08:23 +12:00
110 changed files with 1473 additions and 374 deletions

View File

@@ -1,6 +1,14 @@
--- ---
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: weekly
autofix_prs: false
# Skip hooks that have issues in pre-commit CI environment
skip: [pylint, yamllint]
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.7.0b2 PROJECT_NUMBER = 2025.7.2
# Using the PROJECT_BRIEF tag one can provide an optional one line description # 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 # 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 { namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { 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: public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {} 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> template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
TemplatableStringValue(F f) 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 { template<typename... Ts> class TemplatableKeyValuePair {

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_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) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // 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 from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
@@ -8,6 +10,7 @@ from esphome.const import (
CONF_CONTRAST, CONF_CONTRAST,
CONF_DATA_PINS, CONF_DATA_PINS,
CONF_FREQUENCY, CONF_FREQUENCY,
CONF_I2C,
CONF_I2C_ID, CONF_I2C_ID,
CONF_ID, CONF_ID,
CONF_PIN, CONF_PIN,
@@ -20,6 +23,9 @@ from esphome.const import (
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), 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 = { SETTERS = {
# pin assignment # pin assignment
CONF_DATA_PINS: "set_data_pins", CONF_DATA_PINS: "set_data_pins",

View File

@@ -22,6 +22,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } 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) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac); wifi_get_macaddr(STATION_IF, mac);
} }

View File

@@ -20,14 +20,16 @@ adjusted_ids = set()
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.ensure_list( cv.ensure_list(
{ cv.COMPONENT_SCHEMA.extend(
cv.GenerateID(): cv.declare_id(EspLdo), {
cv.Required(CONF_VOLTAGE): cv.All( cv.GenerateID(): cv.declare_id(EspLdo),
cv.voltage, cv.float_range(min=0.5, max=2.7) cv.Required(CONF_VOLTAGE): cv.All(
), cv.voltage, cv.float_range(min=0.5, max=2.7)
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), ),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
} cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
}
)
), ),
cv.only_with_esp_idf, cv.only_with_esp_idf,
only_on_variant(supported=[VARIANT_ESP32P4]), only_on_variant(supported=[VARIANT_ESP32P4]),

View File

@@ -17,6 +17,9 @@ class EspLdo : public Component {
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
void set_voltage(float voltage) { this->voltage_ = voltage; } void set_voltage(float voltage) { this->voltage_ = voltage; }
void adjust_voltage(float voltage); void adjust_voltage(float voltage);
float get_setup_priority() const override {
return setup_priority::BUS; // LDO setup should be done early
}
protected: protected:
int channel_; int channel_;

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_PIN from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
from esphome.core import CORE
from .. import gpio_ns from .. import gpio_ns
_LOGGER = logging.getLogger(__name__)
GPIOBinarySensor = gpio_ns.class_( GPIOBinarySensor = gpio_ns.class_(
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
) )
@@ -24,7 +29,21 @@ CONFIG_SCHEMA = (
.extend( .extend(
{ {
cv.Required(CONF_PIN): pins.gpio_input_pin_schema, 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( cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True INTERRUPT_TYPES, upper=True
), ),
@@ -41,6 +60,22 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin)) cg.add(var.set_pin(pin))
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) # Check for ESP8266 GPIO16 interrupt limitation
if config[CONF_USE_INTERRUPT]: # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
# the Arduino attachInterrupt() function. This is the only known GPIO pin
# across all supported platforms that has this limitation, so we handle it
# here instead of in the platform-specific code.
use_interrupt = config[CONF_USE_INTERRUPT]
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

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

View File

@@ -180,7 +180,7 @@ async def to_code(config):
await speaker.register_speaker(var, config) await speaker.register_speaker(var, config)
if config[CONF_DAC_TYPE] == "internal": if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else: else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
if use_legacy(): if use_legacy():

View File

@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0) @coroutine_with_priority(1.0)
async def to_code(config): 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_define("USE_JSON")
cg.add_global(json_ns.using) cg.add_global(json_ns.using)

View File

@@ -1,83 +1,76 @@
#include "json_util.h" #include "json_util.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
namespace esphome { namespace esphome {
namespace json { namespace json {
static const char *const TAG = "json"; static const char *const TAG = "json";
static std::vector<char> global_json_build_buffer; // NOLINT // Build an allocator for the JSON Library using the RAMAllocator class
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); 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) { std::string build_json(const json_build_t &f) {
// Here we are allocating up to 5kb of memory, // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// with the heap size minus 2kb to be safe if less than 5kb auto doc_allocator = SpiRamAllocator();
// as we can not have a true dynamic sized document. JsonDocument json_document(&doc_allocator);
// The excess memory is freed below with `shrinkToFit()` if (json_document.overflowed()) {
auto free_heap = ALLOCATOR.get_max_free_block_size(); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
size_t request_size = std::min(free_heap, (size_t) 512); return "{}";
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;
} }
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) { bool parse_json(const std::string &data, const json_parse_t &f) {
// Here we are allocating 1.5 times the data size, // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// with the heap size minus 2kb to be safe if less than that auto doc_allocator = SpiRamAllocator();
// as we can not have a true dynamic sized document. JsonDocument json_document(&doc_allocator);
// The excess memory is freed below with `shrinkToFit()` if (json_document.overflowed()) {
auto free_heap = ALLOCATOR.get_max_free_block_size(); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); return false;
while (true) { }
DynamicJsonDocument json_document(request_size); DeserializationError err = deserializeJson(json_document, data);
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();
JsonObject root = json_document.as<JsonObject>(); JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
return f(root); return f(root);
} else if (err == DeserializationError::NoMemory) { } 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");
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); return false;
return false; }
} ESP_LOGE(TAG, "Parse error: %s", err.c_str());
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
}
};
return false; return false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
} // namespace json } // namespace json

View File

@@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.binary_sensor"; static const char *const TAG = "ld2420.binary_sensor";
void LD2420BinarySensor::dump_config() { void LD2420BinarySensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); ESP_LOGCONFIG(TAG, "Binary Sensor:");
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
} }

View File

@@ -2,7 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
static const char *const TAG = "LD2420.button"; static const char *const TAG = "ld2420.button";
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {

View File

@@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple";
// Memory-efficient lookup tables // Memory-efficient lookup tables
struct StringToUint8 { struct StringToUint8 {
const char *str; const char *str;
uint8_t value; const uint8_t value;
}; };
static constexpr StringToUint8 OP_MODE_BY_STR[] = { static constexpr StringToUint8 OP_MODE_BY_STR[] = {
@@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = {
// Helper function for lookups // Helper function for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) { for (const auto &entry : arr) {
if (str == entry.str) if (str == entry.str) {
return entry.value; return entry.value;
}
} }
return 0xFF; // Not found return 0xFF; // Not found
} }
@@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() {
void LD2420Component::loop() { void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it. // If there is a active send command do not process it here, the send command call will handle it.
if (!this->get_cmd_active_()) { while (!this->cmd_active_ && this->available()) {
if (!this->available()) this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
return;
static uint8_t buffer[2048];
static uint8_t rx_data;
while (this->available()) {
rx_data = this->read();
this->readline_(rx_data, buffer, sizeof(buffer));
}
} }
} }
@@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() {
// Store average and peak values // Store average and peak values
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
if (this->gate_peak[gate] < peak) if (this->gate_peak[gate] < peak) {
this->gate_peak[gate] = peak; this->gate_peak[gate] = peak;
}
uint32_t calculated_value = uint32_t calculated_value =
(static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate]))); (static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
@@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) {
} }
} else { } else {
// Set the current data back so we don't have new data that can be applied in error. // Set the current data back so we don't have new data that can be applied in error.
if (this->get_calibration_()) if (this->get_calibration_()) {
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
}
this->set_calibration_(false); this->set_calibration_(false);
} }
} else { } else {
@@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) {
} }
void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
static int pos = 0; if (rx_data < 0) {
return; // No data available
if (rx_data >= 0) { }
if (pos < len - 1) { if (this->buffer_pos_ < len - 1) {
buffer[pos++] = rx_data; buffer[this->buffer_pos_++] = rx_data;
buffer[pos] = 0; buffer[this->buffer_pos_] = 0;
} else { } else {
pos = 0; // We should never get here, but just in case...
} ESP_LOGW(TAG, "Max command length exceeded; ignoring");
if (pos >= 4) { this->buffer_pos_ = 0;
if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { }
this->set_cmd_active_(false); // Set command state to inactive after responce. if (this->buffer_pos_ < 4) {
this->handle_ack_data_(buffer, pos); return; // Not enough data to process yet
pos = 0; }
} else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { this->cmd_active_ = false; // Set command state to inactive after response
this->handle_simple_mode_(buffer, pos); this->handle_ack_data_(buffer, this->buffer_pos_);
pos = 0; this->buffer_pos_ = 0;
} else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) &&
(this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
this->handle_energy_mode_(buffer, pos); this->handle_simple_mode_(buffer, this->buffer_pos_);
pos = 0; this->buffer_pos_ = 0;
} } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
} (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
this->handle_energy_mode_(buffer, this->buffer_pos_);
this->buffer_pos_ = 0;
} }
} }
@@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
// Resonable refresh rate for home assistant database size health // Resonable refresh rate for home assistant database size health
const int32_t current_millis = App.get_loop_component_start_time(); const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) {
return; return;
}
this->last_periodic_millis = current_millis; this->last_periodic_millis = current_millis;
for (auto &listener : this->listeners_) { for (auto &listener : this->listeners_) {
listener->on_distance(this->get_distance_()); listener->on_distance(this->get_distance_());
@@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
} }
} }
outbuf[index] = '\0'; outbuf[index] = '\0';
if (index > 1) if (index > 1) {
this->set_distance_(strtol(outbuf, &endptr, 10)); this->set_distance_(strtol(outbuf, &endptr, 10));
}
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
// Resonable refresh rate for home assistant database size health // Resonable refresh rate for home assistant database size health
const int32_t current_millis = App.get_loop_component_start_time(); const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) {
return; return;
}
this->last_normal_periodic_millis = current_millis; this->last_normal_periodic_millis = current_millis;
for (auto &listener : this->listeners_) for (auto &listener : this->listeners_)
listener->on_distance(this->get_distance_()); listener->on_distance(this->get_distance_());
@@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
int LD2420Component::send_cmd_from_array(CmdFrameT frame) { int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
uint32_t start_millis = millis(); uint32_t start_millis = millis();
uint8_t error = 0; uint8_t error = 0;
uint8_t ack_buffer[64]; uint8_t ack_buffer[MAX_LINE_LENGTH];
uint8_t cmd_buffer[64]; uint8_t cmd_buffer[MAX_LINE_LENGTH];
this->cmd_reply_.ack = false; this->cmd_reply_.ack = false;
if (frame.command != CMD_RESTART) if (frame.command != CMD_RESTART) {
this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. this->cmd_active_ = true;
} // Restart does not reply, thus no ack state required
uint8_t retry = 3; uint8_t retry = 3;
while (retry) { while (retry) {
frame.length = 0; frame.length = 0;
@@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
frame.length += sizeof(frame.footer); frame.length += sizeof(frame.footer);
for (uint16_t index = 0; index < frame.length; index++) { this->write_array(cmd_buffer, frame.length);
this->write_byte(cmd_buffer[index]);
}
error = 0; error = 0;
if (frame.command == CMD_RESTART) { if (frame.command == CMD_RESTART) {
@@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
while (!this->cmd_reply_.ack) { while (!this->cmd_reply_.ack) {
while (this->available()) { while (this->available()) {
this->readline_(read(), ack_buffer, sizeof(ack_buffer)); this->readline_(this->read(), ack_buffer, sizeof(ack_buffer));
} }
delay_microseconds_safe(1450); delay_microseconds_safe(1450);
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
@@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
break; break;
} }
} }
if (this->cmd_reply_.ack) if (this->cmd_reply_.ack) {
retry = 0; retry = 0;
if (this->cmd_reply_.error > 0) }
if (this->cmd_reply_.error > 0) {
this->handle_cmd_error(error); this->handle_cmd_error(error);
}
} }
return error; return error;
} }
@@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) {
cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.data_length += sizeof(unknown_parm);
cmd_frame.footer = CMD_FRAME_FOOTER; cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
if (this->send_cmd_from_array(cmd_frame) == 0) if (this->send_cmd_from_array(cmd_frame) == 0) {
this->set_mode_(mode); this->set_mode_(mode);
}
} }
void LD2420Component::get_firmware_version_() { void LD2420Component::get_firmware_version_() {
@@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
#ifdef USE_NUMBER #ifdef USE_NUMBER
void LD2420Component::init_gate_config_numbers() { void LD2420Component::init_gate_config_numbers() {
if (this->gate_timeout_number_ != nullptr) if (this->gate_timeout_number_ != nullptr) {
this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout)); this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout));
if (this->gate_select_number_ != nullptr) }
if (this->gate_select_number_ != nullptr) {
this->gate_select_number_->publish_state(0); this->gate_select_number_->publish_state(0);
if (this->min_gate_distance_number_ != nullptr) }
if (this->min_gate_distance_number_ != nullptr) {
this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate)); this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate));
if (this->max_gate_distance_number_ != nullptr) }
if (this->max_gate_distance_number_ != nullptr) {
this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate)); this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate));
if (this->gate_move_sensitivity_factor_number_ != nullptr) }
if (this->gate_move_sensitivity_factor_number_ != nullptr) {
this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor);
if (this->gate_still_sensitivity_factor_number_ != nullptr) }
if (this->gate_still_sensitivity_factor_number_ != nullptr) {
this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor);
}
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
if (this->gate_still_threshold_numbers_[gate] != nullptr) { if (this->gate_still_threshold_numbers_[gate] != nullptr) {
this->gate_still_threshold_numbers_[gate]->publish_state( this->gate_still_threshold_numbers_[gate]->publish_state(

View File

@@ -20,8 +20,9 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const uint8_t TOTAL_GATES = 16;
static const uint8_t CALIBRATE_SAMPLES = 64; static const uint8_t CALIBRATE_SAMPLES = 64;
static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
static const uint8_t TOTAL_GATES = 16;
enum OpMode : uint8_t { enum OpMode : uint8_t {
OP_NORMAL_MODE = 1, OP_NORMAL_MODE = 1,
@@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice {
float gate_move_sensitivity_factor{0.5}; float gate_move_sensitivity_factor{0.5};
float gate_still_sensitivity_factor{0.5}; float gate_still_sensitivity_factor{0.5};
int32_t last_periodic_millis = millis(); int32_t last_periodic_millis{0};
int32_t report_periodic_millis = millis(); int32_t report_periodic_millis{0};
int32_t monitor_periodic_millis = millis(); int32_t monitor_periodic_millis{0};
int32_t last_normal_periodic_millis = millis(); int32_t last_normal_periodic_millis{0};
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES];
uint16_t gate_avg[TOTAL_GATES]; uint16_t gate_avg[TOTAL_GATES];
uint16_t gate_peak[TOTAL_GATES]; uint16_t gate_peak[TOTAL_GATES];
@@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice {
void set_presence_(bool presence) { this->presence_ = presence; }; void set_presence_(bool presence) { this->presence_ = presence; };
uint16_t get_distance_() { return this->distance_; }; uint16_t get_distance_() { return this->distance_; };
void set_distance_(uint16_t distance) { this->distance_ = distance; }; void set_distance_(uint16_t distance) { this->distance_ = distance; };
bool get_cmd_active_() { return this->cmd_active_; };
void set_cmd_active_(bool active) { this->cmd_active_ = active; };
void handle_simple_mode_(const uint8_t *inbuf, int len); void handle_simple_mode_(const uint8_t *inbuf, int len);
void handle_energy_mode_(uint8_t *buffer, int len); void handle_energy_mode_(uint8_t *buffer, int len);
void handle_ack_data_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len);
@@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice {
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16); std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16);
#endif #endif
uint32_t max_distance_gate_; uint16_t distance_{0};
uint32_t min_distance_gate_;
uint16_t system_mode_; uint16_t system_mode_;
uint16_t gate_energy_[TOTAL_GATES]; uint16_t gate_energy_[TOTAL_GATES];
uint16_t distance_{0}; uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
uint8_t config_checksum_{0}; uint8_t buffer_data_[MAX_LINE_LENGTH];
char firmware_ver_[8]{"v0.0.0"}; char firmware_ver_[8]{"v0.0.0"};
bool cmd_active_{false}; bool cmd_active_{false};
bool presence_{false}; bool presence_{false};

View File

@@ -2,7 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
static const char *const TAG = "LD2420.number"; static const char *const TAG = "ld2420.number";
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {

View File

@@ -5,7 +5,7 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.select"; static const char *const TAG = "ld2420.select";
void LD2420Select::control(const std::string &value) { void LD2420Select::control(const std::string &value) {
this->publish_state(value); this->publish_state(value);

View File

@@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.sensor"; static const char *const TAG = "ld2420.sensor";
void LD2420Sensor::dump_config() { void LD2420Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); ESP_LOGCONFIG(TAG, "Sensor:");
LOG_SENSOR(" ", "Distance", this->distance_sensor_); LOG_SENSOR(" ", "Distance", this->distance_sensor_);
} }

View File

@@ -5,10 +5,10 @@
namespace esphome { namespace esphome {
namespace ld2420 { namespace ld2420 {
static const char *const TAG = "LD2420.text_sensor"; static const char *const TAG = "ld2420.text_sensor";
void LD2420TextSensor::dump_config() { void LD2420TextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); ESP_LOGCONFIG(TAG, "Text Sensor:");
LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_);
} }

View File

@@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_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) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
WiFi.macAddress(mac); 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 // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
void LightJSONSchema::dump_json(LightState &state, JsonObject root) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects()) if (state.supports_effects())
root["effect"] = state.get_effect_name(); 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) if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
root["brightness"] = uint8_t(values.get_brightness() * 255); 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) { if (values.get_color_mode() & ColorCapability::RGB) {
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 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) { 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"]); auto val = parse_on_off(root["state"]);
switch (val) { switch (val) {
case PARSE_ON: 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); call.set_brightness(float(root["brightness"]) / 255.0f);
} }
if (root.containsKey("color")) { if (root["color"].is<JsonObject>()) {
JsonObject color = root["color"]; JsonObject color = root["color"];
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. // 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; float max_rgb = 0.0f;
if (color.containsKey("r")) { if (color["r"].is<uint8_t>()) {
float r = float(color["r"]) / 255.0f; float r = float(color["r"]) / 255.0f;
max_rgb = fmaxf(max_rgb, r); max_rgb = fmaxf(max_rgb, r);
call.set_red(r); call.set_red(r);
} }
if (color.containsKey("g")) { if (color["g"].is<uint8_t>()) {
float g = float(color["g"]) / 255.0f; float g = float(color["g"]) / 255.0f;
max_rgb = fmaxf(max_rgb, g); max_rgb = fmaxf(max_rgb, g);
call.set_green(g); call.set_green(g);
} }
if (color.containsKey("b")) { if (color["b"].is<uint8_t>()) {
float b = float(color["b"]) / 255.0f; float b = float(color["b"]) / 255.0f;
max_rgb = fmaxf(max_rgb, b); max_rgb = fmaxf(max_rgb, b);
call.set_blue(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); 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); 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 // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
// white channel in RGBWW. // white channel in RGBWW.
if (color.containsKey("c")) { if (color["c"].is<uint8_t>()) {
call.set_warm_white(float(color["w"]) / 255.0f); call.set_warm_white(float(color["w"]) / 255.0f);
} else { } else {
call.set_white(float(color["w"]) / 255.0f); 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); 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"])); 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) { void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
LightJSONSchema::parse_color_json(state, call, 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); auto length = uint32_t(float(root["flash"]) * 1000);
call.set_flash_length(length); call.set_flash_length(length);
} }
if (root.containsKey("transition")) { if (root["transition"].is<uint16_t>()) {
auto length = uint32_t(float(root["transition"]) * 1000); auto length = uint32_t(float(root["transition"]) * 1000);
call.set_transition_length(length); call.set_transition_length(length);
} }
if (root.containsKey("effect")) { if (root["effect"].is<const char *>()) {
const char *effect = root["effect"]; const char *effect = root["effect"];
call.set_effect(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) Logger = logger_ns.class_("Logger", cg.Component)
LoggerMessageTrigger = logger_ns.class_( LoggerMessageTrigger = logger_ns.class_(
"LoggerMessageTrigger", "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" 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( await automation.build_automation(
trigger, trigger,
[ [
(cg.int_, "level"), (cg.uint8, "level"),
(cg.const_char_ptr, "tag"), (cg.const_char_ptr, "tag"),
(cg.const_char_ptr, "message"), (cg.const_char_ptr, "message"),
], ],

View File

@@ -192,7 +192,7 @@ class WidgetType:
class NumberType(WidgetType): class NumberType(WidgetType):
def get_max(self, config: dict): 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): 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_VALUE,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.cpp_generator import IntLiteral
from ..automation import action_to_code from ..automation import action_to_code
from ..defines import ( from ..defines import (
@@ -29,9 +30,9 @@ from ..defines import (
) )
from ..helpers import add_lv_use, lvgl_components_required from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import ( from ..lv_validation import (
angle,
get_end_value, get_end_value,
get_start_value, get_start_value,
lv_angle,
lv_bool, lv_bool,
lv_color, lv_color,
lv_float, lv_float,
@@ -162,7 +163,7 @@ SCALE_SCHEMA = cv.Schema(
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
cv.Optional(CONF_ROTATION): angle, cv.Optional(CONF_ROTATION): lv_angle,
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
} }
) )
@@ -187,7 +188,9 @@ class MeterType(WidgetType):
for scale_conf in config.get(CONF_SCALES, ()): for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf: if CONF_ROTATION in scale_conf:
rotation = scale_conf[CONF_ROTATION] // 10 rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
if isinstance(rotation, IntLiteral):
rotation = int(str(rotation)) // 10
with LocalVariable( with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var: ) as meter_var:
@@ -205,21 +208,20 @@ class MeterType(WidgetType):
var, var,
meter_var, meter_var,
ticks[CONF_COUNT], ticks[CONF_COUNT],
ticks[CONF_WIDTH], await size.process(ticks[CONF_WIDTH]),
ticks[CONF_LENGTH], await size.process(ticks[CONF_LENGTH]),
color, color,
) )
if CONF_MAJOR in ticks: if CONF_MAJOR in ticks:
major = ticks[CONF_MAJOR] major = ticks[CONF_MAJOR]
color = await lv_color.process(major[CONF_COLOR])
lv.meter_set_scale_major_ticks( lv.meter_set_scale_major_ticks(
var, var,
meter_var, meter_var,
major[CONF_STRIDE], major[CONF_STRIDE],
major[CONF_WIDTH], await size.process(major[CONF_WIDTH]),
major[CONF_LENGTH], await size.process(major[CONF_LENGTH]),
color, await lv_color.process(major[CONF_COLOR]),
major[CONF_LABEL_GAP], await size.process(major[CONF_LABEL_GAP]),
) )
for indicator in scale_conf.get(CONF_INDICATORS, ()): for indicator in scale_conf.get(CONF_INDICATORS, ()):
(t, v) = next(iter(indicator.items())) (t, v) = next(iter(indicator.items()))
@@ -233,7 +235,11 @@ class MeterType(WidgetType):
lv_assign( lv_assign(
ivar, ivar,
lv_expr.meter_add_needle_line( lv_expr.meter_add_needle_line(
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] var,
meter_var,
await size.process(v[CONF_WIDTH]),
color,
await size.process(v[CONF_R_MOD]),
), ),
) )
if t == CONF_ARC: if t == CONF_ARC:
@@ -241,7 +247,11 @@ class MeterType(WidgetType):
lv_assign( lv_assign(
ivar, ivar,
lv_expr.meter_add_arc( lv_expr.meter_add_arc(
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] var,
meter_var,
await size.process(v[CONF_WIDTH]),
color,
await size.process(v[CONF_R_MOD]),
), ),
) )
if t == CONF_TICK_STYLE: if t == CONF_TICK_STYLE:
@@ -257,7 +267,7 @@ class MeterType(WidgetType):
color_start, color_start,
color_end, color_end,
v[CONF_LOCAL], v[CONF_LOCAL],
v[CONF_WIDTH], await size.process(v[CONF_WIDTH]),
), ),
) )
if t == CONF_IMAGE: if t == CONF_IMAGE:

View File

@@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
} }
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &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(); const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
if (acp_supported_features & ACP_FEAT_ARM_AWAY) { if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
supported_features.add("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) { 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()) if (!this->binary_sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
if (this->binary_sensor_->is_status_binary_sensor()) 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) { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
config.state_topic = false; 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(); root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
std::string MQTTButtonComponent::component_type() const { return "button"; } std::string MQTTButtonComponent::component_type() const { return "button"; }

View File

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

View File

@@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
using namespace esphome::climate; using namespace esphome::climate;
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();
// current_temperature_topic // current_temperature_topic
if (traits.get_supports_current_temperature()) { if (traits.get_supports_current_temperature()) {
@@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// mode_state_topic // mode_state_topic
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
// modes // modes
JsonArray modes = root.createNestedArray(MQTT_MODES); JsonArray modes = root[MQTT_MODES].to<JsonArray>();
// sort array for nice UI in HA // sort array for nice UI in HA
if (traits.supports_mode(CLIMATE_MODE_AUTO)) if (traits.supports_mode(CLIMATE_MODE_AUTO))
modes.add("auto"); modes.add("auto");
@@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// preset_mode_state_topic // preset_mode_state_topic
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
// presets // presets
JsonArray presets = root.createNestedArray("preset_modes"); JsonArray presets = root["preset_modes"].to<JsonArray>();
if (traits.supports_preset(CLIMATE_PRESET_HOME)) if (traits.supports_preset(CLIMATE_PRESET_HOME))
presets.add("home"); presets.add("home");
if (traits.supports_preset(CLIMATE_PRESET_AWAY)) if (traits.supports_preset(CLIMATE_PRESET_AWAY))
@@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// fan_mode_state_topic // fan_mode_state_topic
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
// fan_modes // 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)) if (traits.supports_fan_mode(CLIMATE_FAN_ON))
fan_modes.add("on"); fan_modes.add("on");
if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
@@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// swing_mode_state_topic // swing_mode_state_topic
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
// swing_modes // 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)) if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
swing_modes.add("off"); swing_modes.add("off");
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) 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.state_topic = false;
config.command_topic = false; config.command_topic = false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void MQTTClimateComponent::setup() { void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits(); 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()); 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( return global_mqtt_client->publish_json(
this->get_discovery_topic_(discovery_info), this->get_discovery_topic_(discovery_info),
[this](JsonObject root) { [this](JsonObject root) {
@@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
} }
std::string node_area = App.get_area(); 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(); const auto mac = get_mac_address();
device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
device_info[MQTT_DEVICE_NAME] = node_friendly_name; device_info[MQTT_DEVICE_NAME] = node_friendly_name;
@@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
}, },
this->qos_, discovery_info.retain); this->qos_, discovery_info.retain);
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
uint8_t MQTTComponent::get_qos() const { return this->qos_; } 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) { 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()) if (!this->cover_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();

View File

@@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
void MQTTDateComponent::setup() { void MQTTDateComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->date_->make_call(); auto call = this->date_->make_call();
if (root.containsKey("year")) { if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]); call.set_year(root["year"]);
} }
if (root.containsKey("month")) { if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]); call.set_month(root["month"]);
} }
if (root.containsKey("day")) { if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]); call.set_day(root["day"]);
} }
call.perform(); call.perform();
@@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
} }
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { 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) { 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["year"] = year;
root["month"] = month; root["month"] = month;
root["day"] = day; root["day"] = day;

View File

@@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
void MQTTDateTimeComponent::setup() { void MQTTDateTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->datetime_->make_call(); auto call = this->datetime_->make_call();
if (root.containsKey("year")) { if (root["year"].is<uint16_t>()) {
call.set_year(root["year"]); call.set_year(root["year"]);
} }
if (root.containsKey("month")) { if (root["month"].is<uint8_t>()) {
call.set_month(root["month"]); call.set_month(root["month"]);
} }
if (root.containsKey("day")) { if (root["day"].is<uint8_t>()) {
call.set_day(root["day"]); call.set_day(root["day"]);
} }
if (root.containsKey("hour")) { if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]); call.set_hour(root["hour"]);
} }
if (root.containsKey("minute")) { if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]); call.set_minute(root["minute"]);
} }
if (root.containsKey("second")) { if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]); call.set_second(root["second"]);
} }
call.perform(); 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, bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) { uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { 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["year"] = year;
root["month"] = month; root["month"] = month;
root["day"] = day; root["day"] = day;

View File

@@ -16,7 +16,8 @@ using namespace esphome::event;
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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()) for (const auto &event_type : this->event_->get_event_types())
event_types.add(event_type); event_types.add(event_type);
@@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
} }
bool MQTTEventComponent::publish_event_(const std::string &event_type) { bool MQTTEventComponent::publish_event_(const std::string &event_type) {
return this->publish_json(this->get_state_topic_(), return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});
} }
std::string MQTTEventComponent::component_type() const { return "event"; } 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(); } bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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()) { if (this->state_->get_traits().supports_direction()) {
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_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) {} MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() { bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(), return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});
} }
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root["schema"] = "json"; root["schema"] = "json";
auto traits = this->state_->get_traits(); auto traits = this->state_->get_traits();
root[MQTT_COLOR_MODE] = true; 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)) if (traits.supports_color_mode(ColorMode::ON_OFF))
color_modes.add("onoff"); color_modes.add("onoff");
if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
@@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (this->state_->supports_effects()) { if (this->state_->supports_effects()) {
root["effect"] = true; 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()) for (auto *effect : this->state_->get_effects())
effect_list.add(effect->get_name()); effect_list.add(effect->get_name());
effect_list.add("None"); effect_list.add("None");

View File

@@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
std::string MQTTLockComponent::component_type() const { return "lock"; } std::string MQTTLockComponent::component_type() const { return "lock"; }
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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; root[MQTT_OPTIMISTIC] = true;
}
if (this->lock_->traits.get_supports_open()) if (this->lock_->traits.get_supports_open())
root[MQTT_PAYLOAD_OPEN] = "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) { void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = number_->traits; const auto &traits = number_->traits;
// https://www.home-assistant.io/integrations/number.mqtt/ // 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_MIN] = traits.get_min_value();
root[MQTT_MAX] = traits.get_max_value(); root[MQTT_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step(); 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) { void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
const auto &traits = select_->traits; const auto &traits = select_->traits;
// https://www.home-assistant.io/integrations/select.mqtt/ // 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()) for (const auto &option : traits.get_options())
options.add(option); 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::disable_expire_after() { this->expire_after_ = 0; }
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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(); root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
if (!this->sensor_->get_unit_of_measurement().empty()) if (!this->sensor_->get_unit_of_measurement().empty())
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); 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"; } std::string MQTTSwitchComponent::component_type() const { return "switch"; }
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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; root[MQTT_OPTIMISTIC] = true;
}
} }
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } 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_; } const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
switch (this->text_->traits.get_mode()) { switch (this->text_->traits.get_mode()) {
case TEXT_MODE_TEXT: case TEXT_MODE_TEXT:
root[MQTT_MODE] = "text"; root[MQTT_MODE] = "text";

View File

@@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { 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(); root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
}
config.command_topic = false; config.command_topic = false;
} }
void MQTTTextSensor::setup() { void MQTTTextSensor::setup() {

View File

@@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
void MQTTTimeComponent::setup() { void MQTTTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->time_->make_call(); auto call = this->time_->make_call();
if (root.containsKey("hour")) { if (root["hour"].is<uint8_t>()) {
call.set_hour(root["hour"]); call.set_hour(root["hour"]);
} }
if (root.containsKey("minute")) { if (root["minute"].is<uint8_t>()) {
call.set_minute(root["minute"]); call.set_minute(root["minute"]);
} }
if (root.containsKey("second")) { if (root["second"].is<uint8_t>()) {
call.set_second(root["second"]); call.set_second(root["second"]);
} }
call.perform(); call.perform();
@@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
} }
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { 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) { 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["hour"] = hour;
root["minute"] = minute; root["minute"] = minute;
root["second"] = second; root["second"] = second;

View File

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

View File

@@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
} }
} }
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &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(); root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
}
auto traits = this->valve_->get_traits(); auto traits = this->valve_->get_traits();
if (traits.get_is_assumed_state()) { 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%] // map 16 bit humidity value into range [-6%, 118%]
float const humidity_partial = double(humidity) / (1 << 16); 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 = float const compensated_humidity_percentage =
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);

View File

@@ -2,7 +2,7 @@ import logging
from esphome import automation from esphome import automation
import esphome.codegen as cg 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.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import ( from esphome.components.image import (
CONF_INVERT_ALPHA, CONF_INVERT_ALPHA,
@@ -11,6 +11,7 @@ from esphome.components.image import (
Image_, Image_,
get_image_type_enum, get_image_type_enum,
get_transparency_enum, get_transparency_enum,
validate_settings,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
host=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]), get_image_type_enum(config[CONF_TYPE]),
transparent, transparent,
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
) )
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) 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, 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), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size), download_buffer_initial_size_(download_buffer_size),
format_(format), format_(format),
fixed_width_(width), fixed_width_(width),
fixed_height_(height) { fixed_height_(height),
is_big_endian_(is_big_endian) {
this->set_url(url); this->set_url(url);
} }
@@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
break; break;
} }
case ImageType::IMAGE_TYPE_GRAYSCALE: { 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 (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) { if (gray == 1) {
gray = 0; gray = 0;
@@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
case ImageType::IMAGE_TYPE_RGB565: { case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color); this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color); uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); if (this->is_big_endian_) {
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); 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) { if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w; 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. * @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, 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; 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_; const int fixed_width_;
/** height requested on configuration, or 0 if non specified. */ /** height requested on configuration, or 0 if non specified. */
const int fixed_height_; 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, * 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 * 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_); 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_ this->state = state < 0.003 && this->zero_means_zero_
? 0.0 ? 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; this->has_state_ = true;
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
} }

View File

@@ -44,6 +44,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } 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) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#ifdef USE_WIFI #ifdef USE_WIFI
WiFi.macAddress(mac); WiFi.macAddress(mac);

View File

@@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
value = clamp(value, -1.0f, 1.0f); value = clamp(value, -1.0f, 1.0f);
float level; float level;
if (value < 0.0) { if (value < 0.0) {
level = lerp(-value, this->idle_level_, this->min_level_); level = std::lerp(this->idle_level_, this->min_level_, -value);
} else { } 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->output_->set_level(level);
this->current_value_ = value; 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)) { if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
this->delete_tasks_(); this->delete_tasks_();
if (this->hard_stop_) { 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->speaker_->stop();
this->hard_stop_ = false; this->hard_stop_ = false;
} else { } else {
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
} }
} }
this->is_playing_ = false; this->is_playing_ = false;
return AudioPipelineState::STOPPED; if (!this->speaker_->is_running()) {
return AudioPipelineState::STOPPED;
} else {
this->is_finishing_ = true;
}
} }
if (this->pause_state_) { if (this->pause_state_) {
return AudioPipelineState::PAUSED; 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)) { if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
// No tasks are running, so the pipeline is stopped. // No tasks are running, so the pipeline is stopped.
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);

View File

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

View File

@@ -151,8 +151,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
if sub is not None: if sub is not None:
item[k] = sub item[k] = sub
for old, new in replace_keys: for old, new in replace_keys:
item[new] = merge_config(item.get(old), item.get(new)) if str(new) == str(old):
del item[old] item[new] = item[old]
else:
item[new] = merge_config(item.get(old), item.get(new))
del item[old]
elif isinstance(item, str): elif isinstance(item, str):
sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing)
if isinstance(sub, JinjaStr) or sub != item: if isinstance(sub, JinjaStr) or sub != item:

View File

@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
temp_ring_buffer->write((void *) data.data(), data.size()); 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; } 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_; msg.wake_word_phrase = this->wake_word_;
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)) { if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
ESP_LOGW(TAG, "Could not request start"); ESP_LOGW(TAG, "Could not request start");
this->error_trigger_->trigger("not-connected", "Could not request start"); this->error_trigger_->trigger("not-connected", "Could not request start");
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) { 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_) { if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
// Announcement has started playing, wait for it to finish this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
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;
this->cancel_timeout("playing"); this->cancel_timeout("playing");
ESP_LOGD(TAG, "Announcement finished playing"); ESP_LOGD(TAG, "Announcement finished playing");
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
break; break;
case State::AWAITING_RESPONSE: case State::AWAITING_RESPONSE:
this->signal_stop_(); this->signal_stop_();
// Fallthrough intended to stop a streaming TTS announcement that has potentially started break;
case State::STREAMING_RESPONSE: case State::STREAMING_RESPONSE:
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
// Stop any ongoing media player announcement // Stop any ongoing media player announcement
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
.set_announcement(true) .set_announcement(true)
.perform(); .perform();
} }
if (this->started_streaming_tts_) {
// Haven't reached the TTS_END stage, so send the stop signal to HA.
this->signal_stop_();
}
#endif #endif
break; break;
case State::RESPONSE_FINISHED: case State::RESPONSE_FINISHED:
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (this->media_player_ != nullptr) { if (this->media_player_ != nullptr) {
for (const auto &arg : msg.data) { for (const auto &arg : msg.data) {
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { 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_->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->started_streaming_tts_ = true;
this->start_playback_timeout_();
tts_url_for_trigger = this->tts_response_url_; tts_url_for_trigger = this->tts_response_url_;
this->tts_response_url_.clear(); // Reset streaming 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]() { this->defer([this, url]() {
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { 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_->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->start_playback_timeout_();
} }
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
#endif #endif
this->tts_end_trigger_->trigger(url); this->tts_end_trigger_->trigger(url);
}); });
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; 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; break;
} }
case api::enums::VOICE_ASSISTANT_RUN_END: { case api::enums::VOICE_ASSISTANT_RUN_END: {
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) { if (this->media_player_ != nullptr) {
this->tts_start_trigger_->trigger(msg.text); this->tts_start_trigger_->trigger(msg.text);
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
if (!msg.preannounce_media_id.empty()) { if (!msg.preannounce_media_id.empty()) {
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform(); 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(); .perform();
this->continue_conversation_ = msg.start_conversation; 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_(); this->start_playback_timeout_();
if (this->continuous_) { if (this->continuous_) {

View File

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

View File

@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
return config return config
def validate_ota_removed(config: ConfigType) -> ConfigType: def validate_ota(config: ConfigType) -> ConfigType:
# Only raise error if OTA is explicitly enabled (True) # The OTA option only accepts False to explicitly disable OTA for web_server
# If it's False or not specified, we can safely ignore it # IMPORTANT: Setting ota: false ONLY affects the web_server component
if config.get(CONF_OTA): # 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( raise cv.Invalid(
f"The '{CONF_OTA}' option has been removed from 'web_server'. " f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
f"Please use the new OTA platform structure instead:\n\n" f"To enable OTA, please use the new OTA platform structure instead:\n\n"
f"ota:\n" f"ota:\n"
f" - platform: web_server\n\n" f" - platform: web_server\n\n"
f"See https://esphome.io/components/ota for more information." f"See https://esphome.io/components/ota for more information."
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
web_server_base.WebServerBase web_server_base.WebServerBase
), ),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, 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_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean,
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
default_url, default_url,
validate_local, validate_local,
validate_sorting_groups, 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_css_url(config[CONF_CSS_URL]))
cg.add(var.set_js_url(config[CONF_JS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL]))
# OTA is now handled by the web_server OTA platform # 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])) cg.add(var.set_expose_log(config[CONF_LOG]))
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
cg.add_define("USE_WEBSERVER_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/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/components/captive_portal/captive_portal.h"
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <Updater.h> #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, void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override; bool final) override;
bool canHandle(AsyncWebServerRequest *request) const 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) // NOLINTNEXTLINE(readability-identifier-naming)
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// Finalize // Finalize
if (final) { 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=%u, contentLength=%zu", index, len,
this->ota_read_length_, request->contentLength()); this->ota_read_length_, request->contentLength());
// For Arduino framework, the Update library tracks expected size from firmware header // 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) { return json::build_json([this](JsonObject root) {
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root["comment"] = App.get_comment(); root["comment"] = App.get_comment();
#ifdef USE_WEBSERVER_OTA #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
root["ota"] = true; // web_server OTA platform is configured root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
#else #else
root["ota"] = false; root["ota"] = true;
#endif #endif
root["log"] = this->expose_log_; root["log"] = this->expose_log_;
root["lang"] = "en"; root["lang"] = "en";
@@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
light::LightJSONSchema::dump_json(*obj, root); light::LightJSONSchema::dump_json(*obj, root);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("effects"); JsonArray opt = root["effects"].to<JsonArray>();
opt.add("None"); opt.add("None");
for (auto const &option : obj->get_effects()) { for (auto const &option : obj->get_effects()) {
opt.add(option->get_name()); 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) { 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); set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option"); JsonArray opt = root["option"].to<JsonArray>();
for (auto &option : obj->traits.get_options()) { for (auto &option : obj->traits.get_options()) {
opt.add(option); 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); return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
} }
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
const auto traits = obj->get_traits(); const auto traits = obj->get_traits();
@@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
char buf[16]; char buf[16];
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("modes"); JsonArray opt = root["modes"].to<JsonArray>();
for (climate::ClimateMode m : traits.get_supported_modes()) for (climate::ClimateMode m : traits.get_supported_modes())
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
if (!traits.get_supported_custom_fan_modes().empty()) { 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()) for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
} }
if (!traits.get_supported_custom_fan_modes().empty()) { 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()) for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
opt.add(custom_fan_mode); opt.add(custom_fan_mode);
} }
if (traits.get_supports_swing_modes()) { 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()) for (auto swing_mode : traits.get_supported_swing_modes())
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
} }
if (traits.get_supports_presets() && obj->preset.has_value()) { 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()) for (climate::ClimatePreset m : traits.get_supported_presets())
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
} }
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { 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()) for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset); opt.add(custom_preset);
} }
@@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
root["state"] = root["target_temperature"]; root["state"] = root["target_temperature"];
} }
}); });
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
#endif #endif
@@ -1618,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
request->send(404); 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) { std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(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; root["event_type"] = event_type;
} }
if (start_config == DETAIL_ALL) { 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()) { for (auto const &event_type : obj->get_event_types()) {
event_types.add(event_type); 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); return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return json::build_json([this, obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
root["value"] = obj->update_info.latest_version; 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); this->add_sorting_info_(root, obj);
} }
}); });
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
#endif #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 " 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>")); "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 " 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>")); "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
#endif #endif

View File

@@ -40,4 +40,4 @@ async def to_code(config):
if CORE.is_esp8266: if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None) cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json # 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 #ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) { for (auto &group : ws->sorting_groups_) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
message = json::build_json([group](JsonObject root) { message = json::build_json([group](JsonObject root) {
root["name"] = group.second.name; root["name"] = group.second.name;
root["sorting_weight"] = group.second.weight; 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 // 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 // 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/dns.h"
#include "lwip/err.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/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.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()) { if (!manual_ip.has_value()) {
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!) // sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591 // https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526 // https://github.com/espressif/arduino-esp32/issues/10526
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING {
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { LwIPLock lock;
LOCK_TCPIP_CORE(); // 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 // No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) { if (dhcp_status != ESP_NETIF_DHCP_STARTED) {

View File

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

View File

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

View File

@@ -309,6 +309,12 @@ void Application::disable_component_loop_(Component *component) {
if (this->in_loop_ && i == this->current_loop_index_) { if (this->in_loop_ && i == this->current_loop_index_) {
// Decrement so we'll process the swapped component next // Decrement so we'll process the swapped component next
this->current_loop_index_--; 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; return;

View File

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

View File

@@ -138,7 +138,7 @@ void Component::call_dump_config() {
} }
} }
} }
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg); ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg);
} }
} }
@@ -191,7 +191,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
return false; return false;
} }
void Component::mark_failed() { void Component::mark_failed() {
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_FAILED; this->component_state_ |= COMPONENT_STATE_FAILED;
this->status_set_error(); this->status_set_error();
@@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
} }
void Component::reset_to_construction_state() { void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
// Clear error status when resetting // Clear error status when resetting
@@ -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)); App.scheduler.set_timeout(this, name, 0, std::move(f));
} }
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT 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 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, void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
float backoff_increase_factor) { // NOLINT 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_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const { bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || 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; (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
} }
bool Component::can_proceed() { return true; } bool Component::can_proceed() { return true; }
@@ -275,14 +276,14 @@ void Component::status_set_warning(const char *message) {
return; return;
this->component_state_ |= STATUS_LED_WARNING; this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message);
} }
void Component::status_set_error(const char *message) { void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0) if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return; return;
this->component_state_ |= STATUS_LED_ERROR; this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message);
if (strcmp(message, "unspecified") != 0) { if (strcmp(message, "unspecified") != 0) {
// Lazy allocate the error messages vector if needed // Lazy allocate the error messages vector if needed
if (!component_error_messages) { if (!component_error_messages) {
@@ -303,13 +304,13 @@ void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0) if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return; return;
this->component_state_ &= ~STATUS_LED_WARNING; this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source());
} }
void Component::status_clear_error() { void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0) if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return; return;
this->component_state_ &= ~STATUS_LED_ERROR; this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source());
} }
void Component::status_momentary_warning(const std::string &name, uint32_t length) { void Component::status_momentary_warning(const std::string &name, uint32_t length) {
this->status_set_warning(); this->status_set_warning();
@@ -403,7 +404,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
} }
if (should_warn) { if (should_warn) {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms"); ESP_LOGW(TAG, "Components should block for at most 30 ms");
} }

View File

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

View File

@@ -683,6 +683,23 @@ class InterruptLock {
#endif #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. /** 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 * Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
@@ -783,7 +800,7 @@ template<class T> class RAMAllocator {
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
T *reallocate(T *p, size_t n, size_t manual_size) { T *reallocate(T *p, size_t n, size_t manual_size) {
size_t size = n * sizeof(T); size_t size = n * manual_size;
T *ptr = nullptr; T *ptr = nullptr;
#ifdef USE_ESP32 #ifdef USE_ESP32
if (this->flags_ & Flags::ALLOC_EXTERNAL) { if (this->flags_ & Flags::ALLOC_EXTERNAL) {

View File

@@ -1,17 +1,12 @@
#pragma once #pragma once
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32)
#include <atomic> #include <atomic>
#include <cstddef> #include <cstddef>
#if defined(USE_ESP32)
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.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. * 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 } // 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 // 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) { 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 // Early return if name is invalid - no items to cancel
if (name_cstr == nullptr || name_cstr[0] == '\0') { if (name_cstr == nullptr) {
return false; return false;
} }

View File

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

View File

@@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
os.environ.setdefault( os.environ.setdefault(
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) "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) cmd = ["platformio"] + list(args)
if not CORE.verbose: if not CORE.verbose:

View File

@@ -147,6 +147,13 @@ class RedirectText:
continue continue
self._write_color_replace(line) 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: else:
self._write_color_replace(s) self._write_color_replace(s)
@@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
result.sort(key=lambda x: x.path) result.sort(key=lambda x: x.path)
return result 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 # Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) 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 = "[platformio]\n"
content += f"description = ESPHome {__version__}\n" content += f"description = ESPHome {__version__}\n"
@@ -222,6 +225,9 @@ def write_platformio_project():
write_gitignore() write_gitignore()
write_platformio_ini(content) write_platformio_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
#pragma once #pragma once
@@ -394,3 +400,20 @@ def write_gitignore():
if not os.path.isfile(path): if not os.path.isfile(path):
with open(file=path, mode="w", encoding="utf-8") as f: with open(file=path, mode="w", encoding="utf-8") as f:
f.write(GITIGNORE_CONTENT) 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 = lib_deps =
esphome/noise-c@0.1.10 ; api esphome/noise-c@0.1.10 ; api
improv/Improv@1.2.4 ; improv_serial / esp32_improv 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 wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
@@ -138,7 +138,7 @@ lib_deps =
WiFi ; wifi,web_server_base,ethernet (Arduino built-in) WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in) Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps} ${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) NetworkClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in) ESPmDNS ; mdns (Arduino built-in)
@@ -235,7 +235,7 @@ build_flags =
-DUSE_ZEPHYR -DUSE_ZEPHYR
-DUSE_NRF52 -DUSE_NRF52
lib_deps = lib_deps =
bblanchon/ArduinoJson@7.0.0 ; json bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393

View File

@@ -0,0 +1,69 @@
"""Tests for the GPIO binary sensor component."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
def test_gpio_binary_sensor_basic_setup(
generate_main: Callable[[str | Path], str],
) -> None:
"""
When the GPIO binary sensor is set in the yaml file, it should be registered in main
"""
main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
assert "new gpio::GPIOBinarySensor();" in main_cpp
assert "App.register_binary_sensor" in main_cpp
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt(
generate_main: Callable[[str | Path], str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""
Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning
"""
main_cpp = generate_main(
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
)
# Check that interrupt is disabled for GPIO16
assert "bs_gpio16->set_use_interrupt(false);" in main_cpp
# Check that the warning was logged
assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text
assert "Falling back to polling mode" in caplog.text
def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
generate_main: Callable[[str | Path], str],
) -> None:
"""
Test that ESP8266 pins other than GPIO16 still use interrupt mode
"""
main_cpp = generate_main(
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
)
# GPIO5 should still use interrupts
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
def test_gpio_binary_sensor_explicit_polling_mode(
generate_main: Callable[[str | Path], str],
) -> None:
"""
Test that explicitly setting use_interrupt: false works
"""
main_cpp = generate_main(
"tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml"
)
assert "bs_polling->set_use_interrupt(false);" in main_cpp

View File

@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
board: esp32dev
binary_sensor:
- platform: gpio
pin: 5
name: "Test GPIO Binary Sensor"
id: bs_gpio

View File

@@ -0,0 +1,20 @@
esphome:
name: test
esp8266:
board: d1_mini
binary_sensor:
- platform: gpio
pin:
number: 16
mode: INPUT_PULLDOWN_16
name: "GPIO16 Touch Sensor"
id: bs_gpio16
- platform: gpio
pin:
number: 5
mode: INPUT_PULLUP
name: "GPIO5 Button"
id: bs_gpio5

View File

@@ -0,0 +1,12 @@
esphome:
name: test
esp32:
board: esp32dev
binary_sensor:
- platform: gpio
pin: 5
name: "Polling Mode Sensor"
id: bs_polling
use_interrupt: false

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ esp_ldo:
- id: ldo_4 - id: ldo_4
channel: 4 channel: 4
voltage: 2.0V voltage: 2.0V
setup_priority: 900
esphome: esphome:
on_boot: on_boot:

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

@@ -919,21 +919,27 @@ lvgl:
text_color: 0xFFFFFF text_color: 0xFFFFFF
scales: scales:
- ticks: - ticks:
width: 1 width: !lambda return 1;
count: 61 count: 61
length: 20 length: 20%
color: 0xFFFFFF color: 0xFFFFFF
range_from: 0 range_from: 0
range_to: 60 range_to: 60
angle_range: 360 angle_range: 360
rotation: 270 rotation: !lambda return 2700;
indicators: indicators:
- tick_style:
start_value: 0
end_value: 60
color_start: 0x0000bd
color_end: 0xbd0000
width: !lambda return 1;
- line: - line:
opa: 50% opa: 50%
id: minute_hand id: minute_hand
color: 0xFF0000 color: 0xFF0000
r_mod: -1 r_mod: !lambda return -1;
width: 3 width: !lambda return 3;
- -
angle_range: 330 angle_range: 330
rotation: 300 rotation: 300

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 from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv 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"] CODEOWNERS = ["@esphome/tests"]
@@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
LoopTestISRComponent = loop_test_component_ns.class_( LoopTestISRComponent = loop_test_component_ns.class_(
"LoopTestISRComponent", cg.Component "LoopTestISRComponent", cg.Component
) )
LoopTestUpdateComponent = loop_test_component_ns.class_(
"LoopTestUpdateComponent", cg.PollingComponent
)
CONF_DISABLE_AFTER = "disable_after" CONF_DISABLE_AFTER = "disable_after"
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
CONF_ISR_COMPONENTS = "isr_components" CONF_ISR_COMPONENTS = "isr_components"
CONF_UPDATE_COMPONENTS = "update_components"
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
COMPONENT_CONFIG_SCHEMA = cv.Schema( 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( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(LoopTestComponent), cv.GenerateID(): cv.declare_id(LoopTestComponent),
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), 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_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) ).extend(cv.COMPONENT_SCHEMA)
@@ -94,3 +111,12 @@ async def to_code(config):
var = cg.new_Pvariable(isr_config[CONF_ID]) var = cg.new_Pvariable(isr_config[CONF_ID])
await cg.register_component(var, isr_config) await cg.register_component(var, isr_config)
cg.add(var.set_name(isr_config[CONF_NAME])) 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(); 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 loop_test_component
} // namespace esphome } // namespace esphome

View File

@@ -4,6 +4,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
namespace loop_test_component { namespace loop_test_component {
@@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
LoopTestComponent *parent_; 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 loop_test_component
} // namespace esphome } // namespace esphome

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