mirror of
https://github.com/esphome/esphome.git
synced 2025-11-17 15:26:01 +00:00
Compare commits
65 Commits
2025.7.0b3
...
2025.7.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6b222c370 | ||
|
|
573dad1736 | ||
|
|
3a6cc0ea3d | ||
|
|
2f9475a927 | ||
|
|
8dce7b0905 | ||
|
|
8b0ad3072f | ||
|
|
93028a4d90 | ||
|
|
c9793f3741 | ||
|
|
2b5cceda58 | ||
|
|
dc26ed9c46 | ||
|
|
8674012406 | ||
|
|
ae12deff87 | ||
|
|
cb6acfe24b | ||
|
|
fc8c5a7438 | ||
|
|
f8777d3b66 | ||
|
|
76e75f4cdc | ||
|
|
896d7f8f76 | ||
|
|
d92ee563f2 | ||
|
|
d6ff790823 | ||
|
|
7ac60c15dc | ||
|
|
6fe4ffa0cf | ||
|
|
576ce7ee35 | ||
|
|
8a45e877bb | ||
|
|
84607c1255 | ||
|
|
8664ec0a3b | ||
|
|
32d8c60a0b | ||
|
|
976a1e27b4 | ||
|
|
cc2c1b1d89 | ||
|
|
85495d38b7 | ||
|
|
84a77ee427 | ||
|
|
11a4115e30 | ||
|
|
121ed687f3 | ||
|
|
c602f3082e | ||
|
|
4a43f922c6 | ||
|
|
21e66b76e4 | ||
|
|
cdeed7afa7 | ||
|
|
1a9f02fa63 | ||
|
|
7ad1b039f9 | ||
|
|
e255d73c29 | ||
|
|
46f5c44b37 | ||
|
|
9d80889bc9 | ||
|
|
08a5ba6ef1 | ||
|
|
28128c65e5 | ||
|
|
efcad565ee | ||
|
|
cd987feb5b | ||
|
|
5707389faa | ||
|
|
3f78db5c63 | ||
|
|
de0656a188 | ||
|
|
90a16ffa89 | ||
|
|
4182076f64 | ||
|
|
8c8c08d40c | ||
|
|
18e2f41424 | ||
|
|
bd0fe34b14 | ||
|
|
37982290f7 | ||
|
|
02b7db7311 | ||
|
|
9bc3ff5f53 | ||
|
|
786cb7ded5 | ||
|
|
7f01c25782 | ||
|
|
321f2f87b0 | ||
|
|
11a051401f | ||
|
|
6148dd7e41 | ||
|
|
42b6939e90 | ||
|
|
35b3f75f7c | ||
|
|
78e8001aa8 | ||
|
|
84fc6ff71a |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.7.0b3
|
||||
PROJECT_NUMBER = 2025.7.4
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -11,6 +11,18 @@ namespace esphome {
|
||||
namespace api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
return val ? std::string(val) : std::string();
|
||||
} // For lambdas returning char* (e.g., itoa)
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
|
||||
@@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||
TemplatableStringValue(F f)
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
|
||||
};
|
||||
|
||||
template<typename... Ts> class TemplatableKeyValuePair {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/as3935/as3935.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as3935_spi {
|
||||
|
||||
@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||
elif CORE.is_esp8266:
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
|
||||
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
|
||||
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.positive_time_period_minutes,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x76)),
|
||||
cv.only_with_arduino,
|
||||
cv.only_with_framework(
|
||||
frameworks=Framework.ARDUINO,
|
||||
suggestions={
|
||||
Framework.ESP_IDF: (
|
||||
"bme68x_bsec2_i2c",
|
||||
"sensor/bme68x_bsec2",
|
||||
)
|
||||
},
|
||||
),
|
||||
cv.Any(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <lwip/igmp.h>
|
||||
#include <lwip/init.h>
|
||||
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
|
||||
ip4_addr_t multicast_addr =
|
||||
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
||||
|
||||
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
err_t err;
|
||||
{
|
||||
LwIPLock lock;
|
||||
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
|
||||
@@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
|
||||
if (listen_method_ == E131_MULTICAST) {
|
||||
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
||||
|
||||
LwIPLock lock;
|
||||
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
#include "lwip/priv/tcpip_priv.h"
|
||||
#endif
|
||||
|
||||
LwIPLock::LwIPLock() {
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
|
||||
// its internal state. Any thread can take this lock to safely access lwIP APIs.
|
||||
//
|
||||
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
|
||||
// already holds the lwIP core lock. This prevents recursive locking attempts and
|
||||
// allows nested LwIPLock instances to work correctly.
|
||||
//
|
||||
// If we don't already hold the lock, acquire it. This will block until the lock
|
||||
// is available if another thread currently holds it.
|
||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
LOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
LwIPLock::~LwIPLock() {
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
// Only release the lwIP core lock if this thread currently holds it.
|
||||
//
|
||||
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
|
||||
// ownership tracking. It returns true only if the current thread is registered
|
||||
// as the lock holder.
|
||||
//
|
||||
// This check is essential because:
|
||||
// 1. We may not have acquired the lock in the constructor (if we already held it)
|
||||
// 2. The lock might have been released by other means between constructor and destructor
|
||||
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
|
||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
@@ -8,6 +10,7 @@ from esphome.const import (
|
||||
CONF_CONTRAST,
|
||||
CONF_DATA_PINS,
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_PIN,
|
||||
@@ -20,6 +23,9 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
import esphome.final_validate as fv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
@@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
if CONF_I2C_PINS not in config:
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
if fconf.get(CONF_I2C):
|
||||
raise cv.Invalid(
|
||||
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
SETTERS = {
|
||||
# pin assignment
|
||||
CONF_DATA_PINS: "set_data_pins",
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
@@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() {
|
||||
|
||||
// Configure each touch pad
|
||||
for (auto *child : this->children_) {
|
||||
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
||||
if (this->setup_mode_) {
|
||||
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
|
||||
} else {
|
||||
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Register ISR handler
|
||||
@@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
|
||||
child->publish_state(new_state);
|
||||
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
||||
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), event.value, child->get_threshold());
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
|
||||
}
|
||||
break; // Exit inner loop after processing matching pad
|
||||
}
|
||||
@@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
// as any pad remains touched. This allows us to detect both new touches and
|
||||
// continued touches, but releases must be detected by timeout in the main loop.
|
||||
|
||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||
// Therefore: touched = (value < threshold)
|
||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||
|
||||
// Process all configured pads to check their current state
|
||||
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
||||
// so we must scan all configured pads to find which ones were touched
|
||||
@@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
}
|
||||
|
||||
// Skip pads that aren’t in the trigger mask
|
||||
bool is_touched = (mask >> pad) & 1;
|
||||
if (!is_touched) {
|
||||
if (((mask >> pad) & 1) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||
// Therefore: touched = (value < threshold)
|
||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||
bool is_touched = value < child->get_threshold();
|
||||
|
||||
// Always send the current state - the main loop will filter for changes
|
||||
// We send both touched and untouched states because the ISR doesn't
|
||||
// track previous state (to keep ISR fast and simple)
|
||||
|
||||
@@ -22,6 +22,10 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||
|
||||
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
wifi_get_macaddr(STATION_IF, mac);
|
||||
}
|
||||
|
||||
@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
}
|
||||
|
||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||
LwIPLock lock;
|
||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||
return dns_ip;
|
||||
}
|
||||
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns1;
|
||||
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1 = dns_getserver(0);
|
||||
const ip_addr_t *dns_ip2 = dns_getserver(1);
|
||||
const ip_addr_t *dns_ip1;
|
||||
const ip_addr_t *dns_ip2;
|
||||
{
|
||||
LwIPLock lock;
|
||||
dns_ip1 = dns_getserver(0);
|
||||
dns_ip2 = dns_getserver(1);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
|
||||
@@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
|
||||
return {};
|
||||
}
|
||||
void Fan::save_state_() {
|
||||
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
|
||||
return;
|
||||
}
|
||||
|
||||
FanRestoreState state{};
|
||||
state.state = this->state;
|
||||
state.oscillating = this->oscillating;
|
||||
|
||||
@@ -2,7 +2,13 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import fastled_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER
|
||||
from esphome.const import (
|
||||
CONF_CHIPSET,
|
||||
CONF_NUM_LEDS,
|
||||
CONF_PIN,
|
||||
CONF_RGB_ORDER,
|
||||
Framework,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["fastled_base"]
|
||||
|
||||
@@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
||||
}
|
||||
),
|
||||
_validate,
|
||||
cv.only_with_framework(
|
||||
frameworks=Framework.ARDUINO,
|
||||
suggestions={
|
||||
Framework.ESP_IDF: (
|
||||
"esp32_rmt_led_strip",
|
||||
"light/esp32_rmt_led_strip",
|
||||
)
|
||||
},
|
||||
),
|
||||
cv.require_framework_version(
|
||||
esp8266_arduino=cv.Version(2, 7, 4),
|
||||
esp32_arduino=cv.Version(99, 0, 0),
|
||||
max_version=True,
|
||||
extra_message="Please see note on documentation for FastLED",
|
||||
),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_DATA_RATE,
|
||||
CONF_NUM_LEDS,
|
||||
CONF_RGB_ORDER,
|
||||
Framework,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["fastled_base"]
|
||||
@@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DATA_RATE): cv.frequency,
|
||||
}
|
||||
),
|
||||
cv.only_with_framework(
|
||||
frameworks=Framework.ARDUINO,
|
||||
suggestions={
|
||||
Framework.ESP_IDF: (
|
||||
"spi_led_strip",
|
||||
"light/spi_led_strip",
|
||||
)
|
||||
},
|
||||
),
|
||||
cv.require_framework_version(
|
||||
esp8266_arduino=cv.Version(2, 7, 4),
|
||||
esp32_arduino=cv.Version(99, 0, 0),
|
||||
|
||||
@@ -4,7 +4,13 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
|
||||
from esphome.const import (
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_NUMBER,
|
||||
CONF_PIN,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
from .. import gpio_ns
|
||||
@@ -29,7 +35,21 @@ CONFIG_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
|
||||
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
|
||||
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
||||
# stable operation on these platforms. Future maintainers should verify platform
|
||||
# capabilities before changing this default behavior.
|
||||
cv.SplitDefault(
|
||||
CONF_USE_INTERRUPT,
|
||||
bk72xx=False,
|
||||
esp32=True,
|
||||
esp8266=True,
|
||||
host=True,
|
||||
ln882x=False,
|
||||
nrf52=True,
|
||||
rp2040=True,
|
||||
rtl87xx=False,
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
|
||||
INTERRUPT_TYPES, upper=True
|
||||
),
|
||||
@@ -62,6 +82,18 @@ async def to_code(config):
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
# Check if pin is shared with other components (allow_other_uses)
|
||||
# When a pin is shared, interrupts can interfere with other components
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
||||
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
||||
"The sensor will use polling mode for compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
config[CONF_PIN][CONF_NUMBER],
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
if use_interrupt:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace gt911 {
|
||||
|
||||
static const char *const TAG = "gt911.touchscreen";
|
||||
|
||||
static const uint8_t PRIMARY_ADDRESS = 0x5D; // default I2C address for GT911
|
||||
static const uint8_t SECONDARY_ADDRESS = 0x14; // secondary I2C address for GT911
|
||||
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
|
||||
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
|
||||
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
|
||||
@@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
||||
|
||||
#define ERROR_CHECK(err) \
|
||||
if ((err) != i2c::ERROR_OK) { \
|
||||
ESP_LOGE(TAG, "Failed to communicate!"); \
|
||||
this->status_set_warning(); \
|
||||
this->status_set_warning("Communication failure"); \
|
||||
return; \
|
||||
}
|
||||
|
||||
@@ -30,31 +31,31 @@ void GT911Touchscreen::setup() {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// The interrupt pin is used as an input during reset to select the I2C address.
|
||||
// temporarily set the interrupt pin to output to control address selection
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->digital_write(false);
|
||||
}
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(50); // NOLINT
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
this->interrupt_pin_->setup();
|
||||
}
|
||||
}
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// set pre-configured input mode
|
||||
this->interrupt_pin_->setup();
|
||||
}
|
||||
|
||||
// check the configuration of the int line.
|
||||
uint8_t data[4];
|
||||
err = this->write(GET_SWITCHES, 2);
|
||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
|
||||
this->address_ = SECONDARY_ADDRESS;
|
||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
}
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->read(data, 1);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]);
|
||||
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// datasheet says NOT to use pullup/down on the int line.
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_,
|
||||
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ void GT911Touchscreen::setup() {
|
||||
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
|
||||
// no calibration? Attempt to read the max values from the touchscreen.
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->write(GET_MAX_VALUES, 2);
|
||||
err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES));
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->read(data, sizeof(data));
|
||||
if (err == i2c::ERROR_OK) {
|
||||
@@ -75,15 +76,12 @@ void GT911Touchscreen::setup() {
|
||||
}
|
||||
}
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!");
|
||||
this->mark_failed();
|
||||
this->mark_failed("Failed to read calibration");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to communicate!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
this->mark_failed("Failed to communicate");
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
|
||||
@@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() {
|
||||
uint8_t touch_state = 0;
|
||||
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
|
||||
|
||||
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false);
|
||||
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE));
|
||||
ERROR_CHECK(err);
|
||||
err = this->read(&touch_state, 1);
|
||||
ERROR_CHECK(err);
|
||||
@@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
|
||||
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
|
||||
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES));
|
||||
ERROR_CHECK(err);
|
||||
// num_of_touches is guaranteed to be 0..5. Also read the key data
|
||||
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
|
||||
@@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
}
|
||||
|
||||
} // namespace gt911
|
||||
|
||||
@@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
|
||||
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
@@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||
|
||||
for (auto build : root["builds"].as<JsonArray>()) {
|
||||
if (!build.containsKey("chipFamily")) {
|
||||
if (!build["chipFamily"].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
||||
if (!build.containsKey("ota")) {
|
||||
if (!build["ota"].is<JsonObject>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
auto ota = build["ota"];
|
||||
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
|
||||
JsonObject ota = build["ota"].as<JsonObject>();
|
||||
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
||||
|
||||
if (ota.containsKey("summary"))
|
||||
if (ota["summary"].is<const char *>())
|
||||
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
||||
if (ota.containsKey("release_url"))
|
||||
if (ota["release_url"].is<const char *>())
|
||||
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@coroutine_with_priority(1.0)
|
||||
async def to_code(config):
|
||||
cg.add_library("bblanchon/ArduinoJson", "6.18.5")
|
||||
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
|
||||
cg.add_define("USE_JSON")
|
||||
cg.add_global(json_ns.using)
|
||||
|
||||
@@ -1,83 +1,76 @@
|
||||
#include "json_util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
|
||||
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
static const char *const TAG = "json";
|
||||
|
||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
||||
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
struct SpiRamAllocator : ArduinoJson::Allocator {
|
||||
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
|
||||
|
||||
void deallocate(void *pointer) override {
|
||||
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
|
||||
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
|
||||
// RAMAllocator::deallocate implementation just calls free() regardless of whether
|
||||
// the memory was allocated with heap_caps_malloc or malloc.
|
||||
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
|
||||
// and routes free() to the appropriate heap.
|
||||
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
|
||||
}
|
||||
|
||||
void *reallocate(void *ptr, size_t new_size) override {
|
||||
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
|
||||
}
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
|
||||
};
|
||||
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// Here we are allocating up to 5kb of memory,
|
||||
// with the heap size minus 2kb to be safe if less than 5kb
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
if (request_size == free_heap) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
|
||||
free_heap);
|
||||
return "{}";
|
||||
}
|
||||
request_size = std::min(request_size * 2, free_heap);
|
||||
continue;
|
||||
}
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto doc_allocator = SpiRamAllocator();
|
||||
JsonDocument json_document(&doc_allocator);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return "{}";
|
||||
}
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// Here we are allocating 1.5 times the data size,
|
||||
// with the heap size minus 2kb to be safe if less than that
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
||||
while (true) {
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
|
||||
free_heap);
|
||||
return false;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
json_document.shrinkToFit();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto doc_allocator = SpiRamAllocator();
|
||||
JsonDocument json_document(&doc_allocator);
|
||||
if (json_document.overflowed()) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||
return false;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
|
||||
if (err == DeserializationError::Ok) {
|
||||
return f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
if (request_size * 2 >= free_heap) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
||||
request_size *= 2;
|
||||
continue;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if (err == DeserializationError::Ok) {
|
||||
return f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return false;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -477,10 +477,11 @@ void LD2450Component::handle_periodic_data_() {
|
||||
// X
|
||||
start = TARGET_X + index * 8;
|
||||
is_moving = false;
|
||||
// tx is used for further calculations, so always needs to be populated
|
||||
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
tx = val;
|
||||
sensor::Sensor *sx = this->move_x_sensors_[index];
|
||||
if (sx != nullptr) {
|
||||
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
tx = val;
|
||||
if (this->cached_target_data_[index].x != val) {
|
||||
sx->publish_state(val);
|
||||
this->cached_target_data_[index].x = val;
|
||||
@@ -488,10 +489,11 @@ void LD2450Component::handle_periodic_data_() {
|
||||
}
|
||||
// Y
|
||||
start = TARGET_Y + index * 8;
|
||||
// ty is used for further calculations, so always needs to be populated
|
||||
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
ty = val;
|
||||
sensor::Sensor *sy = this->move_y_sensors_[index];
|
||||
if (sy != nullptr) {
|
||||
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
ty = val;
|
||||
if (this->cached_target_data_[index].y != val) {
|
||||
sy->publish_state(val);
|
||||
this->cached_target_data_[index].y = val;
|
||||
|
||||
@@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
WiFi.macAddress(mac);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace light {
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (state.supports_effects())
|
||||
root["effect"] = state.get_effect_name();
|
||||
|
||||
@@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
|
||||
root["brightness"] = uint8_t(values.get_brightness() * 255);
|
||||
|
||||
JsonObject color = root.createNestedObject("color");
|
||||
JsonObject color = root["color"].to<JsonObject>();
|
||||
if (values.get_color_mode() & ColorCapability::RGB) {
|
||||
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
||||
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
|
||||
@@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
}
|
||||
|
||||
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
|
||||
if (root.containsKey("state")) {
|
||||
if (root["state"].is<const char *>()) {
|
||||
auto val = parse_on_off(root["state"]);
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("brightness")) {
|
||||
if (root["brightness"].is<uint8_t>()) {
|
||||
call.set_brightness(float(root["brightness"]) / 255.0f);
|
||||
}
|
||||
|
||||
if (root.containsKey("color")) {
|
||||
if (root["color"].is<JsonObject>()) {
|
||||
JsonObject color = root["color"];
|
||||
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
|
||||
float max_rgb = 0.0f;
|
||||
if (color.containsKey("r")) {
|
||||
if (color["r"].is<uint8_t>()) {
|
||||
float r = float(color["r"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, r);
|
||||
call.set_red(r);
|
||||
}
|
||||
if (color.containsKey("g")) {
|
||||
if (color["g"].is<uint8_t>()) {
|
||||
float g = float(color["g"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, g);
|
||||
call.set_green(g);
|
||||
}
|
||||
if (color.containsKey("b")) {
|
||||
if (color["b"].is<uint8_t>()) {
|
||||
float b = float(color["b"]) / 255.0f;
|
||||
max_rgb = fmaxf(max_rgb, b);
|
||||
call.set_blue(b);
|
||||
}
|
||||
if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) {
|
||||
if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) {
|
||||
call.set_color_brightness(max_rgb);
|
||||
}
|
||||
|
||||
if (color.containsKey("c")) {
|
||||
if (color["c"].is<uint8_t>()) {
|
||||
call.set_cold_white(float(color["c"]) / 255.0f);
|
||||
}
|
||||
if (color.containsKey("w")) {
|
||||
if (color["w"].is<uint8_t>()) {
|
||||
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
||||
// white channel in RGBWW.
|
||||
if (color.containsKey("c")) {
|
||||
if (color["c"].is<uint8_t>()) {
|
||||
call.set_warm_white(float(color["w"]) / 255.0f);
|
||||
} else {
|
||||
call.set_white(float(color["w"]) / 255.0f);
|
||||
@@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("white_value")) { // legacy API
|
||||
if (root["white_value"].is<uint8_t>()) { // legacy API
|
||||
call.set_white(float(root["white_value"]) / 255.0f);
|
||||
}
|
||||
|
||||
if (root.containsKey("color_temp")) {
|
||||
if (root["color_temp"].is<uint16_t>()) {
|
||||
call.set_color_temperature(float(root["color_temp"]));
|
||||
}
|
||||
}
|
||||
@@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
|
||||
LightJSONSchema::parse_color_json(state, call, root);
|
||||
|
||||
if (root.containsKey("flash")) {
|
||||
if (root["flash"].is<uint32_t>()) {
|
||||
auto length = uint32_t(float(root["flash"]) * 1000);
|
||||
call.set_flash_length(length);
|
||||
}
|
||||
|
||||
if (root.containsKey("transition")) {
|
||||
if (root["transition"].is<uint16_t>()) {
|
||||
auto length = uint32_t(float(root["transition"]) * 1000);
|
||||
call.set_transition_length(length);
|
||||
}
|
||||
|
||||
if (root.containsKey("effect")) {
|
||||
if (root["effect"].is<const char *>()) {
|
||||
const char *effect = root["effect"];
|
||||
call.set_effect(effect);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value):
|
||||
Logger = logger_ns.class_("Logger", cg.Component)
|
||||
LoggerMessageTrigger = logger_ns.class_(
|
||||
"LoggerMessageTrigger",
|
||||
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
|
||||
automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr),
|
||||
)
|
||||
|
||||
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
|
||||
@@ -368,7 +368,7 @@ async def to_code(config):
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.int_, "level"),
|
||||
(cg.uint8, "level"),
|
||||
(cg.const_char_ptr, "tag"),
|
||||
(cg.const_char_ptr, "message"),
|
||||
],
|
||||
@@ -400,6 +400,7 @@ CONF_LOGGER_LOG = "logger.log"
|
||||
LOGGER_LOG_ACTION_SCHEMA = cv.All(
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
|
||||
cv.Required(CONF_FORMAT): cv.string,
|
||||
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||
cv.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(
|
||||
|
||||
@@ -192,7 +192,7 @@ class WidgetType:
|
||||
|
||||
class NumberType(WidgetType):
|
||||
def get_max(self, config: dict):
|
||||
return int(config[CONF_MAX_VALUE] or 100)
|
||||
return int(config.get(CONF_MAX_VALUE, 100))
|
||||
|
||||
def get_min(self, config: dict):
|
||||
return int(config[CONF_MIN_VALUE] or 0)
|
||||
return int(config.get(CONF_MIN_VALUE, 0))
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import IntLiteral
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -188,6 +189,8 @@ class MeterType(WidgetType):
|
||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||
if CONF_ROTATION in scale_conf:
|
||||
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
||||
if isinstance(rotation, IntLiteral):
|
||||
rotation = int(str(rotation)) // 10
|
||||
with LocalVariable(
|
||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||
) as meter_var:
|
||||
@@ -264,7 +267,7 @@ class MeterType(WidgetType):
|
||||
color_start,
|
||||
color_end,
|
||||
v[CONF_LOCAL],
|
||||
size.process(v[CONF_WIDTH]),
|
||||
await size.process(v[CONF_WIDTH]),
|
||||
),
|
||||
)
|
||||
if t == CONF_IMAGE:
|
||||
|
||||
@@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
|
||||
}
|
||||
|
||||
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>();
|
||||
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
||||
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
||||
supported_features.add("arm_away");
|
||||
|
||||
@@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
|
||||
}
|
||||
|
||||
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->binary_sensor_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
|
||||
@@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() {
|
||||
}
|
||||
|
||||
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
config.state_topic = false;
|
||||
if (!this->button_->get_device_class().empty())
|
||||
if (!this->button_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
||||
|
||||
@@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
std::string topic = "esphome/discover/";
|
||||
topic.append(App.get_name());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
this->publish_json(
|
||||
topic,
|
||||
[](JsonObject root) {
|
||||
@@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
},
|
||||
2, this->discovery_info_.retain);
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void MQTTClientComponent::dump_config() {
|
||||
@@ -191,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
|
||||
this->dns_resolve_error_ = false;
|
||||
this->dns_resolved_ = false;
|
||||
ip_addr_t addr;
|
||||
err_t err;
|
||||
{
|
||||
LwIPLock lock;
|
||||
#if USE_NETWORK_IPV6
|
||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||
#else
|
||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||
this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
switch (err) {
|
||||
case ERR_OK: {
|
||||
// Got IP immediately
|
||||
|
||||
@@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
|
||||
using namespace esphome::climate;
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto traits = this->device_->get_traits();
|
||||
// current_temperature_topic
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
@@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// mode_state_topic
|
||||
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
||||
// modes
|
||||
JsonArray modes = root.createNestedArray(MQTT_MODES);
|
||||
JsonArray modes = root[MQTT_MODES].to<JsonArray>();
|
||||
// sort array for nice UI in HA
|
||||
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
||||
modes.add("auto");
|
||||
@@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// preset_mode_state_topic
|
||||
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
||||
// presets
|
||||
JsonArray presets = root.createNestedArray("preset_modes");
|
||||
JsonArray presets = root["preset_modes"].to<JsonArray>();
|
||||
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
||||
presets.add("home");
|
||||
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
||||
@@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// fan_mode_state_topic
|
||||
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
||||
// fan_modes
|
||||
JsonArray fan_modes = root.createNestedArray("fan_modes");
|
||||
JsonArray fan_modes = root["fan_modes"].to<JsonArray>();
|
||||
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
|
||||
fan_modes.add("on");
|
||||
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
||||
@@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// swing_mode_state_topic
|
||||
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
||||
// swing_modes
|
||||
JsonArray swing_modes = root.createNestedArray("swing_modes");
|
||||
JsonArray swing_modes = root["swing_modes"].to<JsonArray>();
|
||||
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
|
||||
swing_modes.add("off");
|
||||
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
|
||||
@@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
|
||||
config.state_topic = false;
|
||||
config.command_topic = false;
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
void MQTTClimateComponent::setup() {
|
||||
auto traits = this->device_->get_traits();
|
||||
|
||||
@@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return global_mqtt_client->publish_json(
|
||||
this->get_discovery_topic_(discovery_info),
|
||||
[this](JsonObject root) {
|
||||
@@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
std::string node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
const auto mac = get_mac_address();
|
||||
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
||||
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
||||
@@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
||||
},
|
||||
this->qos_, discovery_info.retain);
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
||||
|
||||
@@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->cover_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
|
||||
void MQTTDateComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->date_->make_call();
|
||||
if (root.containsKey("year")) {
|
||||
if (root["year"].is<uint16_t>()) {
|
||||
call.set_year(root["year"]);
|
||||
}
|
||||
if (root.containsKey("month")) {
|
||||
if (root["month"].is<uint8_t>()) {
|
||||
call.set_month(root["month"]);
|
||||
}
|
||||
if (root.containsKey("day")) {
|
||||
if (root["day"].is<uint8_t>()) {
|
||||
call.set_day(root["day"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["year"] = year;
|
||||
root["month"] = month;
|
||||
root["day"] = day;
|
||||
|
||||
@@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
|
||||
void MQTTDateTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->datetime_->make_call();
|
||||
if (root.containsKey("year")) {
|
||||
if (root["year"].is<uint16_t>()) {
|
||||
call.set_year(root["year"]);
|
||||
}
|
||||
if (root.containsKey("month")) {
|
||||
if (root["month"].is<uint8_t>()) {
|
||||
call.set_month(root["month"]);
|
||||
}
|
||||
if (root.containsKey("day")) {
|
||||
if (root["day"].is<uint8_t>()) {
|
||||
call.set_day(root["day"]);
|
||||
}
|
||||
if (root.containsKey("hour")) {
|
||||
if (root["hour"].is<uint8_t>()) {
|
||||
call.set_hour(root["hour"]);
|
||||
}
|
||||
if (root.containsKey("minute")) {
|
||||
if (root["minute"].is<uint8_t>()) {
|
||||
call.set_minute(root["minute"]);
|
||||
}
|
||||
if (root.containsKey("second")) {
|
||||
if (root["second"].is<uint8_t>()) {
|
||||
call.set_second(root["second"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() {
|
||||
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
||||
uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["year"] = year;
|
||||
root["month"] = month;
|
||||
root["day"] = day;
|
||||
|
||||
@@ -16,7 +16,8 @@ using namespace esphome::event;
|
||||
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
||||
|
||||
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>();
|
||||
for (const auto &event_type : this->event_->get_event_types())
|
||||
event_types.add(event_type);
|
||||
|
||||
@@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
|
||||
}
|
||||
|
||||
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
||||
return this->publish_json(this->get_state_topic_(),
|
||||
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; });
|
||||
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_EVENT_TYPE] = event_type;
|
||||
});
|
||||
}
|
||||
|
||||
std::string MQTTEventComponent::component_type() const { return "event"; }
|
||||
|
||||
@@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() {
|
||||
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
||||
|
||||
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
||||
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
||||
|
||||
@@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
|
||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||
|
||||
bool MQTTJSONLightComponent::publish_state_() {
|
||||
return this->publish_json(this->get_state_topic_(),
|
||||
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); });
|
||||
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
LightJSONSchema::dump_json(*this->state_, root);
|
||||
});
|
||||
}
|
||||
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
||||
|
||||
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["schema"] = "json";
|
||||
auto traits = this->state_->get_traits();
|
||||
|
||||
root[MQTT_COLOR_MODE] = true;
|
||||
JsonArray color_modes = root.createNestedArray("supported_color_modes");
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray color_modes = root["supported_color_modes"].to<JsonArray>();
|
||||
if (traits.supports_color_mode(ColorMode::ON_OFF))
|
||||
color_modes.add("onoff");
|
||||
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
||||
@@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
|
||||
if (this->state_->supports_effects()) {
|
||||
root["effect"] = true;
|
||||
JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST);
|
||||
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||
for (auto *effect : this->state_->get_effects())
|
||||
effect_list.add(effect->get_name());
|
||||
effect_list.add("None");
|
||||
|
||||
@@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
|
||||
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
||||
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
||||
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->lock_->traits.get_assumed_state())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->lock_->traits.get_assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
if (this->lock_->traits.get_supports_open())
|
||||
root[MQTT_PAYLOAD_OPEN] = "OPEN";
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_
|
||||
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
const auto &traits = number_->traits;
|
||||
// https://www.home-assistant.io/integrations/number.mqtt/
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_MIN] = traits.get_min_value();
|
||||
root[MQTT_MAX] = traits.get_max_value();
|
||||
root[MQTT_STEP] = traits.get_step();
|
||||
|
||||
@@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_
|
||||
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
const auto &traits = select_->traits;
|
||||
// https://www.home-assistant.io/integrations/select.mqtt/
|
||||
JsonArray options = root.createNestedArray(MQTT_OPTIONS);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonArray options = root[MQTT_OPTIONS].to<JsonArray>();
|
||||
for (const auto &option : traits.get_options())
|
||||
options.add(option);
|
||||
|
||||
|
||||
@@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
|
||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->sensor_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
}
|
||||
|
||||
if (!this->sensor_->get_unit_of_measurement().empty())
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
||||
|
||||
@@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() {
|
||||
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
|
||||
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
||||
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->switch_->assumed_state())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (this->switch_->assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
}
|
||||
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; }
|
||||
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
|
||||
@@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
|
||||
|
||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->sensor_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
}
|
||||
config.command_topic = false;
|
||||
}
|
||||
void MQTTTextSensor::setup() {
|
||||
|
||||
@@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
|
||||
void MQTTTimeComponent::setup() {
|
||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||
auto call = this->time_->make_call();
|
||||
if (root.containsKey("hour")) {
|
||||
if (root["hour"].is<uint8_t>()) {
|
||||
call.set_hour(root["hour"]);
|
||||
}
|
||||
if (root.containsKey("minute")) {
|
||||
if (root["minute"].is<uint8_t>()) {
|
||||
call.set_minute(root["minute"]);
|
||||
}
|
||||
if (root.containsKey("second")) {
|
||||
if (root["second"].is<uint8_t>()) {
|
||||
call.set_second(root["second"]);
|
||||
}
|
||||
call.perform();
|
||||
@@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["hour"] = hour;
|
||||
root["minute"] = minute;
|
||||
root["second"] = second;
|
||||
|
||||
@@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
|
||||
}
|
||||
|
||||
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root["schema"] = "json";
|
||||
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
||||
}
|
||||
|
||||
@@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (!this->valve_->get_device_class().empty())
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->valve_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
||||
}
|
||||
|
||||
auto traits = this->valve_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
|
||||
@@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) {
|
||||
|
||||
// map 16 bit humidity value into range [-6%, 118%]
|
||||
float const humidity_partial = double(humidity) / (1 << 16);
|
||||
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
|
||||
float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial);
|
||||
float const compensated_humidity_percentage =
|
||||
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
||||
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
||||
|
||||
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_TYPE,
|
||||
CONF_VARIANT,
|
||||
Framework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
@@ -162,7 +163,15 @@ def _validate_method(value):
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.only_with_arduino,
|
||||
cv.only_with_framework(
|
||||
frameworks=Framework.ARDUINO,
|
||||
suggestions={
|
||||
Framework.ESP_IDF: (
|
||||
"esp32_rmt_led_strip",
|
||||
"light/esp32_rmt_led_strip",
|
||||
)
|
||||
},
|
||||
),
|
||||
cv.require_framework_version(
|
||||
esp8266_arduino=cv.Version(2, 4, 0),
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_REQUEST_HEADERS
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
|
||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||
from esphome.components.image import (
|
||||
CONF_INVERT_ALPHA,
|
||||
@@ -11,6 +11,7 @@ from esphome.components.image import (
|
||||
Image_,
|
||||
get_image_type_enum,
|
||||
get_transparency_enum,
|
||||
validate_settings,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
host=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_settings,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -213,6 +215,7 @@ async def to_code(config):
|
||||
get_image_type_enum(config[CONF_TYPE]),
|
||||
transparent,
|
||||
config[CONF_BUFFER_SIZE],
|
||||
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
|
||||
@@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) {
|
||||
}
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||
image::Transparency transparency, uint32_t download_buffer_size)
|
||||
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
buffer_(nullptr),
|
||||
download_buffer_(download_buffer_size),
|
||||
download_buffer_initial_size_(download_buffer_size),
|
||||
format_(format),
|
||||
fixed_width_(width),
|
||||
fixed_height_(height) {
|
||||
fixed_height_(height),
|
||||
is_big_endian_(is_big_endian) {
|
||||
this->set_url(url);
|
||||
}
|
||||
|
||||
@@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
@@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
this->map_chroma_key(color);
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
if (this->is_big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
} else {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
}
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent,
|
||||
* @param buffer_size Size of the buffer used to download the image.
|
||||
*/
|
||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||
image::Transparency transparency, uint32_t buffer_size);
|
||||
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
@@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent,
|
||||
const int fixed_width_;
|
||||
/** height requested on configuration, or 0 if non specified. */
|
||||
const int fixed_height_;
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_;
|
||||
/**
|
||||
* Actual width of the current image. If fixed_width_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
|
||||
@@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) {
|
||||
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
|
||||
this->state = state < 0.003 && this->zero_means_zero_
|
||||
? 0.0
|
||||
: clamp(lerp(state, min_value_, max_value_), min_value_, max_value_);
|
||||
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
this->has_state_ = true;
|
||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,20 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
if CORE.is_esp32:
|
||||
variant = esp32.get_esp32_variant()
|
||||
if variant in (esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S2):
|
||||
max_idle = 65535
|
||||
else:
|
||||
max_idle = 32767
|
||||
if CONF_CLOCK_RESOLUTION in config:
|
||||
max_idle = int(max_idle * 1000000 / config[CONF_CLOCK_RESOLUTION])
|
||||
if config[CONF_IDLE].total_microseconds > max_idle:
|
||||
raise cv.Invalid(f"config 'idle' exceeds the maximum value of {max_idle}us")
|
||||
return config
|
||||
|
||||
|
||||
def validate_tolerance(value):
|
||||
if isinstance(value, dict):
|
||||
return TOLERANCE_SCHEMA(value)
|
||||
@@ -136,7 +150,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
||||
cv.boolean,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.add_extra(validate_config)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -86,10 +86,9 @@ void RemoteReceiverComponent::setup() {
|
||||
|
||||
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
|
||||
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
|
||||
uint32_t max_idle_ns = 65535u * 1000;
|
||||
memset(&this->store_.config, 0, sizeof(this->store_.config));
|
||||
this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
|
||||
this->store_.config.signal_range_max_ns = std::min(this->idle_us_ * 1000, max_idle_ns);
|
||||
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
|
||||
this->store_.filter_symbols = this->filter_symbols_;
|
||||
this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t);
|
||||
this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_);
|
||||
|
||||
@@ -44,6 +44,10 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
|
||||
// RP2040 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
#ifdef USE_WIFI
|
||||
WiFi.macAddress(mac);
|
||||
|
||||
@@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
SDL_Rect rect{x, y, 1, 1};
|
||||
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
|
||||
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
|
||||
|
||||
@@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
|
||||
value = clamp(value, -1.0f, 1.0f);
|
||||
float level;
|
||||
if (value < 0.0) {
|
||||
level = lerp(-value, this->idle_level_, this->min_level_);
|
||||
level = std::lerp(this->idle_level_, this->min_level_, -value);
|
||||
} else {
|
||||
level = lerp(value, this->idle_level_, this->max_level_);
|
||||
level = std::lerp(this->idle_level_, this->max_level_, value);
|
||||
}
|
||||
this->output_->set_level(level);
|
||||
this->current_value_ = value;
|
||||
|
||||
@@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() {
|
||||
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
|
||||
this->delete_tasks_();
|
||||
if (this->hard_stop_) {
|
||||
// Stop command was sent, so immediately end of the playback
|
||||
// Stop command was sent, so immediately end the playback
|
||||
this->speaker_->stop();
|
||||
this->hard_stop_ = false;
|
||||
} else {
|
||||
@@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() {
|
||||
}
|
||||
}
|
||||
this->is_playing_ = false;
|
||||
return AudioPipelineState::STOPPED;
|
||||
if (!this->speaker_->is_running()) {
|
||||
return AudioPipelineState::STOPPED;
|
||||
} else {
|
||||
this->is_finishing_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->pause_state_) {
|
||||
return AudioPipelineState::PAUSED;
|
||||
}
|
||||
|
||||
if (this->is_finishing_) {
|
||||
if (!this->speaker_->is_running()) {
|
||||
this->is_finishing_ = false;
|
||||
} else {
|
||||
return AudioPipelineState::PLAYING;
|
||||
}
|
||||
}
|
||||
|
||||
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
|
||||
// No tasks are running, so the pipeline is stopped.
|
||||
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
|
||||
|
||||
@@ -114,6 +114,7 @@ class AudioPipeline {
|
||||
|
||||
bool hard_stop_{false};
|
||||
bool is_playing_{false};
|
||||
bool is_finishing_{false};
|
||||
bool pause_state_{false};
|
||||
bool task_stack_in_psram_;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import fan
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
|
||||
from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
|
||||
|
||||
from .. import CONF_TUYA_ID, Tuya, tuya_ns
|
||||
|
||||
@@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
|
||||
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
fan.FAN_SCHEMA.extend(
|
||||
fan.fan_schema(TuyaFan)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
|
||||
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
|
||||
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
|
||||
@@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
)
|
||||
|
||||
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_TUYA_ID])
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT])
|
||||
var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
|
||||
await cg.register_component(var, config)
|
||||
await fan.register_fan(var, config)
|
||||
|
||||
|
||||
@@ -35,6 +35,27 @@ void VoiceAssistant::setup() {
|
||||
temp_ring_buffer->write((void *) data.data(), data.size());
|
||||
}
|
||||
});
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_->add_on_state_callback([this]() {
|
||||
switch (this->media_player_->state) {
|
||||
case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING:
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) {
|
||||
// State changed to announcing after receiving the url
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::PLAYING;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) {
|
||||
// No longer announcing the TTS response
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::FINISHED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
@@ -223,6 +244,13 @@ void VoiceAssistant::loop() {
|
||||
msg.wake_word_phrase = this->wake_word_;
|
||||
this->wake_word_ = "";
|
||||
|
||||
// Reset media player state tracking
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
|
||||
ESP_LOGW(TAG, "Could not request start");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||
@@ -314,17 +342,10 @@ void VoiceAssistant::loop() {
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING);
|
||||
playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING);
|
||||
|
||||
if (playing && this->media_player_wait_for_announcement_start_) {
|
||||
// Announcement has started playing, wait for it to finish
|
||||
this->media_player_wait_for_announcement_start_ = false;
|
||||
this->media_player_wait_for_announcement_end_ = true;
|
||||
}
|
||||
|
||||
if (!playing && this->media_player_wait_for_announcement_end_) {
|
||||
// Announcement has finished playing
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::IDLE;
|
||||
this->cancel_timeout("playing");
|
||||
ESP_LOGD(TAG, "Announcement finished playing");
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
@@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() {
|
||||
break;
|
||||
case State::AWAITING_RESPONSE:
|
||||
this->signal_stop_();
|
||||
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
|
||||
break;
|
||||
case State::STREAMING_RESPONSE:
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
// Stop any ongoing media player announcement
|
||||
@@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() {
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
if (this->started_streaming_tts_) {
|
||||
// Haven't reached the TTS_END stage, so send the stop signal to HA.
|
||||
this->signal_stop_();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case State::RESPONSE_FINISHED:
|
||||
@@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
if (this->media_player_ != nullptr) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
this->started_streaming_tts_ = true;
|
||||
this->start_playback_timeout_();
|
||||
|
||||
tts_url_for_trigger = this->tts_response_url_;
|
||||
this->tts_response_url_.clear(); // Reset streaming URL
|
||||
this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,18 +741,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
this->defer([this, url]() {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
}
|
||||
this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
|
||||
#endif
|
||||
this->tts_end_trigger_->trigger(url);
|
||||
});
|
||||
State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE;
|
||||
this->set_state_(new_state, new_state);
|
||||
if (new_state != this->state_) {
|
||||
// Don't needlessly change the state. The intent progress stage may have already changed the state to streaming
|
||||
// response.
|
||||
this->set_state_(new_state, new_state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_RUN_END: {
|
||||
@@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
if (this->media_player_ != nullptr) {
|
||||
this->tts_start_trigger_->trigger(msg.text);
|
||||
|
||||
this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT;
|
||||
|
||||
if (!msg.preannounce_media_id.empty()) {
|
||||
this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform();
|
||||
}
|
||||
@@ -886,9 +921,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg)
|
||||
.perform();
|
||||
this->continue_conversation_ = msg.start_conversation;
|
||||
|
||||
this->media_player_wait_for_announcement_start_ = true;
|
||||
this->media_player_wait_for_announcement_end_ = false;
|
||||
// Start the playback timeout, as the media player state isn't immediately updated
|
||||
this->start_playback_timeout_();
|
||||
|
||||
if (this->continuous_) {
|
||||
|
||||
@@ -90,6 +90,15 @@ struct Configuration {
|
||||
uint32_t max_active_wake_words;
|
||||
};
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
enum class MediaPlayerResponseState {
|
||||
IDLE,
|
||||
URL_SENT,
|
||||
PLAYING,
|
||||
FINISHED,
|
||||
};
|
||||
#endif
|
||||
|
||||
class VoiceAssistant : public Component {
|
||||
public:
|
||||
VoiceAssistant();
|
||||
@@ -272,8 +281,8 @@ class VoiceAssistant : public Component {
|
||||
media_player::MediaPlayer *media_player_{nullptr};
|
||||
std::string tts_response_url_{""};
|
||||
bool started_streaming_tts_{false};
|
||||
bool media_player_wait_for_announcement_start_{false};
|
||||
bool media_player_wait_for_announcement_end_{false};
|
||||
|
||||
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
|
||||
#endif
|
||||
|
||||
bool local_output_{false};
|
||||
|
||||
@@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_ota_removed(config: ConfigType) -> ConfigType:
|
||||
# Only raise error if OTA is explicitly enabled (True)
|
||||
# If it's False or not specified, we can safely ignore it
|
||||
if config.get(CONF_OTA):
|
||||
def validate_ota(config: ConfigType) -> ConfigType:
|
||||
# The OTA option only accepts False to explicitly disable OTA for web_server
|
||||
# IMPORTANT: Setting ota: false ONLY affects the web_server component
|
||||
# The captive_portal component will still be able to perform OTA updates
|
||||
if CONF_OTA in config and config[CONF_OTA] is not False:
|
||||
raise cv.Invalid(
|
||||
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
|
||||
f"Please use the new OTA platform structure instead:\n\n"
|
||||
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
|
||||
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
|
||||
f"ota:\n"
|
||||
f" - platform: web_server\n\n"
|
||||
f"See https://esphome.io/components/ota for more information."
|
||||
@@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA): cv.boolean,
|
||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||
@@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
default_url,
|
||||
validate_local,
|
||||
validate_sorting_groups,
|
||||
validate_ota_removed,
|
||||
validate_ota,
|
||||
)
|
||||
|
||||
|
||||
@@ -288,7 +289,11 @@ async def to_code(config):
|
||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||
# OTA is now handled by the web_server OTA platform
|
||||
# The CONF_OTA option is kept only for backwards compatibility validation
|
||||
# The CONF_OTA option is kept to allow explicitly disabling OTA for web_server
|
||||
# IMPORTANT: This ONLY affects the web_server component, NOT captive_portal
|
||||
# Captive portal will still be able to perform OTA updates even when this is set
|
||||
if config.get(CONF_OTA) is False:
|
||||
cg.add_define("USE_WEBSERVER_OTA_DISABLED")
|
||||
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
#include "esphome/components/captive_portal/captive_portal.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#ifdef USE_ESP8266
|
||||
#include <Updater.h>
|
||||
@@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler {
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) override;
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
||||
// Check if this is an OTA update request
|
||||
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
|
||||
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
|
||||
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
|
||||
// Captive portal can still perform OTA updates - check if request is from active captive portal
|
||||
// Note: global_captive_portal is the standard way components communicate in ESPHome
|
||||
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
|
||||
captive_portal::global_captive_portal->is_active();
|
||||
#elif defined(USE_WEBSERVER_OTA_DISABLED)
|
||||
// OTA disabled for web_server and no captive portal compiled in
|
||||
return false;
|
||||
#else
|
||||
// OTA enabled for web_server
|
||||
return is_ota_request;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -57,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
||||
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
||||
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
|
||||
}
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report progress - use call_deferred since we're in web server task
|
||||
@@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
||||
|
||||
// Finalize
|
||||
if (final) {
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
|
||||
this->ota_read_length_, request->contentLength());
|
||||
|
||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
||||
|
||||
@@ -268,10 +268,10 @@ std::string WebServer::get_config_json() {
|
||||
return json::build_json([this](JsonObject root) {
|
||||
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
||||
root["comment"] = App.get_comment();
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
root["ota"] = true; // web_server OTA platform is configured
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||
root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
#else
|
||||
root["ota"] = false;
|
||||
root["ota"] = true;
|
||||
#endif
|
||||
root["log"] = this->expose_log_;
|
||||
root["lang"] = "en";
|
||||
@@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
||||
|
||||
light::LightJSONSchema::dump_json(*obj, root);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("effects");
|
||||
JsonArray opt = root["effects"].to<JsonArray>();
|
||||
opt.add("None");
|
||||
for (auto const &option : obj->get_effects()) {
|
||||
opt.add(option->get_name());
|
||||
@@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
|
||||
return json::build_json([this, obj, value, start_config](JsonObject root) {
|
||||
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("option");
|
||||
JsonArray opt = root["option"].to<JsonArray>();
|
||||
for (auto &option : obj->traits.get_options()) {
|
||||
opt.add(option);
|
||||
}
|
||||
@@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s
|
||||
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
|
||||
}
|
||||
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
||||
const auto traits = obj->get_traits();
|
||||
@@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
char buf[16];
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("modes");
|
||||
JsonArray opt = root["modes"].to<JsonArray>();
|
||||
for (climate::ClimateMode m : traits.get_supported_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
JsonArray opt = root.createNestedArray("fan_modes");
|
||||
JsonArray opt = root["fan_modes"].to<JsonArray>();
|
||||
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
||||
}
|
||||
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
JsonArray opt = root.createNestedArray("custom_fan_modes");
|
||||
JsonArray opt = root["custom_fan_modes"].to<JsonArray>();
|
||||
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
||||
opt.add(custom_fan_mode);
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
JsonArray opt = root.createNestedArray("swing_modes");
|
||||
JsonArray opt = root["swing_modes"].to<JsonArray>();
|
||||
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
||||
}
|
||||
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
||||
JsonArray opt = root.createNestedArray("presets");
|
||||
JsonArray opt = root["presets"].to<JsonArray>();
|
||||
for (climate::ClimatePreset m : traits.get_supported_presets())
|
||||
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
|
||||
JsonArray opt = root.createNestedArray("custom_presets");
|
||||
JsonArray opt = root["custom_presets"].to<JsonArray>();
|
||||
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||
opt.add(custom_preset);
|
||||
}
|
||||
@@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
root["state"] = root["target_temperature"];
|
||||
}
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1618,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; }
|
||||
static std::string get_event_type(event::Event *event) {
|
||||
return (event && event->last_event_type) ? *event->last_event_type : "";
|
||||
}
|
||||
|
||||
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
|
||||
auto *event = static_cast<event::Event *>(source);
|
||||
@@ -1635,7 +1639,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
|
||||
root["event_type"] = event_type;
|
||||
}
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray event_types = root.createNestedArray("event_types");
|
||||
JsonArray event_types = root["event_types"].to<JsonArray>();
|
||||
for (auto const &event_type : obj->get_event_types()) {
|
||||
event_types.add(event_type);
|
||||
}
|
||||
@@ -1682,6 +1686,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so
|
||||
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||
}
|
||||
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
||||
root["value"] = obj->update_info.latest_version;
|
||||
@@ -1707,6 +1712,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
|
||||
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
|
||||
"REST API documentation.</p>"));
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
|
||||
// Show OTA form only if web_server OTA is not explicitly disabled
|
||||
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
||||
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||
#endif
|
||||
|
||||
@@ -40,4 +40,4 @@ async def to_code(config):
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
|
||||
|
||||
@@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
for (auto &group : ws->sorting_groups_) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
message = json::build_json([group](JsonObject root) {
|
||||
root["name"] = group.second.name;
|
||||
root["sorting_weight"] = group.second.weight;
|
||||
});
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
// a (very) large number of these should be able to be queued initially without defer
|
||||
// since the only thing in the send buffer at this point is the initial ping/config
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
#include "lwip/priv/tcpip_priv.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -295,25 +291,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
}
|
||||
|
||||
if (!manual_ip.has_value()) {
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
LOCK_TCPIP_CORE();
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
{
|
||||
LwIPLock lock;
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
|
||||
// No manual IP is set; use DHCP client
|
||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <esp_wireguard.h>
|
||||
#include <esp_wireguard_err.h>
|
||||
@@ -42,7 +43,10 @@ void Wireguard::setup() {
|
||||
|
||||
this->publish_enabled_state();
|
||||
|
||||
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
|
||||
}
|
||||
|
||||
if (this->wg_initialized_ == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Initialized");
|
||||
@@ -249,7 +253,10 @@ void Wireguard::start_connection_() {
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Starting connection");
|
||||
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
|
||||
}
|
||||
|
||||
if (this->wg_connected_ == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Connection started");
|
||||
@@ -280,7 +287,10 @@ void Wireguard::start_connection_() {
|
||||
void Wireguard::stop_connection_() {
|
||||
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
|
||||
ESP_LOGD(TAG, "Stopping connection");
|
||||
esp_wireguard_disconnect(&(this->wg_ctx_));
|
||||
{
|
||||
LwIPLock lock;
|
||||
esp_wireguard_disconnect(&(this->wg_ctx_));
|
||||
}
|
||||
this->wg_connected_ = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ from esphome.const import (
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
VALID_SUBSTITUTIONS_CHARACTERS,
|
||||
Framework,
|
||||
__version__ as ESPHOME_VERSION,
|
||||
)
|
||||
from esphome.core import (
|
||||
@@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid):
|
||||
"""Represents an invalid value in the final validation phase where the path should not be prepended."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Version:
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
extra: str = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.major}.{self.minor}.{self.patch}"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str) -> Version:
|
||||
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
|
||||
if match is None:
|
||||
raise ValueError(f"Not a valid version number {value}")
|
||||
major = int(match[1])
|
||||
minor = int(match[2])
|
||||
patch = int(match[3])
|
||||
extra = match[4] or ""
|
||||
return Version(major=major, minor=minor, patch=patch, extra=extra)
|
||||
|
||||
@property
|
||||
def is_beta(self) -> bool:
|
||||
"""Check if this version is a beta version."""
|
||||
return self.extra.startswith("b")
|
||||
|
||||
@property
|
||||
def is_dev(self) -> bool:
|
||||
"""Check if this version is a development version."""
|
||||
return self.extra.startswith("dev")
|
||||
|
||||
|
||||
def check_not_templatable(value):
|
||||
if isinstance(value, Lambda):
|
||||
raise Invalid("This option is not templatable!")
|
||||
@@ -619,16 +652,35 @@ def only_on(platforms):
|
||||
return validator_
|
||||
|
||||
|
||||
def only_with_framework(frameworks):
|
||||
def only_with_framework(
|
||||
frameworks: Framework | str | list[Framework | str], suggestions=None
|
||||
):
|
||||
"""Validate that this option can only be specified on the given frameworks."""
|
||||
if not isinstance(frameworks, list):
|
||||
frameworks = [frameworks]
|
||||
|
||||
frameworks = [Framework(framework) for framework in frameworks]
|
||||
|
||||
if suggestions is None:
|
||||
suggestions = {}
|
||||
|
||||
version = Version.parse(ESPHOME_VERSION)
|
||||
if version.is_beta:
|
||||
docs_format = "https://beta.esphome.io/components/{path}"
|
||||
elif version.is_dev:
|
||||
docs_format = "https://next.esphome.io/components/{path}"
|
||||
else:
|
||||
docs_format = "https://esphome.io/components/{path}"
|
||||
|
||||
def validator_(obj):
|
||||
if CORE.target_framework not in frameworks:
|
||||
raise Invalid(
|
||||
f"This feature is only available with frameworks {frameworks}"
|
||||
)
|
||||
err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
|
||||
if suggestion := suggestions.get(CORE.target_framework, None):
|
||||
(component, docs_path) = suggestion
|
||||
err_str += f"\nPlease use '{component}'"
|
||||
if docs_path:
|
||||
err_str += f": {docs_format.format(path=docs_path)}"
|
||||
raise Invalid(err_str)
|
||||
return obj
|
||||
|
||||
return validator_
|
||||
@@ -637,8 +689,8 @@ def only_with_framework(frameworks):
|
||||
only_on_esp32 = only_on(PLATFORM_ESP32)
|
||||
only_on_esp8266 = only_on(PLATFORM_ESP8266)
|
||||
only_on_rp2040 = only_on(PLATFORM_RP2040)
|
||||
only_with_arduino = only_with_framework("arduino")
|
||||
only_with_esp_idf = only_with_framework("esp-idf")
|
||||
only_with_arduino = only_with_framework(Framework.ARDUINO)
|
||||
only_with_esp_idf = only_with_framework(Framework.ESP_IDF)
|
||||
|
||||
|
||||
# Adapted from:
|
||||
@@ -1965,26 +2017,6 @@ def source_refresh(value: str):
|
||||
return positive_time_period_seconds(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Version:
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.major}.{self.minor}.{self.patch}"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str) -> Version:
|
||||
match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
|
||||
if match is None:
|
||||
raise ValueError(f"Not a valid version number {value}")
|
||||
major = int(match[1])
|
||||
minor = int(match[2])
|
||||
patch = int(match[3])
|
||||
return Version(major=major, minor=minor, patch=patch)
|
||||
|
||||
|
||||
def version_number(value):
|
||||
value = string_strict(value)
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.7.0b3"
|
||||
__version__ = "2025.7.4"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -68,8 +68,11 @@ void Application::setup() {
|
||||
|
||||
do {
|
||||
uint8_t new_app_state = STATUS_LED_WARNING;
|
||||
this->scheduler.call();
|
||||
this->feed_wdt();
|
||||
uint32_t now = millis();
|
||||
|
||||
// Process pending loop enables to handle GPIO interrupts during setup
|
||||
this->before_loop_tasks_(now);
|
||||
|
||||
for (uint32_t j = 0; j <= i; j++) {
|
||||
// Update loop_component_start_time_ right before calling each component
|
||||
this->loop_component_start_time_ = millis();
|
||||
@@ -78,6 +81,8 @@ void Application::setup() {
|
||||
this->app_state_ |= new_app_state;
|
||||
this->feed_wdt();
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
yield();
|
||||
} while (!component->can_proceed());
|
||||
@@ -94,30 +99,10 @@ void Application::setup() {
|
||||
void Application::loop() {
|
||||
uint8_t new_app_state = 0;
|
||||
|
||||
this->scheduler.call();
|
||||
|
||||
// Get the initial loop time at the start
|
||||
uint32_t last_op_end_time = millis();
|
||||
|
||||
// Feed WDT with time
|
||||
this->feed_wdt(last_op_end_time);
|
||||
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
if (this->has_pending_enable_loop_requests_) {
|
||||
// Clear flag BEFORE processing to avoid race condition
|
||||
// If ISR sets it during processing, we'll catch it next loop iteration
|
||||
// This is safe because:
|
||||
// 1. Each component has its own pending_enable_loop_ flag that we check
|
||||
// 2. If we can't process a component (wrong state), enable_pending_loops_()
|
||||
// will set this flag back to true
|
||||
// 3. Any new ISR requests during processing will set the flag again
|
||||
this->has_pending_enable_loop_requests_ = false;
|
||||
this->enable_pending_loops_();
|
||||
}
|
||||
|
||||
// Mark that we're in the loop for safe reentrant modifications
|
||||
this->in_loop_ = true;
|
||||
this->before_loop_tasks_(last_op_end_time);
|
||||
|
||||
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||
this->current_loop_index_++) {
|
||||
@@ -138,7 +123,7 @@ void Application::loop() {
|
||||
this->feed_wdt(last_op_end_time);
|
||||
}
|
||||
|
||||
this->in_loop_ = false;
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
|
||||
// Use the last component's end time instead of calling millis() again
|
||||
@@ -309,6 +294,12 @@ void Application::disable_component_loop_(Component *component) {
|
||||
if (this->in_loop_ && i == this->current_loop_index_) {
|
||||
// Decrement so we'll process the swapped component next
|
||||
this->current_loop_index_--;
|
||||
// Update the loop start time to current time so the swapped component
|
||||
// gets correct timing instead of inheriting stale timing.
|
||||
// This prevents integer underflow in timing calculations by ensuring
|
||||
// the swapped component starts with a fresh timing reference, avoiding
|
||||
// errors caused by stale or wrapped timing values.
|
||||
this->loop_component_start_time_ = millis();
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -394,6 +385,36 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
// Process scheduled tasks
|
||||
this->scheduler.call();
|
||||
|
||||
// Feed the watchdog timer
|
||||
this->feed_wdt(loop_start_time);
|
||||
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
if (this->has_pending_enable_loop_requests_) {
|
||||
// Clear flag BEFORE processing to avoid race condition
|
||||
// If ISR sets it during processing, we'll catch it next loop iteration
|
||||
// This is safe because:
|
||||
// 1. Each component has its own pending_enable_loop_ flag that we check
|
||||
// 2. If we can't process a component (wrong state), enable_pending_loops_()
|
||||
// will set this flag back to true
|
||||
// 3. Any new ISR requests during processing will set the flag again
|
||||
this->has_pending_enable_loop_requests_ = false;
|
||||
this->enable_pending_loops_();
|
||||
}
|
||||
|
||||
// Mark that we're in the loop for safe reentrant modifications
|
||||
this->in_loop_ = true;
|
||||
}
|
||||
|
||||
void Application::after_loop_tasks_() {
|
||||
// Clear the in_loop_ flag to indicate we're done processing components
|
||||
this->in_loop_ = false;
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool Application::register_socket_fd(int fd) {
|
||||
// WARNING: This function is NOT thread-safe and must only be called from the main loop
|
||||
|
||||
@@ -504,6 +504,8 @@ class Application {
|
||||
void enable_component_loop_(Component *component);
|
||||
void enable_pending_loops_();
|
||||
void activate_looping_component_(uint16_t index);
|
||||
void before_loop_tasks_(uint32_t loop_start_time);
|
||||
void after_loop_tasks_();
|
||||
|
||||
void feed_wdt_arch_();
|
||||
|
||||
|
||||
@@ -158,14 +158,14 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
void play_complex(Ts... x) override {
|
||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||
this->num_running_++;
|
||||
this->set_timeout(this->delay_.value(x...), f);
|
||||
this->set_timeout("delay", this->delay_.value(x...), f);
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void play(Ts... x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override { this->cancel_timeout(""); }
|
||||
void stop() override { this->cancel_timeout("delay"); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaAction : public Action<Ts...> {
|
||||
|
||||
@@ -252,10 +252,10 @@ void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, name, 0, std::move(f));
|
||||
}
|
||||
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, "", timeout, std::move(f));
|
||||
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
|
||||
}
|
||||
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_interval(this, "", interval, std::move(f));
|
||||
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
|
||||
}
|
||||
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
|
||||
float backoff_increase_factor) { // NOLINT
|
||||
@@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
bool Component::is_ready() const {
|
||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||
}
|
||||
bool Component::can_proceed() { return true; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32)
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
@@ -78,4 +78,4 @@ template<class T, uint8_t SIZE> class EventPool {
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#endif // defined(USE_ESP32)
|
||||
|
||||
@@ -67,7 +67,10 @@ To bit_cast(const From &src) {
|
||||
return dst;
|
||||
}
|
||||
#endif
|
||||
using std::lerp;
|
||||
|
||||
// clang-format off
|
||||
inline float lerp(float completion, float start, float end) = delete; // Please use std::lerp. Notice that it has different order on arguments!
|
||||
// clang-format on
|
||||
|
||||
// std::byteswap from C++23
|
||||
template<typename T> constexpr T byteswap(T n) {
|
||||
@@ -683,6 +686,23 @@ class InterruptLock {
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
|
||||
*
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
|
||||
* It ensures thread-safe access to lwIP APIs.
|
||||
*
|
||||
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
|
||||
*/
|
||||
class LwIPLock {
|
||||
public:
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
|
||||
// Delete copy constructor and copy assignment operator to prevent accidental copying
|
||||
LwIPLock(const LwIPLock &) = delete;
|
||||
LwIPLock &operator=(const LwIPLock &) = delete;
|
||||
};
|
||||
|
||||
/** Helper class to request `loop()` to be called as fast as possible.
|
||||
*
|
||||
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32)
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Lock-free queue for single-producer single-consumer scenarios.
|
||||
@@ -148,4 +143,4 @@ template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQu
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#endif // defined(USE_ESP32)
|
||||
|
||||
@@ -446,7 +446,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
|
||||
// Helper to cancel items by name - must be called with lock held
|
||||
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
|
||||
// Early return if name is invalid - no items to cancel
|
||||
if (name_cstr == nullptr || name_cstr[0] == '\0') {
|
||||
if (name_cstr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,16 +114,17 @@ class Scheduler {
|
||||
name_is_dynamic = false;
|
||||
}
|
||||
|
||||
if (!name || !name[0]) {
|
||||
if (!name) {
|
||||
// nullptr case - no name provided
|
||||
name_.static_name = nullptr;
|
||||
} else if (make_copy) {
|
||||
// Make a copy for dynamic strings
|
||||
// Make a copy for dynamic strings (including empty strings)
|
||||
size_t len = strlen(name);
|
||||
name_.dynamic_name = new char[len + 1];
|
||||
memcpy(name_.dynamic_name, name, len + 1);
|
||||
name_is_dynamic = true;
|
||||
} else {
|
||||
// Use static string directly
|
||||
// Use static string directly (including empty strings)
|
||||
name_.static_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault(
|
||||
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
|
||||
)
|
||||
# Suppress Python syntax warnings from third-party scripts during compilation
|
||||
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||
cmd = ["platformio"] + list(args)
|
||||
|
||||
if not CORE.verbose:
|
||||
|
||||
@@ -147,6 +147,13 @@ class RedirectText:
|
||||
continue
|
||||
|
||||
self._write_color_replace(line)
|
||||
# Check for flash size error and provide helpful guidance
|
||||
if (
|
||||
"Error: The program size" in line
|
||||
and "is greater than maximum allowed" in line
|
||||
and (help_msg := get_esp32_arduino_flash_error_help())
|
||||
):
|
||||
self._write_color_replace(help_msg)
|
||||
else:
|
||||
self._write_color_replace(s)
|
||||
|
||||
@@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
|
||||
|
||||
result.sort(key=lambda x: x.path)
|
||||
return result
|
||||
|
||||
|
||||
def get_esp32_arduino_flash_error_help() -> str | None:
|
||||
"""Returns helpful message when ESP32 with Arduino runs out of flash space."""
|
||||
from esphome.core import CORE
|
||||
|
||||
if not (CORE.is_esp32 and CORE.using_arduino):
|
||||
return None
|
||||
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
return (
|
||||
"\n"
|
||||
+ color(
|
||||
AnsiFore.YELLOW,
|
||||
"💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n",
|
||||
)
|
||||
+ "\n"
|
||||
+ "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n"
|
||||
+ "\n"
|
||||
+ "1. In your YAML configuration, modify the framework section:\n"
|
||||
+ "\n"
|
||||
+ " esp32:\n"
|
||||
+ " framework:\n"
|
||||
+ " type: esp-idf\n"
|
||||
+ "\n"
|
||||
+ "2. Clean build files and compile again\n"
|
||||
+ "\n"
|
||||
+ "Note: ESP-IDF uses less flash space and provides better performance.\n"
|
||||
+ "Some Arduino-specific libraries may need alternatives.\n\n"
|
||||
)
|
||||
|
||||
@@ -162,6 +162,9 @@ def get_ini_content():
|
||||
# Sort to avoid changing build unflags order
|
||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||
|
||||
# Add extra script for C++ flags
|
||||
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||
|
||||
content = "[platformio]\n"
|
||||
content += f"description = ESPHome {__version__}\n"
|
||||
|
||||
@@ -222,6 +225,9 @@ def write_platformio_project():
|
||||
write_gitignore()
|
||||
write_platformio_ini(content)
|
||||
|
||||
# Write extra script for C++ specific flags
|
||||
write_cxx_flags_script()
|
||||
|
||||
|
||||
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
||||
#pragma once
|
||||
@@ -394,3 +400,20 @@ def write_gitignore():
|
||||
if not os.path.isfile(path):
|
||||
with open(file=path, mode="w", encoding="utf-8") as f:
|
||||
f.write(GITIGNORE_CONTENT)
|
||||
|
||||
|
||||
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||
Import("env")
|
||||
|
||||
# Add C++ specific flags
|
||||
"""
|
||||
|
||||
|
||||
def write_cxx_flags_script() -> None:
|
||||
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||
contents = CXX_FLAGS_FILE_CONTENTS
|
||||
if not CORE.is_host:
|
||||
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||
contents += "\n"
|
||||
write_file_if_changed(path, contents)
|
||||
|
||||
@@ -35,7 +35,7 @@ build_flags =
|
||||
lib_deps =
|
||||
esphome/noise-c@0.1.10 ; api
|
||||
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
||||
bblanchon/ArduinoJson@6.18.5 ; json
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
@@ -138,7 +138,7 @@ lib_deps =
|
||||
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
|
||||
Update ; ota,web_server_base (Arduino built-in)
|
||||
${common:arduino.lib_deps}
|
||||
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
|
||||
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
|
||||
NetworkClientSecure ; http_request,nextion (Arduino built-in)
|
||||
HTTPClient ; http_request,nextion (Arduino built-in)
|
||||
ESPmDNS ; mdns (Arduino built-in)
|
||||
@@ -235,7 +235,7 @@ build_flags =
|
||||
-DUSE_ZEPHYR
|
||||
-DUSE_NRF52
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@7.0.0 ; json
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
|
||||
@@ -6,7 +6,7 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ ! -n "$VIRTUAL_ENV" ]; then
|
||||
if [ -x "$(command -v uv)" ]; then
|
||||
uv venv venv
|
||||
uv venv --seed venv
|
||||
else
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
@@ -4,6 +4,8 @@ esphome:
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
logger:
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: "test 1 text"
|
||||
|
||||
@@ -8,31 +8,31 @@ from esphome.types import ConfigType
|
||||
|
||||
def test_web_server_ota_true_fails_validation() -> None:
|
||||
"""Test that web_server with ota: true fails validation with helpful message."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
from esphome.components.web_server import validate_ota
|
||||
|
||||
# Config with ota: true should fail
|
||||
config: ConfigType = {"ota": True}
|
||||
|
||||
with pytest.raises(cv.Invalid) as exc_info:
|
||||
validate_ota_removed(config)
|
||||
validate_ota(config)
|
||||
|
||||
# Check error message contains migration instructions
|
||||
error_msg = str(exc_info.value)
|
||||
assert "has been removed from 'web_server'" in error_msg
|
||||
assert "only accepts 'false' to disable OTA" in error_msg
|
||||
assert "platform: web_server" in error_msg
|
||||
assert "ota:" in error_msg
|
||||
|
||||
|
||||
def test_web_server_ota_false_passes_validation() -> None:
|
||||
"""Test that web_server with ota: false passes validation."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
from esphome.components.web_server import validate_ota
|
||||
|
||||
# Config with ota: false should pass
|
||||
config: ConfigType = {"ota": False}
|
||||
result = validate_ota_removed(config)
|
||||
result = validate_ota(config)
|
||||
assert result == config
|
||||
|
||||
# Config without ota should also pass
|
||||
config: ConfigType = {}
|
||||
result = validate_ota_removed(config)
|
||||
result = validate_ota(config)
|
||||
assert result == config
|
||||
|
||||
1
tests/components/captive_portal/test.bk72xx-ard.yaml
Normal file
1
tests/components/captive_portal/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
18
tests/components/logger/test-on_message.host.yaml
Normal file
18
tests/components/logger/test-on_message.host.yaml
Normal 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);
|
||||
@@ -928,6 +928,12 @@ lvgl:
|
||||
angle_range: 360
|
||||
rotation: !lambda return 2700;
|
||||
indicators:
|
||||
- tick_style:
|
||||
start_value: 0
|
||||
end_value: 60
|
||||
color_start: 0x0000bd
|
||||
color_end: 0xbd0000
|
||||
width: !lambda return 1;
|
||||
- line:
|
||||
opa: 50%
|
||||
id: minute_hand
|
||||
|
||||
87
tests/integration/fixtures/api_string_lambda.yaml
Normal file
87
tests/integration/fixtures/api_string_lambda.yaml
Normal 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
|
||||
24
tests/integration/fixtures/delay_action_cancellation.yaml
Normal file
24
tests/integration/fixtures/delay_action_cancellation.yaml
Normal 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"
|
||||
@@ -1,7 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
|
||||
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
|
||||
@@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon
|
||||
LoopTestISRComponent = loop_test_component_ns.class_(
|
||||
"LoopTestISRComponent", cg.Component
|
||||
)
|
||||
LoopTestUpdateComponent = loop_test_component_ns.class_(
|
||||
"LoopTestUpdateComponent", cg.PollingComponent
|
||||
)
|
||||
|
||||
CONF_DISABLE_AFTER = "disable_after"
|
||||
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
|
||||
CONF_ISR_COMPONENTS = "isr_components"
|
||||
CONF_UPDATE_COMPONENTS = "update_components"
|
||||
CONF_DISABLE_LOOP_AFTER = "disable_loop_after"
|
||||
|
||||
COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_,
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LoopTestComponent),
|
||||
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list(
|
||||
UPDATE_COMPONENT_CONFIG_SCHEMA
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -94,3 +111,12 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(isr_config[CONF_ID])
|
||||
await cg.register_component(var, isr_config)
|
||||
cg.add(var.set_name(isr_config[CONF_NAME]))
|
||||
|
||||
# Create update test components
|
||||
for update_config in config.get(CONF_UPDATE_COMPONENTS, []):
|
||||
var = cg.new_Pvariable(update_config[CONF_ID])
|
||||
await cg.register_component(var, update_config)
|
||||
|
||||
cg.add(var.set_name(update_config[CONF_NAME]))
|
||||
cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER]))
|
||||
cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
@@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() {
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// LoopTestUpdateComponent implementation
|
||||
void LoopTestUpdateComponent::setup() {
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str());
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::loop() {
|
||||
this->loop_count_++;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_);
|
||||
|
||||
// Disable loop after specified count to test component.update when loop is disabled
|
||||
if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LoopTestUpdateComponent::update() {
|
||||
this->update_count_++;
|
||||
// Check if loop is disabled by testing component state
|
||||
bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE;
|
||||
ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(),
|
||||
this->update_count_, loop_disabled ? "YES" : "NO");
|
||||
}
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace loop_test_component {
|
||||
@@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> {
|
||||
LoopTestComponent *parent_;
|
||||
};
|
||||
|
||||
// Component with update() method to test component.update action
|
||||
class LoopTestUpdateComponent : public PollingComponent {
|
||||
public:
|
||||
LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval
|
||||
|
||||
void set_name(const std::string &name) { this->name_ = name; }
|
||||
void set_disable_loop_after(int count) { this->disable_loop_after_ = count; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
int get_update_count() const { return this->update_count_; }
|
||||
int get_loop_count() const { return this->loop_count_; }
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
int loop_count_{0};
|
||||
int update_count_{0};
|
||||
int disable_loop_after_{0};
|
||||
};
|
||||
|
||||
} // namespace loop_test_component
|
||||
} // namespace esphome
|
||||
|
||||
@@ -40,6 +40,13 @@ loop_test_component:
|
||||
- id: isr_test
|
||||
name: "isr_test"
|
||||
|
||||
# Update test component to test component.update when loop is disabled
|
||||
update_components:
|
||||
- id: update_test_component
|
||||
name: "update_test"
|
||||
disable_loop_after: 3 # Disable loop after 3 iterations
|
||||
update_interval: 0.1s # Fast update interval for testing
|
||||
|
||||
# Interval to re-enable the self_disable_10 component after some time
|
||||
interval:
|
||||
- interval: 0.5s
|
||||
@@ -51,3 +58,28 @@ interval:
|
||||
- logger.log: "Re-enabling self_disable_10 via service"
|
||||
- loop_test_component.enable:
|
||||
id: self_disable_10
|
||||
|
||||
# Test component.update on a component with disabled loop
|
||||
- interval: 0.1s
|
||||
then:
|
||||
- lambda: |-
|
||||
static bool manual_update_done = false;
|
||||
if (!manual_update_done &&
|
||||
id(update_test_component).get_loop_count() == 3 &&
|
||||
id(update_test_component).get_update_count() >= 3) {
|
||||
ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop");
|
||||
manual_update_done = true;
|
||||
}
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
static bool manual_update_triggered = false;
|
||||
if (!manual_update_triggered &&
|
||||
id(update_test_component).get_loop_count() == 3 &&
|
||||
id(update_test_component).get_update_count() >= 3) {
|
||||
manual_update_triggered = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
then:
|
||||
- component.update: update_test_component
|
||||
|
||||
@@ -4,9 +4,7 @@ esphome:
|
||||
priority: -100
|
||||
then:
|
||||
- logger.log: "Starting scheduler string tests"
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging
|
||||
debug_scheduler: true # Enable scheduler debug logging
|
||||
|
||||
host:
|
||||
api:
|
||||
@@ -32,6 +30,12 @@ globals:
|
||||
- id: results_reported
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: edge_tests_done
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: empty_cancel_failed
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
|
||||
script:
|
||||
- id: test_static_strings
|
||||
@@ -147,12 +151,106 @@ script:
|
||||
static TestDynamicDeferComponent test_dynamic_defer_component;
|
||||
test_dynamic_defer_component.test_dynamic_defer();
|
||||
|
||||
- id: test_cancellation_edge_cases
|
||||
then:
|
||||
- logger.log: "Testing cancellation edge cases"
|
||||
- lambda: |-
|
||||
auto *component1 = id(test_sensor1);
|
||||
// Use a different component for empty string tests to avoid interference
|
||||
auto *component2 = id(test_sensor2);
|
||||
|
||||
// Test 12: Cancel with empty string - regression test for issue #9599
|
||||
// First create a timeout with empty name on component2 to avoid interference
|
||||
App.scheduler.set_timeout(component2, "", 500, []() {
|
||||
ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!");
|
||||
id(empty_cancel_failed) = true;
|
||||
});
|
||||
|
||||
// Now cancel it - this should work after our fix
|
||||
bool cancelled_empty = App.scheduler.cancel_timeout(component2, "");
|
||||
ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false");
|
||||
if (!cancelled_empty) {
|
||||
ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!");
|
||||
id(empty_cancel_failed) = true;
|
||||
}
|
||||
|
||||
// Test 13: Cancel non-existent timeout
|
||||
bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist");
|
||||
ESP_LOGI("test", "Cancel non-existent timeout result: %s",
|
||||
cancelled_nonexistent ? "true (unexpected!)" : "false (expected)");
|
||||
|
||||
// Test 14: Multiple timeouts with same name - only last should execute
|
||||
for (int i = 0; i < 5; i++) {
|
||||
App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() {
|
||||
ESP_LOGI("test", "Duplicate timeout %d fired", i);
|
||||
id(timeout_counter) += 1;
|
||||
});
|
||||
}
|
||||
ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'");
|
||||
|
||||
// Test 15: Multiple intervals with same name - only last should run
|
||||
for (int i = 0; i < 3; i++) {
|
||||
App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() {
|
||||
ESP_LOGI("test", "Duplicate interval %d fired", i);
|
||||
id(interval_counter) += 10; // Large increment to detect multiple
|
||||
// Cancel after first execution
|
||||
App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval");
|
||||
});
|
||||
}
|
||||
ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'");
|
||||
|
||||
// Test 16: Cancel with nullptr protection (via empty const char*)
|
||||
const char* null_name = "";
|
||||
App.scheduler.set_timeout(component2, null_name, 600, []() {
|
||||
ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!");
|
||||
id(empty_cancel_failed) = true;
|
||||
});
|
||||
bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name);
|
||||
ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)",
|
||||
cancelled_const_empty ? "true" : "false");
|
||||
if (!cancelled_const_empty) {
|
||||
ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!");
|
||||
id(empty_cancel_failed) = true;
|
||||
}
|
||||
|
||||
// Test 17: Rapid create/cancel/create with same name
|
||||
App.scheduler.set_timeout(component1, "rapid_test", 5000, []() {
|
||||
ESP_LOGI("test", "First rapid timeout - should not fire");
|
||||
id(timeout_counter) += 100;
|
||||
});
|
||||
App.scheduler.cancel_timeout(component1, "rapid_test");
|
||||
App.scheduler.set_timeout(component1, "rapid_test", 250, []() {
|
||||
ESP_LOGI("test", "Second rapid timeout - should fire");
|
||||
id(timeout_counter) += 1;
|
||||
});
|
||||
|
||||
// Test 18: Cancel all with a specific name (multiple instances)
|
||||
// Create multiple with same name
|
||||
App.scheduler.set_timeout(component1, "multi_cancel", 300, []() {
|
||||
ESP_LOGI("test", "Multi-cancel timeout 1");
|
||||
});
|
||||
App.scheduler.set_timeout(component1, "multi_cancel", 350, []() {
|
||||
ESP_LOGI("test", "Multi-cancel timeout 2");
|
||||
});
|
||||
App.scheduler.set_timeout(component1, "multi_cancel", 400, []() {
|
||||
ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire");
|
||||
id(timeout_counter) += 1;
|
||||
});
|
||||
// Note: Each set_timeout with same name cancels the previous one automatically
|
||||
|
||||
- id: report_results
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d",
|
||||
id(timeout_counter), id(interval_counter));
|
||||
|
||||
// Check if empty string cancellation test passed
|
||||
if (id(empty_cancel_failed)) {
|
||||
ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!");
|
||||
} else {
|
||||
ESP_LOGI("test", "Empty string cancellation test PASSED");
|
||||
}
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: Test Sensor 1
|
||||
@@ -189,12 +287,23 @@ interval:
|
||||
- delay: 0.2s
|
||||
- script.execute: test_dynamic_strings
|
||||
|
||||
# Run cancellation edge case tests after dynamic tests
|
||||
- interval: 0.2s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);'
|
||||
then:
|
||||
- lambda: 'id(edge_tests_done) = true;'
|
||||
- delay: 0.5s
|
||||
- script.execute: test_cancellation_edge_cases
|
||||
|
||||
# Report results after all tests
|
||||
- interval: 0.2s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(dynamic_tests_done) && !id(results_reported);'
|
||||
lambda: 'return id(edge_tests_done) && !id(results_reported);'
|
||||
then:
|
||||
- lambda: 'id(results_reported) = true;'
|
||||
- delay: 1s
|
||||
|
||||
100
tests/integration/test_api_string_lambda.py
Normal file
100
tests/integration/test_api_string_lambda.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Integration test for TemplatableStringValue with string lambdas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_string_lambda(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test TemplatableStringValue works with lambdas that return different types."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages for all four service calls
|
||||
string_called_future = loop.create_future()
|
||||
int_called_future = loop.create_future()
|
||||
float_called_future = loop.create_future()
|
||||
char_ptr_called_future = loop.create_future()
|
||||
|
||||
# Patterns to match in logs - confirms the lambdas compiled and executed
|
||||
string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA")
|
||||
int_pattern = re.compile(r"Service called with int: 42")
|
||||
float_pattern = re.compile(r"Service called with float: 3\.14")
|
||||
char_ptr_pattern = re.compile(r"Service called with number for char\* test: 123")
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not string_called_future.done() and string_pattern.search(line):
|
||||
string_called_future.set_result(True)
|
||||
if not int_called_future.done() and int_pattern.search(line):
|
||||
int_called_future.set_result(True)
|
||||
if not float_called_future.done() and float_pattern.search(line):
|
||||
float_called_future.set_result(True)
|
||||
if not char_ptr_called_future.done() and char_ptr_pattern.search(line):
|
||||
char_ptr_called_future.set_result(True)
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "api-string-lambda-test"
|
||||
|
||||
# List services to find our test services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Find all test services
|
||||
string_service = next(
|
||||
(s for s in services if s.name == "test_string_lambda"), None
|
||||
)
|
||||
assert string_service is not None, "test_string_lambda service not found"
|
||||
|
||||
int_service = next((s for s in services if s.name == "test_int_lambda"), None)
|
||||
assert int_service is not None, "test_int_lambda service not found"
|
||||
|
||||
float_service = next(
|
||||
(s for s in services if s.name == "test_float_lambda"), None
|
||||
)
|
||||
assert float_service is not None, "test_float_lambda service not found"
|
||||
|
||||
char_ptr_service = next(
|
||||
(s for s in services if s.name == "test_char_ptr_lambda"), None
|
||||
)
|
||||
assert char_ptr_service is not None, "test_char_ptr_lambda service not found"
|
||||
|
||||
# Execute all four services to test different lambda return types
|
||||
client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"})
|
||||
client.execute_service(int_service, {"input_number": 42})
|
||||
client.execute_service(float_service, {"input_float": 3.14})
|
||||
client.execute_service(
|
||||
char_ptr_service, {"input_number": 123, "input_string": "test_string"}
|
||||
)
|
||||
|
||||
# Wait for all service log messages
|
||||
# This confirms the lambdas compiled successfully and executed
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(
|
||||
string_called_future,
|
||||
int_called_future,
|
||||
float_called_future,
|
||||
char_ptr_called_future,
|
||||
),
|
||||
timeout=5.0,
|
||||
)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"One or more service log messages not received - lambda may have failed to compile or execute"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user